summaryrefslogtreecommitdiffstats
path: root/editor/libeditor
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /editor/libeditor
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'editor/libeditor')
-rw-r--r--editor/libeditor/AutoRangeArray.cpp1276
-rw-r--r--editor/libeditor/AutoRangeArray.h476
-rw-r--r--editor/libeditor/CSSEditUtils.cpp1305
-rw-r--r--editor/libeditor/CSSEditUtils.h389
-rw-r--r--editor/libeditor/ChangeAttributeTransaction.cpp142
-rw-r--r--editor/libeditor/ChangeAttributeTransaction.h91
-rw-r--r--editor/libeditor/ChangeStyleTransaction.cpp333
-rw-r--r--editor/libeditor/ChangeStyleTransaction.h133
-rw-r--r--editor/libeditor/CompositionTransaction.cpp435
-rw-r--r--editor/libeditor/CompositionTransaction.h102
-rw-r--r--editor/libeditor/DeleteContentTransactionBase.cpp24
-rw-r--r--editor/libeditor/DeleteContentTransactionBase.h46
-rw-r--r--editor/libeditor/DeleteMultipleRangesTransaction.cpp122
-rw-r--r--editor/libeditor/DeleteMultipleRangesTransaction.h54
-rw-r--r--editor/libeditor/DeleteNodeTransaction.cpp167
-rw-r--r--editor/libeditor/DeleteNodeTransaction.h77
-rw-r--r--editor/libeditor/DeleteRangeTransaction.cpp399
-rw-r--r--editor/libeditor/DeleteRangeTransaction.h156
-rw-r--r--editor/libeditor/DeleteTextTransaction.cpp194
-rw-r--r--editor/libeditor/DeleteTextTransaction.h99
-rw-r--r--editor/libeditor/EditAction.h867
-rw-r--r--editor/libeditor/EditAggregateTransaction.cpp124
-rw-r--r--editor/libeditor/EditAggregateTransaction.h57
-rw-r--r--editor/libeditor/EditTransactionBase.cpp97
-rw-r--r--editor/libeditor/EditTransactionBase.h86
-rw-r--r--editor/libeditor/EditorBase.cpp6908
-rw-r--r--editor/libeditor/EditorBase.h2945
-rw-r--r--editor/libeditor/EditorCommands.cpp981
-rw-r--r--editor/libeditor/EditorCommands.h931
-rw-r--r--editor/libeditor/EditorController.cpp140
-rw-r--r--editor/libeditor/EditorController.h30
-rw-r--r--editor/libeditor/EditorDOMPoint.h1629
-rw-r--r--editor/libeditor/EditorEventListener.cpp1237
-rw-r--r--editor/libeditor/EditorEventListener.h128
-rw-r--r--editor/libeditor/EditorForwards.h168
-rw-r--r--editor/libeditor/EditorUtils.cpp445
-rw-r--r--editor/libeditor/EditorUtils.h477
-rw-r--r--editor/libeditor/HTMLAbsPositionEditor.cpp993
-rw-r--r--editor/libeditor/HTMLAnonymousNodeEditor.cpp606
-rw-r--r--editor/libeditor/HTMLEditHelpers.cpp174
-rw-r--r--editor/libeditor/HTMLEditHelpers.h1209
-rw-r--r--editor/libeditor/HTMLEditSubActionHandler.cpp12090
-rw-r--r--editor/libeditor/HTMLEditUtils.cpp2710
-rw-r--r--editor/libeditor/HTMLEditUtils.h2788
-rw-r--r--editor/libeditor/HTMLEditor.cpp7324
-rw-r--r--editor/libeditor/HTMLEditor.h4726
-rw-r--r--editor/libeditor/HTMLEditorCommands.cpp1349
-rw-r--r--editor/libeditor/HTMLEditorController.cpp144
-rw-r--r--editor/libeditor/HTMLEditorController.h26
-rw-r--r--editor/libeditor/HTMLEditorDataTransfer.cpp4353
-rw-r--r--editor/libeditor/HTMLEditorDeleteHandler.cpp7062
-rw-r--r--editor/libeditor/HTMLEditorDocumentCommands.cpp478
-rw-r--r--editor/libeditor/HTMLEditorEventListener.cpp437
-rw-r--r--editor/libeditor/HTMLEditorEventListener.h99
-rw-r--r--editor/libeditor/HTMLEditorInlines.h148
-rw-r--r--editor/libeditor/HTMLEditorNestedClasses.h522
-rw-r--r--editor/libeditor/HTMLEditorObjectResizer.cpp1493
-rw-r--r--editor/libeditor/HTMLEditorState.cpp705
-rw-r--r--editor/libeditor/HTMLInlineTableEditor.cpp493
-rw-r--r--editor/libeditor/HTMLStyleEditor.cpp4452
-rw-r--r--editor/libeditor/HTMLTableEditor.cpp4601
-rw-r--r--editor/libeditor/InsertNodeTransaction.cpp184
-rw-r--r--editor/libeditor/InsertNodeTransaction.h91
-rw-r--r--editor/libeditor/InsertTextTransaction.cpp176
-rw-r--r--editor/libeditor/InsertTextTransaction.h93
-rw-r--r--editor/libeditor/InternetCiter.cpp293
-rw-r--r--editor/libeditor/InternetCiter.h28
-rw-r--r--editor/libeditor/JoinNodesTransaction.cpp185
-rw-r--r--editor/libeditor/JoinNodesTransaction.h107
-rw-r--r--editor/libeditor/ManualNAC.h118
-rw-r--r--editor/libeditor/MoveNodeTransaction.cpp300
-rw-r--r--editor/libeditor/MoveNodeTransaction.h119
-rw-r--r--editor/libeditor/PendingStyles.cpp503
-rw-r--r--editor/libeditor/PendingStyles.h339
-rw-r--r--editor/libeditor/PlaceholderTransaction.cpp351
-rw-r--r--editor/libeditor/PlaceholderTransaction.h100
-rw-r--r--editor/libeditor/ReplaceTextTransaction.cpp186
-rw-r--r--editor/libeditor/ReplaceTextTransaction.h95
-rw-r--r--editor/libeditor/SelectionState.cpp591
-rw-r--r--editor/libeditor/SelectionState.h622
-rw-r--r--editor/libeditor/SplitNodeTransaction.cpp219
-rw-r--r--editor/libeditor/SplitNodeTransaction.h92
-rw-r--r--editor/libeditor/TextEditSubActionHandler.cpp815
-rw-r--r--editor/libeditor/TextEditor.cpp1224
-rw-r--r--editor/libeditor/TextEditor.h633
-rw-r--r--editor/libeditor/TextEditorDataTransfer.cpp279
-rw-r--r--editor/libeditor/WSRunObject.cpp4471
-rw-r--r--editor/libeditor/WSRunObject.h1685
-rw-r--r--editor/libeditor/crashtests/1057677.html9
-rw-r--r--editor/libeditor/crashtests/1128787.html17
-rw-r--r--editor/libeditor/crashtests/1134545.html26
-rw-r--r--editor/libeditor/crashtests/1158452.html10
-rw-r--r--editor/libeditor/crashtests/1158651.html18
-rw-r--r--editor/libeditor/crashtests/1244894.xhtml21
-rw-r--r--editor/libeditor/crashtests/1264921.html1
-rw-r--r--editor/libeditor/crashtests/1272490.html20
-rw-r--r--editor/libeditor/crashtests/1274050.html17
-rw-r--r--editor/libeditor/crashtests/1317704.html42
-rw-r--r--editor/libeditor/crashtests/1317718.html14
-rw-r--r--editor/libeditor/crashtests/1324505.html24
-rw-r--r--editor/libeditor/crashtests/1343918.html21
-rw-r--r--editor/libeditor/crashtests/1344097.html20
-rw-r--r--editor/libeditor/crashtests/1345015.html28
-rw-r--r--editor/libeditor/crashtests/1348851.html19
-rw-r--r--editor/libeditor/crashtests/1350772.html16
-rw-r--r--editor/libeditor/crashtests/1364133.html42
-rw-r--r--editor/libeditor/crashtests/1366176.html30
-rw-r--r--editor/libeditor/crashtests/1375131.html33
-rw-r--r--editor/libeditor/crashtests/1381541.html20
-rw-r--r--editor/libeditor/crashtests/1383747.html15
-rw-r--r--editor/libeditor/crashtests/1383755.html26
-rw-r--r--editor/libeditor/crashtests/1383763.html17
-rw-r--r--editor/libeditor/crashtests/1384161.html17
-rw-r--r--editor/libeditor/crashtests/1388075.html60
-rw-r--r--editor/libeditor/crashtests/1393171.html10
-rw-r--r--editor/libeditor/crashtests/1402196.html29
-rw-r--r--editor/libeditor/crashtests/1402469.html22
-rw-r--r--editor/libeditor/crashtests/1402526.html24
-rw-r--r--editor/libeditor/crashtests/1402904.html38
-rw-r--r--editor/libeditor/crashtests/1405747.html40
-rw-r--r--editor/libeditor/crashtests/1405897.html23
-rw-r--r--editor/libeditor/crashtests/1408170.html40
-rw-r--r--editor/libeditor/crashtests/1414581.html31
-rw-r--r--editor/libeditor/crashtests/1415231.html26
-rw-r--r--editor/libeditor/crashtests/1423767.html17
-rw-r--r--editor/libeditor/crashtests/1423776.html25
-rw-r--r--editor/libeditor/crashtests/1424450.html51
-rw-r--r--editor/libeditor/crashtests/1425091.html25
-rw-r--r--editor/libeditor/crashtests/1426709.html27
-rw-r--r--editor/libeditor/crashtests/1429523.html18
-rw-r--r--editor/libeditor/crashtests/1429523.xhtml18
-rw-r--r--editor/libeditor/crashtests/1441619.html12
-rw-r--r--editor/libeditor/crashtests/1443664.html15
-rw-r--r--editor/libeditor/crashtests/1444630.html29
-rw-r--r--editor/libeditor/crashtests/1446451.html13
-rw-r--r--editor/libeditor/crashtests/1464251.html25
-rw-r--r--editor/libeditor/crashtests/1470926.html9
-rw-r--r--editor/libeditor/crashtests/1474978.html21
-rw-r--r--editor/libeditor/crashtests/1517028.html23
-rw-r--r--editor/libeditor/crashtests/1525481.html23
-rw-r--r--editor/libeditor/crashtests/1533913.html19
-rw-r--r--editor/libeditor/crashtests/1534394.html29
-rw-r--r--editor/libeditor/crashtests/1547897.html25
-rw-r--r--editor/libeditor/crashtests/1547898.html16
-rw-r--r--editor/libeditor/crashtests/1556799.html18
-rw-r--r--editor/libeditor/crashtests/1574544.html17
-rw-r--r--editor/libeditor/crashtests/1578916.html28
-rw-r--r--editor/libeditor/crashtests/1579934.html39
-rw-r--r--editor/libeditor/crashtests/1581246.html21
-rw-r--r--editor/libeditor/crashtests/1596516.html18
-rw-r--r--editor/libeditor/crashtests/1605741.html14
-rw-r--r--editor/libeditor/crashtests/1613521.html16
-rw-r--r--editor/libeditor/crashtests/1618906.html17
-rw-r--r--editor/libeditor/crashtests/1623166.html21
-rw-r--r--editor/libeditor/crashtests/1623913.html30
-rw-r--r--editor/libeditor/crashtests/1624005.html14
-rw-r--r--editor/libeditor/crashtests/1624007.html19
-rw-r--r--editor/libeditor/crashtests/1624011.html12
-rw-r--r--editor/libeditor/crashtests/1626002.html27
-rw-r--r--editor/libeditor/crashtests/1636541.html14
-rw-r--r--editor/libeditor/crashtests/1644903.html19
-rw-r--r--editor/libeditor/crashtests/1645983-1.html11
-rw-r--r--editor/libeditor/crashtests/1645983-2.html14
-rw-r--r--editor/libeditor/crashtests/1648564.html29
-rw-r--r--editor/libeditor/crashtests/1655508.html34
-rw-r--r--editor/libeditor/crashtests/1655539.html15
-rw-r--r--editor/libeditor/crashtests/1655988.html29
-rw-r--r--editor/libeditor/crashtests/1659717.html14
-rw-r--r--editor/libeditor/crashtests/1663725.html18
-rw-r--r--editor/libeditor/crashtests/1666556.html28
-rw-r--r--editor/libeditor/crashtests/1677566.html31
-rw-r--r--editor/libeditor/crashtests/1691051.html15
-rw-r--r--editor/libeditor/crashtests/1699866.html22
-rw-r--r--editor/libeditor/crashtests/1701348.html10
-rw-r--r--editor/libeditor/crashtests/1707630.html16
-rw-r--r--editor/libeditor/crashtests/336081-1.xhtml52
-rw-r--r--editor/libeditor/crashtests/403965-1.xhtml7
-rw-r--r--editor/libeditor/crashtests/428489-1.html8
-rw-r--r--editor/libeditor/crashtests/429586-1.html8
-rw-r--r--editor/libeditor/crashtests/431086-1.xhtml22
-rw-r--r--editor/libeditor/crashtests/475132-1.xhtml21
-rw-r--r--editor/libeditor/crashtests/503709-1.xhtml11
-rw-r--r--editor/libeditor/crashtests/513375-1.xhtml19
-rw-r--r--editor/libeditor/crashtests/535632-1.xhtml1
-rw-r--r--editor/libeditor/crashtests/574558-1.xhtml15
-rw-r--r--editor/libeditor/crashtests/580151-1.xhtml26
-rw-r--r--editor/libeditor/crashtests/582138-1.xhtml10
-rw-r--r--editor/libeditor/crashtests/633709.xhtml68
-rw-r--r--editor/libeditor/crashtests/639736-1.xhtml19
-rw-r--r--editor/libeditor/crashtests/713427-2.xhtml28
-rw-r--r--editor/libeditor/crashtests/766360.html18
-rw-r--r--editor/libeditor/crashtests/766387.html21
-rw-r--r--editor/libeditor/crashtests/766413.html42
-rw-r--r--editor/libeditor/crashtests/766795.html21
-rw-r--r--editor/libeditor/crashtests/766845.xhtml27
-rw-r--r--editor/libeditor/crashtests/767169.html23
-rw-r--r--editor/libeditor/crashtests/768748.html16
-rw-r--r--editor/libeditor/crashtests/768765.html36
-rw-r--r--editor/libeditor/crashtests/769008-1.html23
-rw-r--r--editor/libeditor/crashtests/769967.xhtml16
-rw-r--r--editor/libeditor/crashtests/771749.html21
-rw-r--r--editor/libeditor/crashtests/772282.html27
-rw-r--r--editor/libeditor/crashtests/776323.html18
-rw-r--r--editor/libeditor/crashtests/793866.html21
-rw-r--r--editor/libeditor/crashtests/848644.html2
-rw-r--r--editor/libeditor/crashtests/crashtests.list117
-rw-r--r--editor/libeditor/moz.build98
-rw-r--r--editor/libeditor/tests/browser.toml10
-rw-r--r--editor/libeditor/tests/browser_bug527935.js78
-rw-r--r--editor/libeditor/tests/browser_content_command_insert_text.js271
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/LICENSE202
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/README58
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla17
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js43
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/current_revision1
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html11
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js1069
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html1081
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream16
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/LICENSE202
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/README58
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla27
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js1768
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/current_revision1
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/platformFailures.js28
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py0
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py25
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py107
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css116
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html11
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html17
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html11
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css66
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js436
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js489
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js456
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js269
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js5
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js6184
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js383
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js416
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js227
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html138
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html107
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py17
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py364
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py244
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py273
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py210
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py330
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py315
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py285
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py215
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py214
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py575
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py226
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py429
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py801
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py462
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py226
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html103
-rw-r--r--editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream19
-rw-r--r--editor/libeditor/tests/browserscope/mochitest.toml60
-rw-r--r--editor/libeditor/tests/browserscope/test_richtext.html48
-rw-r--r--editor/libeditor/tests/browserscope/test_richtext2.html238
-rw-r--r--editor/libeditor/tests/bug527935.html11
-rw-r--r--editor/libeditor/tests/bug527935_2.html1
-rw-r--r--editor/libeditor/tests/chrome.toml31
-rw-r--r--editor/libeditor/tests/data/cfhtml-chromium.txtbin0 -> 856 bytes
-rw-r--r--editor/libeditor/tests/data/cfhtml-firefox.txtbin0 -> 266 bytes
-rw-r--r--editor/libeditor/tests/data/cfhtml-ie.txtbin0 -> 1080 bytes
-rw-r--r--editor/libeditor/tests/data/cfhtml-nocontext.txt18
-rw-r--r--editor/libeditor/tests/data/cfhtml-ooo.txtbin0 -> 649 bytes
-rw-r--r--editor/libeditor/tests/file_bug289384-1.html1
-rw-r--r--editor/libeditor/tests/file_bug289384-2.html1
-rw-r--r--editor/libeditor/tests/file_bug549262.html10
-rw-r--r--editor/libeditor/tests/file_bug586662.html7
-rw-r--r--editor/libeditor/tests/file_bug611182.html1
-rw-r--r--editor/libeditor/tests/file_bug611182.sjs254
-rw-r--r--editor/libeditor/tests/file_bug635636.xhtml3
-rw-r--r--editor/libeditor/tests/file_bug635636_2.html1
-rw-r--r--editor/libeditor/tests/file_bug674770-1.html5
-rw-r--r--editor/libeditor/tests/file_bug795418-2.sjs10
-rw-r--r--editor/libeditor/tests/file_bug915962.html13
-rw-r--r--editor/libeditor/tests/file_bug966155.html1
-rw-r--r--editor/libeditor/tests/file_bug966552.html1
-rw-r--r--editor/libeditor/tests/file_sanitizer_on_paste.sjs19
-rw-r--r--editor/libeditor/tests/file_select_all_without_body.html38
-rw-r--r--editor/libeditor/tests/green.pngbin0 -> 334 bytes
-rw-r--r--editor/libeditor/tests/mochitest.toml628
-rw-r--r--editor/libeditor/tests/test_CF_HTML_clipboard.html149
-rw-r--r--editor/libeditor/tests/test_abs_positioner_appearance.html177
-rw-r--r--editor/libeditor/tests/test_abs_positioner_hidden_during_dragging.html102
-rw-r--r--editor/libeditor/tests/test_abs_positioner_positioning_elements.html196
-rw-r--r--editor/libeditor/tests/test_backspace_vs.html128
-rw-r--r--editor/libeditor/tests/test_bug1026397.html101
-rw-r--r--editor/libeditor/tests/test_bug1053048.html71
-rw-r--r--editor/libeditor/tests/test_bug1068979.html72
-rw-r--r--editor/libeditor/tests/test_bug1094000.html147
-rw-r--r--editor/libeditor/tests/test_bug1102906.html51
-rw-r--r--editor/libeditor/tests/test_bug1109465.html65
-rw-r--r--editor/libeditor/tests/test_bug1130651.html17
-rw-r--r--editor/libeditor/tests/test_bug1140105.html64
-rw-r--r--editor/libeditor/tests/test_bug1140617.html37
-rw-r--r--editor/libeditor/tests/test_bug1151186.html69
-rw-r--r--editor/libeditor/tests/test_bug1153237.html48
-rw-r--r--editor/libeditor/tests/test_bug1162952.html43
-rw-r--r--editor/libeditor/tests/test_bug1181130-1.html50
-rw-r--r--editor/libeditor/tests/test_bug1181130-2.html44
-rw-r--r--editor/libeditor/tests/test_bug1186799.html78
-rw-r--r--editor/libeditor/tests/test_bug1230473.html103
-rw-r--r--editor/libeditor/tests/test_bug1247483.html61
-rw-r--r--editor/libeditor/tests/test_bug1248128.html52
-rw-r--r--editor/libeditor/tests/test_bug1248185.html56
-rw-r--r--editor/libeditor/tests/test_bug1248186.html56
-rw-r--r--editor/libeditor/tests/test_bug1250010.html87
-rw-r--r--editor/libeditor/tests/test_bug1257363.html180
-rw-r--r--editor/libeditor/tests/test_bug1258085.html66
-rw-r--r--editor/libeditor/tests/test_bug1268736.html62
-rw-r--r--editor/libeditor/tests/test_bug1270235.html46
-rw-r--r--editor/libeditor/tests/test_bug1306532.html68
-rw-r--r--editor/libeditor/tests/test_bug1310912.html242
-rw-r--r--editor/libeditor/tests/test_bug1314790.html57
-rw-r--r--editor/libeditor/tests/test_bug1315065.html145
-rw-r--r--editor/libeditor/tests/test_bug1316302.html50
-rw-r--r--editor/libeditor/tests/test_bug1328023.html61
-rw-r--r--editor/libeditor/tests/test_bug1330796.html95
-rw-r--r--editor/libeditor/tests/test_bug1332876.html57
-rw-r--r--editor/libeditor/tests/test_bug1352799.html84
-rw-r--r--editor/libeditor/tests/test_bug1355792.html17
-rw-r--r--editor/libeditor/tests/test_bug1358025.html98
-rw-r--r--editor/libeditor/tests/test_bug1361008.html61
-rw-r--r--editor/libeditor/tests/test_bug1361052.html50
-rw-r--r--editor/libeditor/tests/test_bug1385905.html51
-rw-r--r--editor/libeditor/tests/test_bug1390562.html67
-rw-r--r--editor/libeditor/tests/test_bug1394758.html60
-rw-r--r--editor/libeditor/tests/test_bug1397412.xhtml65
-rw-r--r--editor/libeditor/tests/test_bug1399722.html38
-rw-r--r--editor/libeditor/tests/test_bug1406726.html126
-rw-r--r--editor/libeditor/tests/test_bug1409520.html45
-rw-r--r--editor/libeditor/tests/test_bug1425997.html63
-rw-r--r--editor/libeditor/tests/test_bug1543312.html70
-rw-r--r--editor/libeditor/tests/test_bug1568996.html67
-rw-r--r--editor/libeditor/tests/test_bug1574596.html64
-rw-r--r--editor/libeditor/tests/test_bug1581337.html33
-rw-r--r--editor/libeditor/tests/test_bug1619852.html34
-rw-r--r--editor/libeditor/tests/test_bug1620778.html27
-rw-r--r--editor/libeditor/tests/test_bug1649005.html49
-rw-r--r--editor/libeditor/tests/test_bug1659276.html78
-rw-r--r--editor/libeditor/tests/test_bug1704381.html48
-rw-r--r--editor/libeditor/tests/test_bug200416.html15
-rw-r--r--editor/libeditor/tests/test_bug289384.html48
-rw-r--r--editor/libeditor/tests/test_bug290026.html52
-rw-r--r--editor/libeditor/tests/test_bug291780.html49
-rw-r--r--editor/libeditor/tests/test_bug309731.html58
-rw-r--r--editor/libeditor/tests/test_bug316447.html16
-rw-r--r--editor/libeditor/tests/test_bug318065.html82
-rw-r--r--editor/libeditor/tests/test_bug332636.html75
-rw-r--r--editor/libeditor/tests/test_bug332636.html^headers^1
-rw-r--r--editor/libeditor/tests/test_bug358033.html41
-rw-r--r--editor/libeditor/tests/test_bug372345.html58
-rw-r--r--editor/libeditor/tests/test_bug404320.html87
-rw-r--r--editor/libeditor/tests/test_bug408231.html233
-rw-r--r--editor/libeditor/tests/test_bug410986.html80
-rw-r--r--editor/libeditor/tests/test_bug414526.html234
-rw-r--r--editor/libeditor/tests/test_bug417418.html78
-rw-r--r--editor/libeditor/tests/test_bug426246.html71
-rw-r--r--editor/libeditor/tests/test_bug430392.html171
-rw-r--r--editor/libeditor/tests/test_bug439808.html37
-rw-r--r--editor/libeditor/tests/test_bug442186.html103
-rw-r--r--editor/libeditor/tests/test_bug455992.html102
-rw-r--r--editor/libeditor/tests/test_bug456244.html69
-rw-r--r--editor/libeditor/tests/test_bug460740.html124
-rw-r--r--editor/libeditor/tests/test_bug46555.html47
-rw-r--r--editor/libeditor/tests/test_bug471319.html77
-rw-r--r--editor/libeditor/tests/test_bug471722.html74
-rw-r--r--editor/libeditor/tests/test_bug478725.html130
-rw-r--r--editor/libeditor/tests/test_bug480647.html110
-rw-r--r--editor/libeditor/tests/test_bug480972.html97
-rw-r--r--editor/libeditor/tests/test_bug483651.html52
-rw-r--r--editor/libeditor/tests/test_bug489202.xhtml73
-rw-r--r--editor/libeditor/tests/test_bug490879.html54
-rw-r--r--editor/libeditor/tests/test_bug502673.html97
-rw-r--r--editor/libeditor/tests/test_bug514156.html46
-rw-r--r--editor/libeditor/tests/test_bug520189.html618
-rw-r--r--editor/libeditor/tests/test_bug525389.html207
-rw-r--r--editor/libeditor/tests/test_bug537046.html49
-rw-r--r--editor/libeditor/tests/test_bug549262.html160
-rw-r--r--editor/libeditor/tests/test_bug550434.html42
-rw-r--r--editor/libeditor/tests/test_bug551704.html126
-rw-r--r--editor/libeditor/tests/test_bug552782.html46
-rw-r--r--editor/libeditor/tests/test_bug567213.html58
-rw-r--r--editor/libeditor/tests/test_bug569988.html103
-rw-r--r--editor/libeditor/tests/test_bug570144.html123
-rw-r--r--editor/libeditor/tests/test_bug578771.html63
-rw-r--r--editor/libeditor/tests/test_bug586662.html62
-rw-r--r--editor/libeditor/tests/test_bug590554.html36
-rw-r--r--editor/libeditor/tests/test_bug592592.html72
-rw-r--r--editor/libeditor/tests/test_bug596001.html57
-rw-r--r--editor/libeditor/tests/test_bug596506.html55
-rw-r--r--editor/libeditor/tests/test_bug597331.html73
-rw-r--r--editor/libeditor/tests/test_bug597784.html37
-rw-r--r--editor/libeditor/tests/test_bug599322.html58
-rw-r--r--editor/libeditor/tests/test_bug599983.html16
-rw-r--r--editor/libeditor/tests/test_bug599983.xhtml67
-rw-r--r--editor/libeditor/tests/test_bug600570.html81
-rw-r--r--editor/libeditor/tests/test_bug603556.html53
-rw-r--r--editor/libeditor/tests/test_bug604532.html42
-rw-r--r--editor/libeditor/tests/test_bug607584.html41
-rw-r--r--editor/libeditor/tests/test_bug607584.xhtml112
-rw-r--r--editor/libeditor/tests/test_bug611182.html105
-rw-r--r--editor/libeditor/tests/test_bug612128.html42
-rw-r--r--editor/libeditor/tests/test_bug612447.html74
-rw-r--r--editor/libeditor/tests/test_bug616590.xhtml101
-rw-r--r--editor/libeditor/tests/test_bug622371.html44
-rw-r--r--editor/libeditor/tests/test_bug625452.html66
-rw-r--r--editor/libeditor/tests/test_bug629172.html105
-rw-r--r--editor/libeditor/tests/test_bug629845.html58
-rw-r--r--editor/libeditor/tests/test_bug635636.html63
-rw-r--r--editor/libeditor/tests/test_bug638596.html34
-rw-r--r--editor/libeditor/tests/test_bug641466.html48
-rw-r--r--editor/libeditor/tests/test_bug645914.html63
-rw-r--r--editor/libeditor/tests/test_bug646194.html36
-rw-r--r--editor/libeditor/tests/test_bug668599.html73
-rw-r--r--editor/libeditor/tests/test_bug674770-1.html85
-rw-r--r--editor/libeditor/tests/test_bug674770-2.html374
-rw-r--r--editor/libeditor/tests/test_bug674861.html194
-rw-r--r--editor/libeditor/tests/test_bug676401.html129
-rw-r--r--editor/libeditor/tests/test_bug677752.html107
-rw-r--r--editor/libeditor/tests/test_bug681229.html50
-rw-r--r--editor/libeditor/tests/test_bug686203.html50
-rw-r--r--editor/libeditor/tests/test_bug692520.html41
-rw-r--r--editor/libeditor/tests/test_bug697842.html114
-rw-r--r--editor/libeditor/tests/test_bug725069.html34
-rw-r--r--editor/libeditor/tests/test_bug735059.html22
-rw-r--r--editor/libeditor/tests/test_bug738366.html24
-rw-r--r--editor/libeditor/tests/test_bug740784.html46
-rw-r--r--editor/libeditor/tests/test_bug742261.html14
-rw-r--r--editor/libeditor/tests/test_bug757371.html26
-rw-r--r--editor/libeditor/tests/test_bug757771.html31
-rw-r--r--editor/libeditor/tests/test_bug772796.html487
-rw-r--r--editor/libeditor/tests/test_bug773262.html63
-rw-r--r--editor/libeditor/tests/test_bug780035.html23
-rw-r--r--editor/libeditor/tests/test_bug780908.xhtml110
-rw-r--r--editor/libeditor/tests/test_bug787432.html17
-rw-r--r--editor/libeditor/tests/test_bug790475.html90
-rw-r--r--editor/libeditor/tests/test_bug795418-2.html87
-rw-r--r--editor/libeditor/tests/test_bug795418-3.html89
-rw-r--r--editor/libeditor/tests/test_bug795418-4.html70
-rw-r--r--editor/libeditor/tests/test_bug795418-5.html69
-rw-r--r--editor/libeditor/tests/test_bug795418-6.html69
-rw-r--r--editor/libeditor/tests/test_bug795418.html70
-rw-r--r--editor/libeditor/tests/test_bug795785.html139
-rw-r--r--editor/libeditor/tests/test_bug796839.html17
-rw-r--r--editor/libeditor/tests/test_bug830600.html97
-rw-r--r--editor/libeditor/tests/test_bug832025.html43
-rw-r--r--editor/libeditor/tests/test_bug850043.html59
-rw-r--r--editor/libeditor/tests/test_bug857487.html68
-rw-r--r--editor/libeditor/tests/test_bug858918.html16
-rw-r--r--editor/libeditor/tests/test_bug915962.html120
-rw-r--r--editor/libeditor/tests/test_bug966155.html54
-rw-r--r--editor/libeditor/tests/test_bug966552.html42
-rw-r--r--editor/libeditor/tests/test_bug974309.html74
-rw-r--r--editor/libeditor/tests/test_bug998188.html51
-rw-r--r--editor/libeditor/tests/test_can_undo_after_setting_value.xhtml38
-rw-r--r--editor/libeditor/tests/test_cannot_undo_after_reinitializing_editor.html50
-rw-r--r--editor/libeditor/tests/test_caret_move_in_vertical_content.html209
-rw-r--r--editor/libeditor/tests/test_cmd_absPos.html296
-rw-r--r--editor/libeditor/tests/test_cmd_backgroundColor.html223
-rw-r--r--editor/libeditor/tests/test_cmd_fontFace_with_empty_string.html30
-rw-r--r--editor/libeditor/tests/test_cmd_fontFace_with_tt.html73
-rw-r--r--editor/libeditor/tests/test_cmd_increaseFont.html22
-rw-r--r--editor/libeditor/tests/test_cmd_paragraphState.html55
-rw-r--r--editor/libeditor/tests/test_composition_event_created_in_chrome.html76
-rw-r--r--editor/libeditor/tests/test_composition_with_highlight_in_texteditor.html62
-rw-r--r--editor/libeditor/tests/test_contenteditable_copy_empty_selection.html33
-rw-r--r--editor/libeditor/tests/test_contenteditable_focus.html335
-rw-r--r--editor/libeditor/tests/test_contenteditable_text_input_handling.html317
-rw-r--r--editor/libeditor/tests/test_cut_copy_delete_command_enabled.html227
-rw-r--r--editor/libeditor/tests/test_cut_copy_delete_command_enabled.xhtml215
-rw-r--r--editor/libeditor/tests/test_cut_copy_password.html92
-rw-r--r--editor/libeditor/tests/test_defaultParagraphSeparatorBR_between_blocks.html62
-rw-r--r--editor/libeditor/tests/test_doc_scrollbar_toggled_designMode_on_mousedown.html64
-rw-r--r--editor/libeditor/tests/test_dom_input_event_on_htmleditor.html1694
-rw-r--r--editor/libeditor/tests/test_dom_input_event_on_texteditor.html926
-rw-r--r--editor/libeditor/tests/test_dragdrop.html3451
-rw-r--r--editor/libeditor/tests/test_execCommandPaste_noTarget.html45
-rw-r--r--editor/libeditor/tests/test_focus_caret_navigation_between_nested_editors.html207
-rw-r--r--editor/libeditor/tests/test_focused_document_element_becoming_editable.html157
-rw-r--r--editor/libeditor/tests/test_handle_new_lines.html129
-rw-r--r--editor/libeditor/tests/test_htmleditor_keyevent_handling.html766
-rw-r--r--editor/libeditor/tests/test_htmleditor_tab_key_handling.html112
-rw-r--r--editor/libeditor/tests/test_htmleditor_toggle_text_direction.html73
-rw-r--r--editor/libeditor/tests/test_initial_selection_and_caret_of_designMode.html57
-rw-r--r--editor/libeditor/tests/test_inlineTableEditing.html62
-rw-r--r--editor/libeditor/tests/test_inline_style_cache.html151
-rw-r--r--editor/libeditor/tests/test_insertHTML_starting_with_multiple_comment_nodes.html40
-rw-r--r--editor/libeditor/tests/test_insertParagraph_in_h2_and_li.html167
-rw-r--r--editor/libeditor/tests/test_insertParagraph_in_inline_editing_host.html67
-rw-r--r--editor/libeditor/tests/test_insertText_around_text_node_in_plaintext_mode.html92
-rw-r--r--editor/libeditor/tests/test_join_split_node_direction_change_command.html57
-rw-r--r--editor/libeditor/tests/test_keypress_untrusted_event.html105
-rw-r--r--editor/libeditor/tests/test_label_contenteditable.html18
-rw-r--r--editor/libeditor/tests/test_middle_click_paste.html680
-rw-r--r--editor/libeditor/tests/test_native_key_bindings_in_shadow.html50
-rw-r--r--editor/libeditor/tests/test_nested_editor.html77
-rw-r--r--editor/libeditor/tests/test_new_plaintext_mail_with_plaintext_signature.html94
-rw-r--r--editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html322
-rw-r--r--editor/libeditor/tests/test_nsIEditorMailSupport_insertTextWithQuotations.html104
-rw-r--r--editor/libeditor/tests/test_nsIEditor_beginningOfDocument.html119
-rw-r--r--editor/libeditor/tests/test_nsIEditor_canUndo_canRedo.html183
-rw-r--r--editor/libeditor/tests/test_nsIEditor_clearUndoRedo.html125
-rw-r--r--editor/libeditor/tests/test_nsIEditor_deleteNode.html242
-rw-r--r--editor/libeditor/tests/test_nsIEditor_documentCharacterSet.html113
-rw-r--r--editor/libeditor/tests/test_nsIEditor_documentIsEmpty.html150
-rw-r--r--editor/libeditor/tests/test_nsIEditor_insertLineBreak.html416
-rw-r--r--editor/libeditor/tests/test_nsIEditor_insertNode.html214
-rw-r--r--editor/libeditor/tests/test_nsIEditor_isSelectionEditable.html16
-rw-r--r--editor/libeditor/tests/test_nsIEditor_outputToString.html135
-rw-r--r--editor/libeditor/tests/test_nsIEditor_undoAll.html115
-rw-r--r--editor/libeditor/tests/test_nsIEditor_undoRedoEnabled.html91
-rw-r--r--editor/libeditor/tests/test_nsIHTMLEditor_getElementOrParentByTagName.html449
-rw-r--r--editor/libeditor/tests/test_nsIHTMLEditor_getParagraphState.html156
-rw-r--r--editor/libeditor/tests/test_nsIHTMLEditor_getSelectedElement.html816
-rw-r--r--editor/libeditor/tests/test_nsIHTMLEditor_insertElementAtSelection.html185
-rw-r--r--editor/libeditor/tests/test_nsIHTMLEditor_removeInlineProperty.html420
-rw-r--r--editor/libeditor/tests/test_nsIHTMLEditor_selectElement.html131
-rw-r--r--editor/libeditor/tests/test_nsIHTMLEditor_setBackgroundColor.html187
-rw-r--r--editor/libeditor/tests/test_nsIHTMLObjectResizer_hideResizers.html54
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html716
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html296
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_deleteTableColumn.html515
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html522
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_getCellAt.html138
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_getCellDataAt.html751
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_getCellIndexes.html91
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_getFirstRow.html105
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_getFirstSelectedCellInTable.html201
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_getSelectedCells.html295
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_getSelectedCellsType.html251
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_getSelectedOrParentTableElement.html283
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_getTableSize.html94
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_insertTableCell.html558
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html547
-rw-r--r--editor/libeditor/tests/test_nsITableEditor_insertTableRow.html432
-rw-r--r--editor/libeditor/tests/test_password_input_with_unmasked_range.html417
-rw-r--r--editor/libeditor/tests/test_password_paste.html65
-rw-r--r--editor/libeditor/tests/test_password_per_word_operation.html148
-rw-r--r--editor/libeditor/tests/test_password_unmask_API.html318
-rw-r--r--editor/libeditor/tests/test_pasteImgFromTransferable.html78
-rw-r--r--editor/libeditor/tests/test_pasteImgTextarea.html19
-rw-r--r--editor/libeditor/tests/test_pasteImgTextarea.xhtml26
-rw-r--r--editor/libeditor/tests/test_paste_as_quote_in_text_control.html48
-rw-r--r--editor/libeditor/tests/test_paste_no_formatting.html214
-rw-r--r--editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html143
-rw-r--r--editor/libeditor/tests/test_pasting_in_root_element.xhtml74
-rw-r--r--editor/libeditor/tests/test_pasting_in_temporarily_created_div_outside_body.html42
-rw-r--r--editor/libeditor/tests/test_pasting_table_rows.html554
-rw-r--r--editor/libeditor/tests/test_pasting_text_longer_than_maxlength.html53
-rw-r--r--editor/libeditor/tests/test_resizers_appearance.html109
-rw-r--r--editor/libeditor/tests/test_resizers_resizing_elements.html299
-rw-r--r--editor/libeditor/tests/test_root_element_replacement.html140
-rw-r--r--editor/libeditor/tests/test_sanitizer_on_paste.html48
-rw-r--r--editor/libeditor/tests/test_select_all_without_body.html26
-rw-r--r--editor/libeditor/tests/test_selection_move_commands.html225
-rw-r--r--editor/libeditor/tests/test_setting_value_longer_than_maxlength_with_setUserInput.html69
-rw-r--r--editor/libeditor/tests/test_spellcheck_pref.html22
-rw-r--r--editor/libeditor/tests/test_state_change_on_reframe.html29
-rw-r--r--editor/libeditor/tests/test_textarea_value_not_include_cr.html95
-rw-r--r--editor/libeditor/tests/test_texteditor_keyevent_handling.html471
-rw-r--r--editor/libeditor/tests/test_texteditor_textnode.html52
-rw-r--r--editor/libeditor/tests/test_texteditor_tripleclick_setvalue.html27
-rw-r--r--editor/libeditor/tests/test_texteditor_wrapping_long_line.html45
-rw-r--r--editor/libeditor/tests/test_typing_at_edge_of_anchor.html476
-rw-r--r--editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html179
-rw-r--r--editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html168
-rw-r--r--editor/libeditor/tests/test_undo_with_editingui.html160
577 files changed, 161217 insertions, 0 deletions
diff --git a/editor/libeditor/AutoRangeArray.cpp b/editor/libeditor/AutoRangeArray.cpp
new file mode 100644
index 0000000000..be89671760
--- /dev/null
+++ b/editor/libeditor/AutoRangeArray.cpp
@@ -0,0 +1,1276 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AutoRangeArray.h"
+
+#include "EditAction.h"
+#include "EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc
+#include "EditorForwards.h" // for CollectChildrenOptions
+#include "HTMLEditUtils.h" // for HTMLEditUtils
+#include "HTMLEditHelpers.h" // for SplitNodeResult
+#include "TextEditor.h" // for TextEditor
+#include "WSRunObject.h" // for WSRunScanner
+
+#include "mozilla/IntegerRange.h" // for IntegerRange
+#include "mozilla/OwningNonNull.h" // for OwningNonNull
+#include "mozilla/dom/Document.h" // for dom::Document
+#include "mozilla/dom/HTMLBRElement.h" // for dom HTMLBRElement
+#include "mozilla/dom/Selection.h" // for dom::Selection
+#include "mozilla/dom/Text.h" // for dom::Text
+
+#include "gfxFontUtils.h" // for gfxFontUtils
+#include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
+#include "nsFrameSelection.h" // for nsFrameSelection
+#include "nsIContent.h" // for nsIContent
+#include "nsINode.h" // for nsINode
+#include "nsRange.h" // for nsRange
+#include "nsTextFragment.h" // for nsTextFragment
+
+namespace mozilla {
+
+using namespace dom;
+
+using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+
+/******************************************************************************
+ * mozilla::AutoRangeArray
+ *****************************************************************************/
+
+template AutoRangeArray::AutoRangeArray(const EditorDOMRange& aRange);
+template AutoRangeArray::AutoRangeArray(const EditorRawDOMRange& aRange);
+template AutoRangeArray::AutoRangeArray(const EditorDOMPoint& aRange);
+template AutoRangeArray::AutoRangeArray(const EditorRawDOMPoint& aRange);
+
+AutoRangeArray::AutoRangeArray(const dom::Selection& aSelection) {
+ Initialize(aSelection);
+}
+
+AutoRangeArray::AutoRangeArray(const AutoRangeArray& aOther)
+ : mAnchorFocusRange(aOther.mAnchorFocusRange),
+ mDirection(aOther.mDirection) {
+ mRanges.SetCapacity(aOther.mRanges.Length());
+ for (const OwningNonNull<nsRange>& range : aOther.mRanges) {
+ RefPtr<nsRange> clonedRange = range->CloneRange();
+ mRanges.AppendElement(std::move(clonedRange));
+ }
+ mAnchorFocusRange = aOther.mAnchorFocusRange;
+}
+
+template <typename PointType>
+AutoRangeArray::AutoRangeArray(const EditorDOMRangeBase<PointType>& aRange) {
+ MOZ_ASSERT(aRange.IsPositionedAndValid());
+ RefPtr<nsRange> range = aRange.CreateRange(IgnoreErrors());
+ if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
+ return;
+ }
+ mRanges.AppendElement(*range);
+ mAnchorFocusRange = std::move(range);
+}
+
+template <typename PT, typename CT>
+AutoRangeArray::AutoRangeArray(const EditorDOMPointBase<PT, CT>& aPoint) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ RefPtr<nsRange> range = aPoint.CreateCollapsedRange(IgnoreErrors());
+ if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
+ return;
+ }
+ mRanges.AppendElement(*range);
+ mAnchorFocusRange = std::move(range);
+}
+
+AutoRangeArray::~AutoRangeArray() {
+ if (mSavedRanges.isSome()) {
+ ClearSavedRanges();
+ }
+}
+
+// static
+bool AutoRangeArray::IsEditableRange(const dom::AbstractRange& aRange,
+ const Element& aEditingHost) {
+ // TODO: Perhaps, we should check whether the start/end boundaries are
+ // first/last point of non-editable element.
+ // See https://github.com/w3c/editing/issues/283#issuecomment-788654850
+ EditorRawDOMPoint atStart(aRange.StartRef());
+ const bool isStartEditable =
+ atStart.IsInContentNode() &&
+ EditorUtils::IsEditableContent(*atStart.ContainerAs<nsIContent>(),
+ EditorUtils::EditorType::HTML) &&
+ !HTMLEditUtils::IsNonEditableReplacedContent(
+ *atStart.ContainerAs<nsIContent>());
+ if (!isStartEditable) {
+ return false;
+ }
+
+ if (aRange.GetStartContainer() != aRange.GetEndContainer()) {
+ EditorRawDOMPoint atEnd(aRange.EndRef());
+ const bool isEndEditable =
+ atEnd.IsInContentNode() &&
+ EditorUtils::IsEditableContent(*atEnd.ContainerAs<nsIContent>(),
+ EditorUtils::EditorType::HTML) &&
+ !HTMLEditUtils::IsNonEditableReplacedContent(
+ *atEnd.ContainerAs<nsIContent>());
+ if (!isEndEditable) {
+ return false;
+ }
+
+ // Now, both start and end points are editable, but if they are in
+ // different editing host, we cannot edit the range.
+ if (atStart.ContainerAs<nsIContent>() != atEnd.ContainerAs<nsIContent>() &&
+ atStart.ContainerAs<nsIContent>()->GetEditingHost() !=
+ atEnd.ContainerAs<nsIContent>()->GetEditingHost()) {
+ return false;
+ }
+ }
+
+ // HTMLEditor does not support modifying outside `<body>` element for now.
+ nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor();
+ return commonAncestor && commonAncestor->IsContent() &&
+ commonAncestor->IsInclusiveDescendantOf(&aEditingHost);
+}
+
+void AutoRangeArray::EnsureOnlyEditableRanges(const Element& aEditingHost) {
+ for (const size_t index : Reversed(IntegerRange(mRanges.Length()))) {
+ const OwningNonNull<nsRange>& range = mRanges[index];
+ if (!AutoRangeArray::IsEditableRange(range, aEditingHost)) {
+ mRanges.RemoveElementAt(index);
+ continue;
+ }
+ // Special handling for `inert` attribute. If anchor node is inert, the
+ // range should be treated as not editable.
+ nsIContent* anchorContent =
+ mDirection == eDirNext
+ ? nsIContent::FromNode(range->GetStartContainer())
+ : nsIContent::FromNode(range->GetEndContainer());
+ if (anchorContent && HTMLEditUtils::ContentIsInert(*anchorContent)) {
+ mRanges.RemoveElementAt(index);
+ continue;
+ }
+ // Additionally, if focus node is inert, the range should be collapsed to
+ // anchor node.
+ nsIContent* focusContent =
+ mDirection == eDirNext
+ ? nsIContent::FromNode(range->GetEndContainer())
+ : nsIContent::FromNode(range->GetStartContainer());
+ if (focusContent && focusContent != anchorContent &&
+ HTMLEditUtils::ContentIsInert(*focusContent)) {
+ range->Collapse(mDirection == eDirNext);
+ }
+ }
+ mAnchorFocusRange = mRanges.IsEmpty() ? nullptr : mRanges.LastElement().get();
+}
+
+void AutoRangeArray::EnsureRangesInTextNode(const Text& aTextNode) {
+ auto GetOffsetInTextNode = [&aTextNode](const nsINode* aNode,
+ uint32_t aOffset) -> uint32_t {
+ MOZ_DIAGNOSTIC_ASSERT(aNode);
+ if (aNode == &aTextNode) {
+ return aOffset;
+ }
+ const nsIContent* anonymousDivElement = aTextNode.GetParent();
+ MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement);
+ MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->IsHTMLElement(nsGkAtoms::div));
+ MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->GetFirstChild() == &aTextNode);
+ if (aNode == anonymousDivElement && aOffset == 0u) {
+ return 0u; // Point before the text node so that use start of the text.
+ }
+ MOZ_DIAGNOSTIC_ASSERT(aNode->IsInclusiveDescendantOf(anonymousDivElement));
+ // Point after the text node so that use end of the text.
+ return aTextNode.TextDataLength();
+ };
+ for (const OwningNonNull<nsRange>& range : mRanges) {
+ if (MOZ_LIKELY(range->GetStartContainer() == &aTextNode &&
+ range->GetEndContainer() == &aTextNode)) {
+ continue;
+ }
+ range->SetStartAndEnd(
+ const_cast<Text*>(&aTextNode),
+ GetOffsetInTextNode(range->GetStartContainer(), range->StartOffset()),
+ const_cast<Text*>(&aTextNode),
+ GetOffsetInTextNode(range->GetEndContainer(), range->EndOffset()));
+ }
+
+ if (MOZ_UNLIKELY(mRanges.Length() >= 2)) {
+ // For avoiding to handle same things in same range, we should drop and
+ // merge unnecessary ranges. Note that the ranges never overlap
+ // because selection ranges are not allowed it so that we need to check only
+ // end offset vs start offset of next one.
+ for (const size_t i : Reversed(IntegerRange(mRanges.Length() - 1u))) {
+ MOZ_ASSERT(mRanges[i]->EndOffset() < mRanges[i + 1]->StartOffset());
+ // XXX Should we delete collapsed range unless the index is 0? Without
+ // Selection API, such situation cannot happen so that `TextEditor`
+ // may behave unexpectedly.
+ if (MOZ_UNLIKELY(mRanges[i]->EndOffset() >=
+ mRanges[i + 1]->StartOffset())) {
+ const uint32_t newEndOffset = mRanges[i + 1]->EndOffset();
+ mRanges.RemoveElementAt(i + 1);
+ if (MOZ_UNLIKELY(NS_WARN_IF(newEndOffset > mRanges[i]->EndOffset()))) {
+ // So, this case shouldn't happen.
+ mRanges[i]->SetStartAndEnd(
+ const_cast<Text*>(&aTextNode), mRanges[i]->StartOffset(),
+ const_cast<Text*>(&aTextNode), newEndOffset);
+ }
+ }
+ }
+ }
+}
+
+Result<nsIEditor::EDirection, nsresult>
+AutoRangeArray::ExtendAnchorFocusRangeFor(
+ const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount) {
+ MOZ_ASSERT(aEditorBase.IsEditActionDataAvailable());
+ MOZ_ASSERT(mAnchorFocusRange);
+ MOZ_ASSERT(mAnchorFocusRange->IsPositioned());
+ MOZ_ASSERT(mAnchorFocusRange->StartRef().IsSet());
+ MOZ_ASSERT(mAnchorFocusRange->EndRef().IsSet());
+
+ if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection(
+ aDirectionAndAmount, *this)) {
+ return aDirectionAndAmount;
+ }
+
+ if (NS_WARN_IF(!aEditorBase.SelectionRef().RangeCount())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // By a preceding call of EnsureOnlyEditableRanges(), anchor/focus range may
+ // have been changed. In that case, we cannot use nsFrameSelection anymore.
+ // FIXME: We should make `nsFrameSelection::CreateRangeExtendedToSomewhere`
+ // work without `Selection` instance.
+ if (MOZ_UNLIKELY(
+ aEditorBase.SelectionRef().GetAnchorFocusRange()->StartRef() !=
+ mAnchorFocusRange->StartRef() ||
+ aEditorBase.SelectionRef().GetAnchorFocusRange()->EndRef() !=
+ mAnchorFocusRange->EndRef())) {
+ return aDirectionAndAmount;
+ }
+
+ RefPtr<nsFrameSelection> frameSelection =
+ aEditorBase.SelectionRef().GetFrameSelection();
+ if (NS_WARN_IF(!frameSelection)) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ RefPtr<Element> editingHost;
+ if (aEditorBase.IsHTMLEditor()) {
+ editingHost = aEditorBase.AsHTMLEditor()->ComputeEditingHost();
+ if (!editingHost) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ Result<RefPtr<nsRange>, nsresult> result(NS_ERROR_UNEXPECTED);
+ nsIEditor::EDirection directionAndAmountResult = aDirectionAndAmount;
+ switch (aDirectionAndAmount) {
+ case nsIEditor::eNextWord:
+ result = frameSelection->CreateRangeExtendedToNextWordBoundary<nsRange>();
+ if (NS_WARN_IF(aEditorBase.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "nsFrameSelection::CreateRangeExtendedToNextWordBoundary() failed");
+ // DeleteSelectionWithTransaction() doesn't handle these actions
+ // because it's inside batching, so don't confuse it:
+ directionAndAmountResult = nsIEditor::eNone;
+ break;
+ case nsIEditor::ePreviousWord:
+ result =
+ frameSelection->CreateRangeExtendedToPreviousWordBoundary<nsRange>();
+ if (NS_WARN_IF(aEditorBase.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "nsFrameSelection::CreateRangeExtendedToPreviousWordBoundary() "
+ "failed");
+ // DeleteSelectionWithTransaction() doesn't handle these actions
+ // because it's inside batching, so don't confuse it:
+ directionAndAmountResult = nsIEditor::eNone;
+ break;
+ case nsIEditor::eNext:
+ result =
+ frameSelection
+ ->CreateRangeExtendedToNextGraphemeClusterBoundary<nsRange>();
+ if (NS_WARN_IF(aEditorBase.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(result.isOk(),
+ "nsFrameSelection::"
+ "CreateRangeExtendedToNextGraphemeClusterBoundary() "
+ "failed");
+ // Don't set directionAndAmount to eNone (see Bug 502259)
+ break;
+ case nsIEditor::ePrevious: {
+ // Only extend the selection where the selection is after a UTF-16
+ // surrogate pair or a variation selector.
+ // For other cases we don't want to do that, in order
+ // to make sure that pressing backspace will only delete the last
+ // typed character.
+ // XXX This is odd if the previous one is a sequence for a grapheme
+ // cluster.
+ const auto atStartOfSelection = GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (MOZ_UNLIKELY(NS_WARN_IF(!atStartOfSelection.IsSet()))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // node might be anonymous DIV, so we find better text node
+ const EditorDOMPoint insertionPoint =
+ aEditorBase.IsTextEditor()
+ ? aEditorBase.AsTextEditor()->FindBetterInsertionPoint(
+ atStartOfSelection)
+ : atStartOfSelection.GetPointInTextNodeIfPointingAroundTextNode<
+ EditorDOMPoint>();
+ if (MOZ_UNLIKELY(!insertionPoint.IsSet())) {
+ NS_WARNING(
+ "EditorBase::FindBetterInsertionPoint() failed, but ignored");
+ return aDirectionAndAmount;
+ }
+
+ if (!insertionPoint.IsInTextNode()) {
+ return aDirectionAndAmount;
+ }
+
+ const nsTextFragment* data =
+ &insertionPoint.ContainerAs<Text>()->TextFragment();
+ uint32_t offset = insertionPoint.Offset();
+ if (!(offset > 1 &&
+ data->IsLowSurrogateFollowingHighSurrogateAt(offset - 1)) &&
+ !(offset > 0 &&
+ gfxFontUtils::IsVarSelector(data->CharAt(offset - 1)))) {
+ return aDirectionAndAmount;
+ }
+ // Different from the `eNext` case, we look for character boundary.
+ // I'm not sure whether this inconsistency between "Delete" and
+ // "Backspace" is intentional or not.
+ result = frameSelection
+ ->CreateRangeExtendedToPreviousCharacterBoundary<nsRange>();
+ if (NS_WARN_IF(aEditorBase.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "nsFrameSelection::"
+ "CreateRangeExtendedToPreviousGraphemeClusterBoundary() failed");
+ break;
+ }
+ case nsIEditor::eToBeginningOfLine:
+ result =
+ frameSelection->CreateRangeExtendedToPreviousHardLineBreak<nsRange>();
+ if (NS_WARN_IF(aEditorBase.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "nsFrameSelection::CreateRangeExtendedToPreviousHardLineBreak() "
+ "failed");
+ directionAndAmountResult = nsIEditor::eNone;
+ break;
+ case nsIEditor::eToEndOfLine:
+ result =
+ frameSelection->CreateRangeExtendedToNextHardLineBreak<nsRange>();
+ if (NS_WARN_IF(aEditorBase.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "nsFrameSelection::CreateRangeExtendedToNextHardLineBreak() failed");
+ directionAndAmountResult = nsIEditor::eNext;
+ break;
+ default:
+ return aDirectionAndAmount;
+ }
+
+ if (result.isErr()) {
+ return Err(result.inspectErr());
+ }
+ RefPtr<nsRange> extendedRange(result.unwrap().forget());
+ if (!extendedRange || NS_WARN_IF(!extendedRange->IsPositioned())) {
+ NS_WARNING("Failed to extend the range, but ignored");
+ return directionAndAmountResult;
+ }
+
+ // If the new range isn't editable, keep using the original range.
+ if (aEditorBase.IsHTMLEditor() &&
+ !AutoRangeArray::IsEditableRange(*extendedRange, *editingHost)) {
+ return aDirectionAndAmount;
+ }
+
+ if (NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
+ extendedRange->GetStartContainer())) ||
+ NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
+ extendedRange->GetEndContainer()))) {
+ NS_WARNING("A range was extended to outer of selection limiter");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Swap focus/anchor range with the extended range.
+ DebugOnly<bool> found = false;
+ for (OwningNonNull<nsRange>& range : mRanges) {
+ if (range == mAnchorFocusRange) {
+ range = *extendedRange;
+ found = true;
+ break;
+ }
+ }
+ MOZ_ASSERT(found);
+ mAnchorFocusRange.swap(extendedRange);
+ return directionAndAmountResult;
+}
+
+Result<bool, nsresult>
+AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent,
+ const Element* aEditingHost) {
+ if (IsCollapsed()) {
+ return false;
+ }
+
+ switch (aDirectionAndAmount) {
+ case nsIEditor::eNext:
+ case nsIEditor::eNextWord:
+ case nsIEditor::ePrevious:
+ case nsIEditor::ePreviousWord:
+ break;
+ default:
+ return false;
+ }
+
+ bool changed = false;
+ for (const OwningNonNull<nsRange>& range : mRanges) {
+ MOZ_ASSERT(!range->IsInAnySelection(),
+ "Changing range in selection may cause running script");
+ Result<bool, nsresult> result =
+ WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
+ aHTMLEditor, range, aEditingHost);
+ if (result.isErr()) {
+ NS_WARNING(
+ "WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() "
+ "failed");
+ return Err(result.inspectErr());
+ }
+ changed |= result.inspect();
+ }
+
+ if (mRanges.Length() == 1 && aIfSelectingOnlyOneAtomicContent ==
+ IfSelectingOnlyOneAtomicContent::Collapse) {
+ MOZ_ASSERT(mRanges[0].get() == mAnchorFocusRange.get());
+ if (mAnchorFocusRange->GetStartContainer() ==
+ mAnchorFocusRange->GetEndContainer() &&
+ mAnchorFocusRange->GetChildAtStartOffset() &&
+ mAnchorFocusRange->StartRef().GetNextSiblingOfChildAtOffset() ==
+ mAnchorFocusRange->GetChildAtEndOffset()) {
+ mAnchorFocusRange->Collapse(aDirectionAndAmount == nsIEditor::eNext ||
+ aDirectionAndAmount == nsIEditor::eNextWord);
+ changed = true;
+ }
+ }
+
+ return changed;
+}
+
+bool AutoRangeArray::SaveAndTrackRanges(HTMLEditor& aHTMLEditor) {
+ if (mSavedRanges.isSome()) {
+ return false;
+ }
+ mSavedRanges.emplace(*this);
+ aHTMLEditor.RangeUpdaterRef().RegisterSelectionState(mSavedRanges.ref());
+ mTrackingHTMLEditor = &aHTMLEditor;
+ return true;
+}
+
+void AutoRangeArray::ClearSavedRanges() {
+ if (mSavedRanges.isNothing()) {
+ return;
+ }
+ OwningNonNull<HTMLEditor> htmlEditor(std::move(mTrackingHTMLEditor));
+ MOZ_ASSERT(!mTrackingHTMLEditor);
+ htmlEditor->RangeUpdaterRef().DropSelectionState(mSavedRanges.ref());
+ mSavedRanges.reset();
+}
+
+// static
+void AutoRangeArray::
+ UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
+ EditorDOMPoint& aStartPoint, EditorDOMPoint& aEndPoint,
+ const Element& aEditingHost) {
+ // FYI: This was moved from
+ // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6743
+
+ // MOOSE major hack:
+ // The GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() and
+ // GetPointAfterFollowingLineBreakOrAtFollowingBlock() don't really do the
+ // right thing for collapsed ranges inside block elements that contain nothing
+ // but a solo <br>. It's easier/ to put a workaround here than to revamp
+ // them. :-(
+ if (aStartPoint != aEndPoint) {
+ return;
+ }
+
+ if (!aStartPoint.IsInContentNode()) {
+ return;
+ }
+
+ // XXX Perhaps, this should be more careful. This may not select only one
+ // node because this just check whether the block is empty or not,
+ // and may not select in non-editable block. However, for inline
+ // editing host case, it's right to look for block element without
+ // editable state check. Now, this method is used for preparation for
+ // other things. So, cannot write test for this method behavior.
+ // So, perhaps, we should get rid of this method and each caller should
+ // handle its job better.
+ Element* const maybeNonEditableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *aStartPoint.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (!maybeNonEditableBlockElement) {
+ return;
+ }
+
+ // Make sure we don't go higher than our root element in the content tree
+ if (aEditingHost.IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
+ return;
+ }
+
+ if (HTMLEditUtils::IsEmptyNode(
+ *maybeNonEditableBlockElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ aStartPoint.Set(maybeNonEditableBlockElement, 0u);
+ aEndPoint.SetToEndOf(maybeNonEditableBlockElement);
+ }
+}
+
+/**
+ * Get the point before the line containing aPointInLine.
+ *
+ * @return If the line starts after a `<br>` element, returns next
+ * sibling of the `<br>` element.
+ * If the line is first line of a block, returns point of
+ * the block.
+ * NOTE: The result may be point of editing host. I.e., the container may be
+ * outside of editing host.
+ */
+static EditorDOMPoint
+GetPointAtFirstContentOfLineOrParentHTMLBlockIfFirstContentOfBlock(
+ const EditorDOMPoint& aPointInLine, EditSubAction aEditSubAction,
+ BlockInlineCheck aBlockInlineCheck, const Element& aEditingHost) {
+ // FYI: This was moved from
+ // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6447
+
+ if (NS_WARN_IF(!aPointInLine.IsSet())) {
+ return EditorDOMPoint();
+ }
+
+ EditorDOMPoint point(aPointInLine);
+ // Start scanning from the container node if aPoint is in a text node.
+ // XXX Perhaps, IsInDataNode() must be expected.
+ if (point.IsInTextNode()) {
+ if (!point.GetContainer()->GetParentNode()) {
+ // Okay, can't promote any further
+ // XXX Why don't we return start of the text node?
+ return point;
+ }
+ // If there is a preformatted linefeed in the text node, let's return
+ // the point after it.
+ EditorDOMPoint atLastPreformattedNewLine =
+ HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
+ point);
+ if (atLastPreformattedNewLine.IsSet()) {
+ return atLastPreformattedNewLine.NextPoint();
+ }
+ point.Set(point.GetContainer());
+ }
+
+ // Look back through any further inline nodes that aren't across a <br>
+ // from us, and that are enclosed in the same block.
+ // I.e., looking for start of current hard line.
+ constexpr HTMLEditUtils::WalkTreeOptions
+ ignoreNonEditableNodeAndStopAtBlockBoundary{
+ HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
+ HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
+ for (nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary,
+ aBlockInlineCheck, &aEditingHost);
+ previousEditableContent && previousEditableContent->GetParentNode() &&
+ !HTMLEditUtils::IsVisibleBRElement(*previousEditableContent) &&
+ !HTMLEditUtils::IsBlockElement(*previousEditableContent,
+ aBlockInlineCheck);
+ previousEditableContent = HTMLEditUtils::GetPreviousContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary,
+ aBlockInlineCheck, &aEditingHost)) {
+ EditorDOMPoint atLastPreformattedNewLine =
+ HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
+ EditorRawDOMPoint::AtEndOf(*previousEditableContent));
+ if (atLastPreformattedNewLine.IsSet()) {
+ return atLastPreformattedNewLine.NextPoint();
+ }
+ point.Set(previousEditableContent);
+ }
+
+ // Finding the real start for this point unless current line starts after
+ // <br> element. Look up the tree for as long as we are the first node in
+ // the container (typically, start of nearest block ancestor), and as long
+ // as we haven't hit the body node.
+ for (nsIContent* nearContent = HTMLEditUtils::GetPreviousContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary,
+ aBlockInlineCheck, &aEditingHost);
+ !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
+ point.GetContainerParent();
+ nearContent = HTMLEditUtils::GetPreviousContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary,
+ aBlockInlineCheck, &aEditingHost)) {
+ // Don't keep looking up if we have found a blockquote element to act on
+ // when we handle outdent.
+ // XXX Sounds like this is hacky. If possible, it should be check in
+ // outdent handler for consistency between edit sub-actions.
+ // We should check Chromium's behavior of outdent when Selection
+ // starts from `<blockquote>` and starts from first child of
+ // `<blockquote>`.
+ if (aEditSubAction == EditSubAction::eOutdent &&
+ point.IsContainerHTMLElement(nsGkAtoms::blockquote)) {
+ break;
+ }
+
+ // Don't walk past the editable section. Note that we need to check
+ // before walking up to a parent because we need to return the parent
+ // object, so the parent itself might not be in the editable area, but
+ // it's OK if we're not performing a block-level action.
+ bool blockLevelAction =
+ aEditSubAction == EditSubAction::eIndent ||
+ aEditSubAction == EditSubAction::eOutdent ||
+ aEditSubAction == EditSubAction::eSetOrClearAlignment ||
+ aEditSubAction == EditSubAction::eCreateOrRemoveBlock ||
+ aEditSubAction == EditSubAction::eFormatBlockForHTMLCommand;
+ // XXX So, does this check whether the container is removable or not? It
+ // seems that here can be rewritten as obviously what here tries to
+ // check.
+ if (!point.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost) &&
+ (blockLevelAction ||
+ !point.GetContainer()->IsInclusiveDescendantOf(&aEditingHost))) {
+ break;
+ }
+
+ // If we're formatting a block, we should reformat first ancestor format
+ // block.
+ if (aEditSubAction == EditSubAction::eFormatBlockForHTMLCommand &&
+ HTMLEditUtils::IsFormatElementForFormatBlockCommand(
+ *point.ContainerAs<Element>())) {
+ point.Set(point.GetContainer());
+ break;
+ }
+
+ point.Set(point.GetContainer());
+ }
+ return point;
+}
+
+/**
+ * Get the point after the following line break or the block which breaks the
+ * line containing aPointInLine.
+ *
+ * @return If the line ends with a visible `<br>` element, returns
+ * the point after the `<br>` element.
+ * If the line ends with a preformatted linefeed, returns
+ * the point after the linefeed unless it's an invisible
+ * line break immediately before a block boundary.
+ * If the line ends with a block boundary, returns the
+ * point of the block.
+ */
+static EditorDOMPoint GetPointAfterFollowingLineBreakOrAtFollowingHTMLBlock(
+ const EditorDOMPoint& aPointInLine, EditSubAction aEditSubAction,
+ BlockInlineCheck aBlockInlineCheck, const Element& aEditingHost) {
+ // FYI: This was moved from
+ // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6541
+
+ if (NS_WARN_IF(!aPointInLine.IsSet())) {
+ return EditorDOMPoint();
+ }
+
+ EditorDOMPoint point(aPointInLine);
+ // Start scanning from the container node if aPoint is in a text node.
+ // XXX Perhaps, IsInDataNode() must be expected.
+ if (point.IsInTextNode()) {
+ if (NS_WARN_IF(!point.GetContainer()->GetParentNode())) {
+ // Okay, can't promote any further
+ // XXX Why don't we return end of the text node?
+ return point;
+ }
+ EditorDOMPoint atNextPreformattedNewLine =
+ HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
+ EditorDOMPoint>(point);
+ if (atNextPreformattedNewLine.IsSet()) {
+ // If the linefeed is last character of the text node, it may be
+ // invisible if it's immediately before a block boundary. In such
+ // case, we should return the block boundary.
+ Element* maybeNonEditableBlockElement = nullptr;
+ if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
+ atNextPreformattedNewLine, &maybeNonEditableBlockElement) &&
+ maybeNonEditableBlockElement) {
+ // If the block is a parent of the editing host, let's return end
+ // of editing host.
+ if (maybeNonEditableBlockElement == &aEditingHost ||
+ !maybeNonEditableBlockElement->IsInclusiveDescendantOf(
+ &aEditingHost)) {
+ return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
+ }
+ // If it's invisible because of parent block boundary, return end
+ // of the block. Otherwise, i.e., it's followed by a child block,
+ // returns the point of the child block.
+ if (atNextPreformattedNewLine.ContainerAs<Text>()
+ ->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
+ return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
+ }
+ return EditorDOMPoint(maybeNonEditableBlockElement);
+ }
+ // Otherwise, return the point after the preformatted linefeed.
+ return atNextPreformattedNewLine.NextPoint();
+ }
+ // want to be after the text node
+ point.SetAfter(point.GetContainer());
+ NS_WARNING_ASSERTION(point.IsSet(), "Failed to set to after the text node");
+ }
+
+ // Look ahead through any further inline nodes that aren't across a <br> from
+ // us, and that are enclosed in the same block.
+ // XXX Currently, we stop block-extending when finding visible <br> element.
+ // This might be different from "block-extend" of execCommand spec.
+ // However, the spec is really unclear.
+ // XXX Probably, scanning only editable nodes is wrong for
+ // EditSubAction::eCreateOrRemoveBlock and
+ // EditSubAction::eFormatBlockForHTMLCommand because it might be better to
+ // wrap existing inline elements even if it's non-editable. For example,
+ // following examples with insertParagraph causes different result:
+ // * <div contenteditable>foo[]<b contenteditable="false">bar</b></div>
+ // * <div contenteditable>foo[]<b>bar</b></div>
+ // * <div contenteditable>foo[]<b contenteditable="false">bar</b>baz</div>
+ // Only in the first case, after the caret position isn't wrapped with
+ // new <div> element.
+ constexpr HTMLEditUtils::WalkTreeOptions
+ ignoreNonEditableNodeAndStopAtBlockBoundary{
+ HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
+ HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
+ for (nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary,
+ aBlockInlineCheck, &aEditingHost);
+ nextEditableContent &&
+ !HTMLEditUtils::IsBlockElement(*nextEditableContent,
+ aBlockInlineCheck) &&
+ nextEditableContent->GetParent();
+ nextEditableContent = HTMLEditUtils::GetNextContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary,
+ aBlockInlineCheck, &aEditingHost)) {
+ EditorDOMPoint atFirstPreformattedNewLine =
+ HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
+ EditorDOMPoint>(EditorRawDOMPoint(nextEditableContent, 0));
+ if (atFirstPreformattedNewLine.IsSet()) {
+ // If the linefeed is last character of the text node, it may be
+ // invisible if it's immediately before a block boundary. In such
+ // case, we should return the block boundary.
+ Element* maybeNonEditableBlockElement = nullptr;
+ if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
+ atFirstPreformattedNewLine, &maybeNonEditableBlockElement) &&
+ maybeNonEditableBlockElement) {
+ // If the block is a parent of the editing host, let's return end
+ // of editing host.
+ if (maybeNonEditableBlockElement == &aEditingHost ||
+ !maybeNonEditableBlockElement->IsInclusiveDescendantOf(
+ &aEditingHost)) {
+ return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
+ }
+ // If it's invisible because of parent block boundary, return end
+ // of the block. Otherwise, i.e., it's followed by a child block,
+ // returns the point of the child block.
+ if (atFirstPreformattedNewLine.ContainerAs<Text>()
+ ->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
+ return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
+ }
+ return EditorDOMPoint(maybeNonEditableBlockElement);
+ }
+ // Otherwise, return the point after the preformatted linefeed.
+ return atFirstPreformattedNewLine.NextPoint();
+ }
+ point.SetAfter(nextEditableContent);
+ if (NS_WARN_IF(!point.IsSet())) {
+ break;
+ }
+ if (HTMLEditUtils::IsVisibleBRElement(*nextEditableContent)) {
+ break;
+ }
+ }
+
+ // Finding the real end for this point unless current line ends with a <br>
+ // element. Look up the tree for as long as we are the last node in the
+ // container (typically, block node), and as long as we haven't hit the body
+ // node.
+ for (nsIContent* nearContent = HTMLEditUtils::GetNextContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary,
+ aBlockInlineCheck, &aEditingHost);
+ !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
+ point.GetContainerParent();
+ nearContent = HTMLEditUtils::GetNextContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary,
+ aBlockInlineCheck, &aEditingHost)) {
+ // Don't walk past the editable section. Note that we need to check before
+ // walking up to a parent because we need to return the parent object, so
+ // the parent itself might not be in the editable area, but it's OK.
+ // XXX Maybe returning parent of editing host is really error prone since
+ // everybody need to check whether the end point is in editing host
+ // when they touch there.
+ if (!point.GetContainer()->IsInclusiveDescendantOf(&aEditingHost) &&
+ !point.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost)) {
+ break;
+ }
+
+ // If we're formatting a block, we should reformat first ancestor format
+ // block.
+ if (aEditSubAction == EditSubAction::eFormatBlockForHTMLCommand &&
+ HTMLEditUtils::IsFormatElementForFormatBlockCommand(
+ *point.ContainerAs<Element>())) {
+ point.SetAfter(point.GetContainer());
+ break;
+ }
+
+ point.SetAfter(point.GetContainer());
+ if (NS_WARN_IF(!point.IsSet())) {
+ break;
+ }
+ }
+ return point;
+}
+
+void AutoRangeArray::ExtendRangesToWrapLines(EditSubAction aEditSubAction,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element& aEditingHost) {
+ // FYI: This is originated in
+ // https://searchfox.org/mozilla-central/rev/1739f1301d658c9bff544a0a095ab11fca2e549d/editor/libeditor/HTMLEditSubActionHandler.cpp#6712
+
+ bool removeSomeRanges = false;
+ for (const OwningNonNull<nsRange>& range : mRanges) {
+ // Remove non-positioned ranges.
+ if (MOZ_UNLIKELY(!range->IsPositioned())) {
+ removeSomeRanges = true;
+ continue;
+ }
+ // If the range is native anonymous subtrees, we must meet a bug of
+ // `Selection` so that we need to hack here.
+ if (MOZ_UNLIKELY(range->GetStartContainer()->IsInNativeAnonymousSubtree() ||
+ range->GetEndContainer()->IsInNativeAnonymousSubtree())) {
+ EditorRawDOMRange rawRange(range);
+ if (!rawRange.EnsureNotInNativeAnonymousSubtree()) {
+ range->Reset();
+ removeSomeRanges = true;
+ continue;
+ }
+ if (NS_FAILED(
+ range->SetStartAndEnd(rawRange.StartRef().ToRawRangeBoundary(),
+ rawRange.EndRef().ToRawRangeBoundary())) ||
+ MOZ_UNLIKELY(!range->IsPositioned())) {
+ range->Reset();
+ removeSomeRanges = true;
+ continue;
+ }
+ }
+ // Finally, extend the range.
+ if (NS_FAILED(ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
+ range, aEditSubAction, aBlockInlineCheck, aEditingHost))) {
+ // If we failed to extend the range, we should use the original range
+ // as-is unless the range is broken at setting the range.
+ if (NS_WARN_IF(!range->IsPositioned())) {
+ removeSomeRanges = true;
+ }
+ }
+ }
+ if (removeSomeRanges) {
+ for (const size_t i : Reversed(IntegerRange(mRanges.Length()))) {
+ if (!mRanges[i]->IsPositioned()) {
+ mRanges.RemoveElementAt(i);
+ }
+ }
+ if (!mAnchorFocusRange || !mAnchorFocusRange->IsPositioned()) {
+ if (mRanges.IsEmpty()) {
+ mAnchorFocusRange = nullptr;
+ } else {
+ mAnchorFocusRange = mRanges.LastElement();
+ }
+ }
+ }
+}
+
+// static
+nsresult AutoRangeArray::ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
+ nsRange& aRange, EditSubAction aEditSubAction,
+ BlockInlineCheck aBlockInlineCheck, const Element& aEditingHost) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !EditorRawDOMPoint(aRange.StartRef()).IsInNativeAnonymousSubtree());
+ MOZ_DIAGNOSTIC_ASSERT(
+ !EditorRawDOMPoint(aRange.EndRef()).IsInNativeAnonymousSubtree());
+
+ if (NS_WARN_IF(!aRange.IsPositioned())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ EditorDOMPoint startPoint(aRange.StartRef()), endPoint(aRange.EndRef());
+
+ // If we're joining blocks, we call this for selecting a line to move.
+ // Therefore, we don't want to select the ancestor blocks in this case
+ // even if they are empty.
+ if (aEditSubAction != EditSubAction::eMergeBlockContents) {
+ AutoRangeArray::
+ UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
+ startPoint, endPoint, aEditingHost);
+ }
+
+ // Make a new adjusted range to represent the appropriate block content.
+ // This is tricky. The basic idea is to push out the range endpoints to
+ // truly enclose the blocks that we will affect.
+
+ // Make sure that the new range ends up to be in the editable section.
+ // XXX Looks like that this check wastes the time. Perhaps, we should
+ // implement a method which checks both two DOM points in the editor
+ // root.
+
+ startPoint =
+ GetPointAtFirstContentOfLineOrParentHTMLBlockIfFirstContentOfBlock(
+ startPoint, aEditSubAction, aBlockInlineCheck, aEditingHost);
+ // XXX GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() may
+ // return point of editing host. Perhaps, we should change it and stop
+ // checking it here since this check may be expensive.
+ // XXX If the container is an element in the editing host but it points end of
+ // the container, this returns nullptr. Is it intentional?
+ if (!startPoint.GetChildOrContainerIfDataNode() ||
+ !startPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
+ &aEditingHost)) {
+ return NS_ERROR_FAILURE;
+ }
+ endPoint = GetPointAfterFollowingLineBreakOrAtFollowingHTMLBlock(
+ endPoint, aEditSubAction, aBlockInlineCheck, aEditingHost);
+ const EditorDOMPoint lastRawPoint =
+ endPoint.IsStartOfContainer() ? endPoint : endPoint.PreviousPoint();
+ // XXX GetPointAfterFollowingLineBreakOrAtFollowingBlock() may return point of
+ // editing host. Perhaps, we should change it and stop checking it here
+ // since this check may be expensive.
+ // XXX If the container is an element in the editing host but it points end of
+ // the container, this returns nullptr. Is it intentional?
+ if (!lastRawPoint.GetChildOrContainerIfDataNode() ||
+ !lastRawPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
+ &aEditingHost)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = aRange.SetStartAndEnd(startPoint.ToRawRangeBoundary(),
+ endPoint.ToRawRangeBoundary());
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+Result<EditorDOMPoint, nsresult>
+AutoRangeArray::SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ HTMLEditor& aHTMLEditor, BlockInlineCheck aBlockInlineCheck,
+ const Element& aEditingHost,
+ const nsIContent* aAncestorLimiter /* = nullptr */) {
+ // FYI: The following code is originated in
+ // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#6971
+
+ // Split text nodes. This is necessary, since given ranges may end in text
+ // nodes in case where part of a pre-formatted elements needs to be moved.
+ EditorDOMPoint pointToPutCaret;
+ IgnoredErrorResult ignoredError;
+ for (const OwningNonNull<nsRange>& range : mRanges) {
+ EditorDOMPoint atEnd(range->EndRef());
+ if (NS_WARN_IF(!atEnd.IsSet()) || !atEnd.IsInTextNode() ||
+ atEnd.GetContainer() == aAncestorLimiter) {
+ continue;
+ }
+
+ if (!atEnd.IsStartOfContainer() && !atEnd.IsEndOfContainer()) {
+ // Split the text node.
+ Result<SplitNodeResult, nsresult> splitAtEndResult =
+ aHTMLEditor.SplitNodeWithTransaction(atEnd);
+ if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitAtEndResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap();
+ unwrappedSplitAtEndResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+
+ // Correct the range.
+ // The new end parent becomes the parent node of the text.
+ MOZ_ASSERT(!range->IsInAnySelection());
+ range->SetEnd(unwrappedSplitAtEndResult.AtNextContent<EditorRawDOMPoint>()
+ .ToRawRangeBoundary(),
+ ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "nsRange::SetEnd() failed, but ignored");
+ ignoredError.SuppressException();
+ }
+ }
+
+ // FYI: The following code is originated in
+ // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#7023
+ AutoTArray<OwningNonNull<RangeItem>, 8> rangeItemArray;
+ rangeItemArray.AppendElements(mRanges.Length());
+
+ // First register ranges for special editor gravity
+ Maybe<size_t> anchorFocusRangeIndex;
+ for (const size_t index : IntegerRange(rangeItemArray.Length())) {
+ rangeItemArray[index] = new RangeItem();
+ rangeItemArray[index]->StoreRange(*mRanges[index]);
+ aHTMLEditor.RangeUpdaterRef().RegisterRangeItem(*rangeItemArray[index]);
+ if (mRanges[index] == mAnchorFocusRange) {
+ anchorFocusRangeIndex = Some(index);
+ }
+ }
+ // TODO: We should keep the array, and just update the ranges.
+ mRanges.Clear();
+ mAnchorFocusRange = nullptr;
+ // Now bust up inlines.
+ nsresult rv = NS_OK;
+ for (const OwningNonNull<RangeItem>& item : Reversed(rangeItemArray)) {
+ // MOZ_KnownLive because 'rangeItemArray' is guaranteed to keep it alive.
+ Result<EditorDOMPoint, nsresult> splitParentsResult =
+ aHTMLEditor.SplitInlineAncestorsAtRangeBoundaries(
+ MOZ_KnownLive(*item), aBlockInlineCheck, aEditingHost,
+ aAncestorLimiter);
+ if (MOZ_UNLIKELY(splitParentsResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitInlineAncestorsAtRangeBoundaries() failed");
+ rv = splitParentsResult.unwrapErr();
+ break;
+ }
+ if (splitParentsResult.inspect().IsSet()) {
+ pointToPutCaret = splitParentsResult.unwrap();
+ }
+ }
+ // Then unregister the ranges
+ for (const size_t index : IntegerRange(rangeItemArray.Length())) {
+ aHTMLEditor.RangeUpdaterRef().DropRangeItem(rangeItemArray[index]);
+ RefPtr<nsRange> range = rangeItemArray[index]->GetRange();
+ if (range && range->IsPositioned()) {
+ if (anchorFocusRangeIndex.isSome() && index == *anchorFocusRangeIndex) {
+ mAnchorFocusRange = range;
+ }
+ mRanges.AppendElement(std::move(range));
+ }
+ }
+ if (!mAnchorFocusRange && !mRanges.IsEmpty()) {
+ mAnchorFocusRange = mRanges.LastElement();
+ }
+
+ // XXX Why do we ignore the other errors here??
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ return pointToPutCaret;
+}
+
+nsresult AutoRangeArray::CollectEditTargetNodes(
+ const HTMLEditor& aHTMLEditor,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ EditSubAction aEditSubAction,
+ CollectNonEditableNodes aCollectNonEditableNodes) const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+
+ // FYI: This was moved from
+ // https://searchfox.org/mozilla-central/rev/4bce7d85ba4796dd03c5dcc7cfe8eee0e4c07b3b/editor/libeditor/HTMLEditSubActionHandler.cpp#7060
+
+ // Gather up a list of all the nodes
+ for (const OwningNonNull<nsRange>& range : mRanges) {
+ DOMSubtreeIterator iter;
+ nsresult rv = iter.Init(*range);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("DOMSubtreeIterator::Init() failed");
+ return rv;
+ }
+ if (aOutArrayOfContents.IsEmpty()) {
+ iter.AppendAllNodesToArray(aOutArrayOfContents);
+ } else {
+ AutoTArray<OwningNonNull<nsIContent>, 24> arrayOfTopChildren;
+ iter.AppendNodesToArray(
+ +[](nsINode& aNode, void* aArray) -> bool {
+ MOZ_ASSERT(aArray);
+ return !static_cast<nsTArray<OwningNonNull<nsIContent>>*>(aArray)
+ ->Contains(&aNode);
+ },
+ arrayOfTopChildren, &aOutArrayOfContents);
+ aOutArrayOfContents.AppendElements(std::move(arrayOfTopChildren));
+ }
+ if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
+ for (const size_t i :
+ Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
+ if (!EditorUtils::IsEditableContent(aOutArrayOfContents[i],
+ EditorUtils::EditorType::HTML)) {
+ aOutArrayOfContents.RemoveElementAt(i);
+ }
+ }
+ }
+ }
+
+ switch (aEditSubAction) {
+ case EditSubAction::eCreateOrRemoveBlock:
+ case EditSubAction::eFormatBlockForHTMLCommand: {
+ // Certain operations should not act on li's and td's, but rather inside
+ // them. Alter the list as needed.
+ CollectChildrenOptions options = {
+ CollectChildrenOption::CollectListChildren,
+ CollectChildrenOption::CollectTableChildren};
+ if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
+ options += CollectChildrenOption::IgnoreNonEditableChildren;
+ }
+ if (aEditSubAction == EditSubAction::eCreateOrRemoveBlock) {
+ for (const size_t index :
+ Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
+ OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
+ if (HTMLEditUtils::IsListItem(content)) {
+ aOutArrayOfContents.RemoveElementAt(index);
+ HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
+ options);
+ }
+ }
+ } else {
+ // <dd> and <dt> are format blocks. Therefore, we should not handle
+ // their children directly. They should be replaced with new format
+ // block.
+ MOZ_ASSERT(
+ HTMLEditUtils::IsFormatTagForFormatBlockCommand(*nsGkAtoms::dt));
+ MOZ_ASSERT(
+ HTMLEditUtils::IsFormatTagForFormatBlockCommand(*nsGkAtoms::dd));
+ for (const size_t index :
+ Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
+ OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
+ MOZ_ASSERT_IF(HTMLEditUtils::IsListItem(content),
+ content->IsAnyOfHTMLElements(
+ nsGkAtoms::dd, nsGkAtoms::dt, nsGkAtoms::li));
+ if (content->IsHTMLElement(nsGkAtoms::li)) {
+ aOutArrayOfContents.RemoveElementAt(index);
+ HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
+ options);
+ }
+ }
+ }
+ // Empty text node shouldn't be selected if unnecessary
+ for (const size_t index :
+ Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
+ if (const Text* text = aOutArrayOfContents[index]->GetAsText()) {
+ // Don't select empty text except to empty block
+ if (!HTMLEditUtils::IsVisibleTextNode(*text)) {
+ aOutArrayOfContents.RemoveElementAt(index);
+ }
+ }
+ }
+ break;
+ }
+ case EditSubAction::eCreateOrChangeList: {
+ // XXX aCollectNonEditableNodes is ignored here. Maybe a bug.
+ CollectChildrenOptions options = {
+ CollectChildrenOption::CollectTableChildren};
+ for (const size_t index :
+ Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
+ // Scan for table elements. If we find table elements other than
+ // table, replace it with a list of any editable non-table content
+ // because if a selection range starts from end in a table-cell and
+ // ends at or starts from outside the `<table>`, we need to make
+ // lists in each selected table-cells.
+ OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
+ if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
+ aOutArrayOfContents.RemoveElementAt(index);
+ HTMLEditUtils::CollectChildren(content, aOutArrayOfContents, index,
+ options);
+ }
+ }
+ // If there is only one node in the array, and it is a `<div>`,
+ // `<blockquote>` or a list element, then look inside of it until we
+ // find inner list or content.
+ if (aOutArrayOfContents.Length() != 1) {
+ break;
+ }
+ Element* deepestDivBlockquoteOrListElement =
+ HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild(
+ aOutArrayOfContents[0],
+ {HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, nsGkAtoms::div, nsGkAtoms::blockquote,
+ nsGkAtoms::ul, nsGkAtoms::ol, nsGkAtoms::dl);
+ if (!deepestDivBlockquoteOrListElement) {
+ break;
+ }
+ if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements(
+ nsGkAtoms::div, nsGkAtoms::blockquote)) {
+ aOutArrayOfContents.Clear();
+ // XXX Before we're called, non-editable nodes are ignored. However,
+ // we may append non-editable nodes here.
+ HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement,
+ aOutArrayOfContents, 0, {});
+ break;
+ }
+ aOutArrayOfContents.ReplaceElementAt(
+ 0, OwningNonNull<nsIContent>(*deepestDivBlockquoteOrListElement));
+ break;
+ }
+ case EditSubAction::eOutdent:
+ case EditSubAction::eIndent:
+ case EditSubAction::eSetPositionToAbsolute: {
+ // Indent/outdent already do something special for list items, but we
+ // still need to make sure we don't act on table elements
+ CollectChildrenOptions options = {
+ CollectChildrenOption::CollectListChildren,
+ CollectChildrenOption::CollectTableChildren};
+ if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
+ options += CollectChildrenOption::IgnoreNonEditableChildren;
+ }
+ for (const size_t index :
+ Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
+ OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
+ if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
+ aOutArrayOfContents.RemoveElementAt(index);
+ HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
+ options);
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ // Outdent should look inside of divs.
+ if (aEditSubAction == EditSubAction::eOutdent &&
+ !aHTMLEditor.IsCSSEnabled()) {
+ CollectChildrenOptions options = {};
+ if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
+ options += CollectChildrenOption::IgnoreNonEditableChildren;
+ }
+ for (const size_t index :
+ Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
+ OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
+ if (content->IsHTMLElement(nsGkAtoms::div)) {
+ aOutArrayOfContents.RemoveElementAt(index);
+ HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
+ options);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+Element* AutoRangeArray::GetClosestAncestorAnyListElementOfRange() const {
+ for (const OwningNonNull<nsRange>& range : mRanges) {
+ nsINode* commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
+ if (MOZ_UNLIKELY(!commonAncestorNode)) {
+ continue;
+ }
+ for (Element* const element :
+ commonAncestorNode->InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsAnyListElement(element)) {
+ return element;
+ }
+ }
+ }
+ return nullptr;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/AutoRangeArray.h b/editor/libeditor/AutoRangeArray.h
new file mode 100644
index 0000000000..e2dd2e0149
--- /dev/null
+++ b/editor/libeditor/AutoRangeArray.h
@@ -0,0 +1,476 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AutoRangeArray_h
+#define AutoRangeArray_h
+
+#include "EditAction.h" // for EditSubAction
+#include "EditorBase.h" // for EditorBase
+#include "EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc
+#include "EditorForwards.h"
+#include "HTMLEditHelpers.h" // for BlockInlineCheck
+#include "SelectionState.h" // for SelectionState
+
+#include "mozilla/ErrorResult.h" // for ErrorResult
+#include "mozilla/IntegerRange.h" // for IntegerRange
+#include "mozilla/Maybe.h" // for Maybe
+#include "mozilla/RangeBoundary.h" // for RangeBoundary
+#include "mozilla/Result.h" // for Result<>
+#include "mozilla/dom/Element.h" // for dom::Element
+#include "mozilla/dom/HTMLBRElement.h" // for dom::HTMLBRElement
+#include "mozilla/dom/Selection.h" // for dom::Selection
+#include "mozilla/dom/Text.h" // for dom::Text
+
+#include "nsDebug.h" // for NS_WARNING, etc
+#include "nsDirection.h" // for nsDirection
+#include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
+#include "nsRange.h" // for nsRange
+
+namespace mozilla {
+
+/******************************************************************************
+ * AutoRangeArray stores ranges which do no belong any `Selection`.
+ * So, different from `AutoSelectionRangeArray`, this can be used for
+ * ranges which may need to be modified before touching the DOM tree,
+ * but does not want to modify `Selection` for the performance.
+ *****************************************************************************/
+class MOZ_STACK_CLASS AutoRangeArray final {
+ public:
+ explicit AutoRangeArray(const dom::Selection& aSelection);
+ template <typename PointType>
+ explicit AutoRangeArray(const EditorDOMRangeBase<PointType>& aRange);
+ template <typename PT, typename CT>
+ explicit AutoRangeArray(const EditorDOMPointBase<PT, CT>& aPoint);
+ // The copy constructor copies everything except saved ranges.
+ explicit AutoRangeArray(const AutoRangeArray& aOther);
+
+ ~AutoRangeArray();
+
+ void Initialize(const dom::Selection& aSelection) {
+ ClearSavedRanges();
+ mDirection = aSelection.GetDirection();
+ mRanges.Clear();
+ for (const uint32_t i : IntegerRange(aSelection.RangeCount())) {
+ MOZ_ASSERT(aSelection.GetRangeAt(i));
+ mRanges.AppendElement(aSelection.GetRangeAt(i)->CloneRange());
+ if (aSelection.GetRangeAt(i) == aSelection.GetAnchorFocusRange()) {
+ mAnchorFocusRange = mRanges.LastElement();
+ }
+ }
+ }
+
+ /**
+ * Check whether all ranges in content nodes or not. If the ranges is empty,
+ * this returns false.
+ */
+ [[nodiscard]] bool IsInContent() const {
+ if (mRanges.IsEmpty()) {
+ return false;
+ }
+ for (const OwningNonNull<nsRange>& range : mRanges) {
+ if (MOZ_UNLIKELY(!range->IsPositioned() || !range->GetStartContainer() ||
+ !range->GetStartContainer()->IsContent() ||
+ !range->GetEndContainer() ||
+ !range->GetEndContainer()->IsContent())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * EnsureOnlyEditableRanges() removes ranges which cannot modify.
+ * Note that this is designed only for `HTMLEditor` because this must not
+ * be required by `TextEditor`.
+ */
+ void EnsureOnlyEditableRanges(const dom::Element& aEditingHost);
+
+ /**
+ * EnsureRangesInTextNode() is designed for TextEditor to guarantee that
+ * all ranges are in its text node which is first child of the anonymous <div>
+ * element and is first child.
+ */
+ void EnsureRangesInTextNode(const dom::Text& aTextNode);
+
+ /**
+ * Extend ranges to make each range select starting from a line start edge and
+ * ending after a line end edge to handle per line edit sub-actions.
+ */
+ void ExtendRangesToWrapLines(EditSubAction aEditSubAction,
+ BlockInlineCheck aBlockInlineCheck,
+ const dom::Element& aEditingHost);
+
+ /**
+ * Check whether the range is in aEditingHost and both containers of start and
+ * end boundaries of the range are editable.
+ */
+ [[nodiscard]] static bool IsEditableRange(const dom::AbstractRange& aRange,
+ const dom::Element& aEditingHost);
+
+ /**
+ * Check whether the first range is in aEditingHost and both containers of
+ * start and end boundaries of the first range are editable.
+ */
+ [[nodiscard]] bool IsFirstRangeEditable(
+ const dom::Element& aEditingHost) const {
+ return IsEditableRange(FirstRangeRef(), aEditingHost);
+ }
+
+ /**
+ * IsAtLeastOneContainerOfRangeBoundariesInclusiveDescendantOf() returns true
+ * if at least one of the containers of the range boundaries is an inclusive
+ * descendant of aContent.
+ */
+ [[nodiscard]] bool
+ IsAtLeastOneContainerOfRangeBoundariesInclusiveDescendantOf(
+ const nsIContent& aContent) const {
+ for (const OwningNonNull<nsRange>& range : mRanges) {
+ nsINode* startContainer = range->GetStartContainer();
+ if (startContainer &&
+ startContainer->IsInclusiveDescendantOf(&aContent)) {
+ return true;
+ }
+ nsINode* endContainer = range->GetEndContainer();
+ if (startContainer == endContainer) {
+ continue;
+ }
+ if (endContainer && endContainer->IsInclusiveDescendantOf(&aContent)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ [[nodiscard]] auto& Ranges() { return mRanges; }
+ [[nodiscard]] const auto& Ranges() const { return mRanges; }
+ [[nodiscard]] OwningNonNull<nsRange>& FirstRangeRef() { return mRanges[0]; }
+ [[nodiscard]] const OwningNonNull<nsRange>& FirstRangeRef() const {
+ return mRanges[0];
+ }
+
+ template <template <typename> typename StrongPtrType>
+ [[nodiscard]] AutoTArray<StrongPtrType<nsRange>, 8> CloneRanges() const {
+ AutoTArray<StrongPtrType<nsRange>, 8> ranges;
+ for (const auto& range : mRanges) {
+ ranges.AppendElement(range->CloneRange());
+ }
+ return ranges;
+ }
+
+ template <typename EditorDOMPointType>
+ [[nodiscard]] EditorDOMPointType GetFirstRangeStartPoint() const {
+ if (mRanges.IsEmpty() || !mRanges[0]->IsPositioned()) {
+ return EditorDOMPointType();
+ }
+ return EditorDOMPointType(mRanges[0]->StartRef());
+ }
+ template <typename EditorDOMPointType>
+ [[nodiscard]] EditorDOMPointType GetFirstRangeEndPoint() const {
+ if (mRanges.IsEmpty() || !mRanges[0]->IsPositioned()) {
+ return EditorDOMPointType();
+ }
+ return EditorDOMPointType(mRanges[0]->EndRef());
+ }
+
+ nsresult SelectNode(nsINode& aNode) {
+ mRanges.Clear();
+ if (!mAnchorFocusRange) {
+ mAnchorFocusRange = nsRange::Create(&aNode);
+ if (!mAnchorFocusRange) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ ErrorResult error;
+ mAnchorFocusRange->SelectNode(aNode, error);
+ if (error.Failed()) {
+ mAnchorFocusRange = nullptr;
+ return error.StealNSResult();
+ }
+ mRanges.AppendElement(*mAnchorFocusRange);
+ return NS_OK;
+ }
+
+ /**
+ * ExtendAnchorFocusRangeFor() extends the anchor-focus range for deleting
+ * content for aDirectionAndAmount. The range won't be extended to outer of
+ * selection limiter. Note that if a range is extened, the range is
+ * recreated. Therefore, caller cannot cache pointer of any ranges before
+ * calling this.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<nsIEditor::EDirection, nsresult>
+ ExtendAnchorFocusRangeFor(const EditorBase& aEditorBase,
+ nsIEditor::EDirection aDirectionAndAmount);
+
+ /**
+ * For compatiblity with the other browsers, we should shrink ranges to
+ * start from an atomic content and/or end after one instead of start
+ * from end of a preceding text node and end by start of a follwing text
+ * node. Returns true if this modifies a range.
+ */
+ enum class IfSelectingOnlyOneAtomicContent {
+ Collapse, // Collapse to the range selecting only one atomic content to
+ // start or after of it. Whether to collapse start or after
+ // it depends on aDirectionAndAmount. This is ignored if
+ // there are multiple ranges.
+ KeepSelecting, // Won't collapse the range.
+ };
+ Result<bool, nsresult> ShrinkRangesIfStartFromOrEndAfterAtomicContent(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent,
+ const dom::Element* aEditingHost);
+
+ /**
+ * The following methods are same as `Selection`'s methods.
+ */
+ [[nodiscard]] bool IsCollapsed() const {
+ return mRanges.IsEmpty() ||
+ (mRanges.Length() == 1 && mRanges[0]->Collapsed());
+ }
+ template <typename PT, typename CT>
+ nsresult Collapse(const EditorDOMPointBase<PT, CT>& aPoint) {
+ mRanges.Clear();
+ if (!mAnchorFocusRange) {
+ ErrorResult error;
+ mAnchorFocusRange = nsRange::Create(aPoint.ToRawRangeBoundary(),
+ aPoint.ToRawRangeBoundary(), error);
+ if (error.Failed()) {
+ mAnchorFocusRange = nullptr;
+ return error.StealNSResult();
+ }
+ } else {
+ nsresult rv = mAnchorFocusRange->CollapseTo(aPoint.ToRawRangeBoundary());
+ if (NS_FAILED(rv)) {
+ mAnchorFocusRange = nullptr;
+ return rv;
+ }
+ }
+ mRanges.AppendElement(*mAnchorFocusRange);
+ return NS_OK;
+ }
+ template <typename SPT, typename SCT, typename EPT, typename ECT>
+ nsresult SetStartAndEnd(const EditorDOMPointBase<SPT, SCT>& aStart,
+ const EditorDOMPointBase<EPT, ECT>& aEnd) {
+ mRanges.Clear();
+ if (!mAnchorFocusRange) {
+ ErrorResult error;
+ mAnchorFocusRange = nsRange::Create(aStart.ToRawRangeBoundary(),
+ aEnd.ToRawRangeBoundary(), error);
+ if (error.Failed()) {
+ mAnchorFocusRange = nullptr;
+ return error.StealNSResult();
+ }
+ } else {
+ nsresult rv = mAnchorFocusRange->SetStartAndEnd(
+ aStart.ToRawRangeBoundary(), aEnd.ToRawRangeBoundary());
+ if (NS_FAILED(rv)) {
+ mAnchorFocusRange = nullptr;
+ return rv;
+ }
+ }
+ mRanges.AppendElement(*mAnchorFocusRange);
+ return NS_OK;
+ }
+ template <typename SPT, typename SCT, typename EPT, typename ECT>
+ nsresult SetBaseAndExtent(const EditorDOMPointBase<SPT, SCT>& aAnchor,
+ const EditorDOMPointBase<EPT, ECT>& aFocus) {
+ if (MOZ_UNLIKELY(!aAnchor.IsSet()) || MOZ_UNLIKELY(!aFocus.IsSet())) {
+ mRanges.Clear();
+ mAnchorFocusRange = nullptr;
+ return NS_ERROR_INVALID_ARG;
+ }
+ return aAnchor.EqualsOrIsBefore(aFocus) ? SetStartAndEnd(aAnchor, aFocus)
+ : SetStartAndEnd(aFocus, aAnchor);
+ }
+ [[nodiscard]] const nsRange* GetAnchorFocusRange() const {
+ return mAnchorFocusRange;
+ }
+ [[nodiscard]] nsDirection GetDirection() const { return mDirection; }
+
+ void SetDirection(nsDirection aDirection) { mDirection = aDirection; }
+
+ [[nodiscard]] const RangeBoundary& AnchorRef() const {
+ if (!mAnchorFocusRange) {
+ static RangeBoundary sEmptyRangeBoundary;
+ return sEmptyRangeBoundary;
+ }
+ return mDirection == nsDirection::eDirNext ? mAnchorFocusRange->StartRef()
+ : mAnchorFocusRange->EndRef();
+ }
+ [[nodiscard]] nsINode* GetAnchorNode() const {
+ return AnchorRef().IsSet() ? AnchorRef().Container() : nullptr;
+ }
+ [[nodiscard]] uint32_t GetAnchorOffset() const {
+ return AnchorRef().IsSet()
+ ? AnchorRef()
+ .Offset(RangeBoundary::OffsetFilter::kValidOffsets)
+ .valueOr(0)
+ : 0;
+ }
+ [[nodiscard]] nsIContent* GetChildAtAnchorOffset() const {
+ return AnchorRef().IsSet() ? AnchorRef().GetChildAtOffset() : nullptr;
+ }
+
+ [[nodiscard]] const RangeBoundary& FocusRef() const {
+ if (!mAnchorFocusRange) {
+ static RangeBoundary sEmptyRangeBoundary;
+ return sEmptyRangeBoundary;
+ }
+ return mDirection == nsDirection::eDirNext ? mAnchorFocusRange->EndRef()
+ : mAnchorFocusRange->StartRef();
+ }
+ [[nodiscard]] nsINode* GetFocusNode() const {
+ return FocusRef().IsSet() ? FocusRef().Container() : nullptr;
+ }
+ [[nodiscard]] uint32_t FocusOffset() const {
+ return FocusRef().IsSet()
+ ? FocusRef()
+ .Offset(RangeBoundary::OffsetFilter::kValidOffsets)
+ .valueOr(0)
+ : 0;
+ }
+ [[nodiscard]] nsIContent* GetChildAtFocusOffset() const {
+ return FocusRef().IsSet() ? FocusRef().GetChildAtOffset() : nullptr;
+ }
+
+ void RemoveAllRanges() {
+ mRanges.Clear();
+ mAnchorFocusRange = nullptr;
+ mDirection = nsDirection::eDirNext;
+ }
+
+ /**
+ * APIs to store ranges with only container node and offset in it, and track
+ * them with RangeUpdater.
+ */
+ [[nodiscard]] bool SaveAndTrackRanges(HTMLEditor& aHTMLEditor);
+ [[nodiscard]] bool HasSavedRanges() const { return mSavedRanges.isSome(); }
+ void ClearSavedRanges();
+ void RestoreFromSavedRanges() {
+ MOZ_DIAGNOSTIC_ASSERT(mSavedRanges.isSome());
+ if (mSavedRanges.isNothing()) {
+ return;
+ }
+ mSavedRanges->ApplyTo(*this);
+ ClearSavedRanges();
+ }
+
+ /**
+ * Apply mRanges and mDirection to aSelection.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult ApplyTo(dom::Selection& aSelection) {
+ dom::SelectionBatcher selectionBatcher(aSelection, __FUNCTION__);
+ aSelection.RemoveAllRanges(IgnoreErrors());
+ MOZ_ASSERT(!aSelection.RangeCount());
+ aSelection.SetDirection(mDirection);
+ IgnoredErrorResult error;
+ for (const OwningNonNull<nsRange>& range : mRanges) {
+ // MOZ_KnownLive(range) due to bug 1622253
+ aSelection.AddRangeAndSelectFramesAndNotifyListeners(MOZ_KnownLive(range),
+ error);
+ if (error.Failed()) {
+ return error.StealNSResult();
+ }
+ }
+ return NS_OK;
+ }
+
+ /**
+ * If the points are same (i.e., mean a collapsed range) and in an empty block
+ * element except the padding <br> element, this makes aStartPoint and
+ * aEndPoint contain the padding <br> element.
+ */
+ static void UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
+ EditorDOMPoint& aStartPoint, EditorDOMPoint& aEndPoint,
+ const dom::Element& aEditingHost);
+
+ /**
+ * CreateRangeExtendedToHardLineStartAndEnd() creates an nsRange instance
+ * which may be expanded to start/end of hard line at both edges of the given
+ * range. If this fails handling something, returns nullptr.
+ */
+ static already_AddRefed<nsRange>
+ CreateRangeWrappingStartAndEndLinesContainingBoundaries(
+ const EditorDOMRange& aRange, EditSubAction aEditSubAction,
+ BlockInlineCheck aBlockInlineCheck, const dom::Element& aEditingHost) {
+ if (!aRange.IsPositioned()) {
+ return nullptr;
+ }
+ return CreateRangeWrappingStartAndEndLinesContainingBoundaries(
+ aRange.StartRef(), aRange.EndRef(), aEditSubAction, aBlockInlineCheck,
+ aEditingHost);
+ }
+ static already_AddRefed<nsRange>
+ CreateRangeWrappingStartAndEndLinesContainingBoundaries(
+ const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint,
+ EditSubAction aEditSubAction, BlockInlineCheck aBlockInlineCheck,
+ const dom::Element& aEditingHost) {
+ RefPtr<nsRange> range =
+ nsRange::Create(aStartPoint.ToRawRangeBoundary(),
+ aEndPoint.ToRawRangeBoundary(), IgnoreErrors());
+ if (MOZ_UNLIKELY(!range)) {
+ return nullptr;
+ }
+ if (NS_FAILED(ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
+ *range, aEditSubAction, aBlockInlineCheck, aEditingHost)) ||
+ MOZ_UNLIKELY(!range->IsPositioned())) {
+ return nullptr;
+ }
+ return range.forget();
+ }
+
+ /**
+ * Splits text nodes if each range end is in middle of a text node, then,
+ * calls HTMLEditor::SplitParentInlineElementsAtRangeBoundaries() for each
+ * range. Finally, updates ranges to keep edit target ranges as expected.
+ *
+ * @param aHTMLEditor The HTMLEditor which will handle the splittings.
+ * @param aBlockInlineCheck Considering block vs inline with whether the
+ * computed style or the HTML default style.
+ * @param aElement The editing host.
+ * @param aAncestorLimiter A content node which you don't want this to
+ * split it.
+ * @return A suggest point to put caret if succeeded, but
+ * it may be unset.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ HTMLEditor& aHTMLEditor, BlockInlineCheck aBlockInlineCheck,
+ const dom::Element& aEditingHost,
+ const nsIContent* aAncestorLimiter = nullptr);
+
+ /**
+ * CollectEditTargetNodes() collects edit target nodes the ranges.
+ * First, this collects all nodes in given ranges, then, modifies the
+ * result for specific edit sub-actions.
+ */
+ enum class CollectNonEditableNodes { No, Yes };
+ nsresult CollectEditTargetNodes(
+ const HTMLEditor& aHTMLEditor,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ EditSubAction aEditSubAction,
+ CollectNonEditableNodes aCollectNonEditableNodes) const;
+
+ /**
+ * Retrieve a closest ancestor list element of a common ancestor of _A_ range
+ * of the ranges. This tries to retrieve it from the first range to the last
+ * range.
+ */
+ dom::Element* GetClosestAncestorAnyListElementOfRange() const;
+
+ private:
+ static nsresult ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
+ nsRange& aRange, EditSubAction aEditSubAction,
+ BlockInlineCheck aBlockInlineCheck, const dom::Element& aEditingHost);
+
+ AutoTArray<mozilla::OwningNonNull<nsRange>, 8> mRanges;
+ RefPtr<nsRange> mAnchorFocusRange;
+ nsDirection mDirection = nsDirection::eDirNext;
+ Maybe<SelectionState> mSavedRanges;
+ RefPtr<HTMLEditor> mTrackingHTMLEditor;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef AutoRangeArray_h
diff --git a/editor/libeditor/CSSEditUtils.cpp b/editor/libeditor/CSSEditUtils.cpp
new file mode 100644
index 0000000000..15aec6a4ab
--- /dev/null
+++ b/editor/libeditor/CSSEditUtils.cpp
@@ -0,0 +1,1305 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CSSEditUtils.h"
+
+#include "ChangeStyleTransaction.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditor.h"
+#include "HTMLEditUtils.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/DeclarationBlock.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ServoCSSParser.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsCSSProps.h"
+#include "nsColor.h"
+#include "nsComputedDOMStyle.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsAtom.h"
+#include "nsIContent.h"
+#include "nsICSSDeclaration.h"
+#include "nsINode.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsUtils.h"
+#include "nsLiteralString.h"
+#include "nsPIDOMWindow.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStringIterator.h"
+#include "nsStyledElement.h"
+#include "nsUnicharUtils.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+static void ProcessBValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString) {
+ if (aInputString && aInputString->EqualsLiteral("-moz-editor-invert-value")) {
+ aOutputString.AssignLiteral("normal");
+ } else {
+ aOutputString.AssignLiteral("bold");
+ }
+}
+
+static void ProcessDefaultValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString) {
+ CopyASCIItoUTF16(MakeStringSpan(aDefaultValueString), aOutputString);
+}
+
+static void ProcessSameValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString) {
+ if (aInputString) {
+ aOutputString.Assign(*aInputString);
+ } else
+ aOutputString.Truncate();
+}
+
+static void ProcessExtendedValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString) {
+ aOutputString.Truncate();
+ if (aInputString) {
+ if (aPrependString) {
+ AppendASCIItoUTF16(MakeStringSpan(aPrependString), aOutputString);
+ }
+ aOutputString.Append(*aInputString);
+ if (aAppendString) {
+ AppendASCIItoUTF16(MakeStringSpan(aAppendString), aOutputString);
+ }
+ }
+}
+
+static void ProcessLengthValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString) {
+ aOutputString.Truncate();
+ if (aInputString) {
+ aOutputString.Append(*aInputString);
+ if (-1 == aOutputString.FindChar(char16_t('%'))) {
+ aOutputString.AppendLiteral("px");
+ }
+ }
+}
+
+static void ProcessListStyleTypeValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString) {
+ aOutputString.Truncate();
+ if (aInputString) {
+ if (aInputString->EqualsLiteral("1")) {
+ aOutputString.AppendLiteral("decimal");
+ } else if (aInputString->EqualsLiteral("a")) {
+ aOutputString.AppendLiteral("lower-alpha");
+ } else if (aInputString->EqualsLiteral("A")) {
+ aOutputString.AppendLiteral("upper-alpha");
+ } else if (aInputString->EqualsLiteral("i")) {
+ aOutputString.AppendLiteral("lower-roman");
+ } else if (aInputString->EqualsLiteral("I")) {
+ aOutputString.AppendLiteral("upper-roman");
+ } else if (aInputString->EqualsLiteral("square") ||
+ aInputString->EqualsLiteral("circle") ||
+ aInputString->EqualsLiteral("disc")) {
+ aOutputString.Append(*aInputString);
+ }
+ }
+}
+
+static void ProcessMarginLeftValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString) {
+ aOutputString.Truncate();
+ if (aInputString) {
+ if (aInputString->EqualsLiteral("center") ||
+ aInputString->EqualsLiteral("-moz-center")) {
+ aOutputString.AppendLiteral("auto");
+ } else if (aInputString->EqualsLiteral("right") ||
+ aInputString->EqualsLiteral("-moz-right")) {
+ aOutputString.AppendLiteral("auto");
+ } else {
+ aOutputString.AppendLiteral("0px");
+ }
+ }
+}
+
+static void ProcessMarginRightValue(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString) {
+ aOutputString.Truncate();
+ if (aInputString) {
+ if (aInputString->EqualsLiteral("center") ||
+ aInputString->EqualsLiteral("-moz-center")) {
+ aOutputString.AppendLiteral("auto");
+ } else if (aInputString->EqualsLiteral("left") ||
+ aInputString->EqualsLiteral("-moz-left")) {
+ aOutputString.AppendLiteral("auto");
+ } else {
+ aOutputString.AppendLiteral("0px");
+ }
+ }
+}
+
+#define CSS_EQUIV_TABLE_NONE \
+ { CSSEditUtils::eCSSEditableProperty_NONE, 0 }
+
+const CSSEditUtils::CSSEquivTable boldEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_font_weight, true, false, ProcessBValue,
+ nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable italicEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_font_style, true, false,
+ ProcessDefaultValue, "italic", nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable underlineEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_text_decoration, true, false,
+ ProcessDefaultValue, "underline", nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable strikeEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_text_decoration, true, false,
+ ProcessDefaultValue, "line-through", nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable ttEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_font_family, true, false,
+ ProcessDefaultValue, "monospace", nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable fontColorEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_color, true, false, ProcessSameValue,
+ nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable fontFaceEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_font_family, true, false,
+ ProcessSameValue, nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable fontSizeEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_font_size, true, false,
+ ProcessSameValue, nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable bgcolorEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_background_color, true, false,
+ ProcessSameValue, nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable backgroundImageEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_background_image, true, true,
+ ProcessExtendedValue, nullptr, "url(", ")"},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable textColorEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_color, true, false, ProcessSameValue,
+ nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable borderEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_border, true, false,
+ ProcessExtendedValue, nullptr, nullptr, "px solid"},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable textAlignEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_text_align, true, false,
+ ProcessSameValue, nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable captionAlignEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_caption_side, true, false,
+ ProcessSameValue, nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable verticalAlignEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_vertical_align, true, false,
+ ProcessSameValue, nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable nowrapEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_whitespace, true, false,
+ ProcessDefaultValue, "nowrap", nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable widthEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_width, true, false, ProcessLengthValue,
+ nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable heightEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_height, true, false, ProcessLengthValue,
+ nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable listStyleTypeEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_list_style_type, true, true,
+ ProcessListStyleTypeValue, nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable tableAlignEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_text_align, false, false,
+ ProcessDefaultValue, "left", nullptr, nullptr},
+ {CSSEditUtils::eCSSEditableProperty_margin_left, true, false,
+ ProcessMarginLeftValue, nullptr, nullptr, nullptr},
+ {CSSEditUtils::eCSSEditableProperty_margin_right, true, false,
+ ProcessMarginRightValue, nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+const CSSEditUtils::CSSEquivTable hrAlignEquivTable[] = {
+ {CSSEditUtils::eCSSEditableProperty_margin_left, true, false,
+ ProcessMarginLeftValue, nullptr, nullptr, nullptr},
+ {CSSEditUtils::eCSSEditableProperty_margin_right, true, false,
+ ProcessMarginRightValue, nullptr, nullptr, nullptr},
+ CSS_EQUIV_TABLE_NONE};
+
+#undef CSS_EQUIV_TABLE_NONE
+
+// static
+bool CSSEditUtils::IsCSSEditableStyle(const Element& aElement,
+ const EditorElementStyle& aStyle) {
+ return CSSEditUtils::IsCSSEditableStyle(*aElement.NodeInfo()->NameAtom(),
+ aStyle);
+}
+
+// static
+bool CSSEditUtils::IsCSSEditableStyle(const nsAtom& aTagName,
+ const EditorElementStyle& aStyle) {
+ nsStaticAtom* const htmlProperty =
+ aStyle.IsInlineStyle() ? aStyle.AsInlineStyle().mHTMLProperty : nullptr;
+ nsAtom* const attributeOrStyle = aStyle.IsInlineStyle()
+ ? aStyle.AsInlineStyle().mAttribute.get()
+ : aStyle.Style();
+
+ // HTML inline styles <b>, <i>, <tt> (chrome only), <u>, <strike>, <font
+ // color> and <font style>.
+ if (nsGkAtoms::b == htmlProperty || nsGkAtoms::i == htmlProperty ||
+ nsGkAtoms::tt == htmlProperty || nsGkAtoms::u == htmlProperty ||
+ nsGkAtoms::strike == htmlProperty ||
+ (nsGkAtoms::font == htmlProperty &&
+ (attributeOrStyle == nsGkAtoms::color ||
+ attributeOrStyle == nsGkAtoms::face))) {
+ return true;
+ }
+
+ // ALIGN attribute on elements supporting it
+ if (attributeOrStyle == nsGkAtoms::align &&
+ (&aTagName == nsGkAtoms::div || &aTagName == nsGkAtoms::p ||
+ &aTagName == nsGkAtoms::h1 || &aTagName == nsGkAtoms::h2 ||
+ &aTagName == nsGkAtoms::h3 || &aTagName == nsGkAtoms::h4 ||
+ &aTagName == nsGkAtoms::h5 || &aTagName == nsGkAtoms::h6 ||
+ &aTagName == nsGkAtoms::td || &aTagName == nsGkAtoms::th ||
+ &aTagName == nsGkAtoms::table || &aTagName == nsGkAtoms::hr ||
+ // For the above, why not use
+ // HTMLEditUtils::SupportsAlignAttr?
+ // It also checks for tbody, tfoot, thead.
+ // Let's add the following elements here even
+ // if "align" has a different meaning for them
+ &aTagName == nsGkAtoms::legend || &aTagName == nsGkAtoms::caption)) {
+ return true;
+ }
+
+ if (attributeOrStyle == nsGkAtoms::valign &&
+ (&aTagName == nsGkAtoms::col || &aTagName == nsGkAtoms::colgroup ||
+ &aTagName == nsGkAtoms::tbody || &aTagName == nsGkAtoms::td ||
+ &aTagName == nsGkAtoms::th || &aTagName == nsGkAtoms::tfoot ||
+ &aTagName == nsGkAtoms::thead || &aTagName == nsGkAtoms::tr)) {
+ return true;
+ }
+
+ // attributes TEXT, BACKGROUND and BGCOLOR on <body>
+ if (&aTagName == nsGkAtoms::body &&
+ (attributeOrStyle == nsGkAtoms::text ||
+ attributeOrStyle == nsGkAtoms::background ||
+ attributeOrStyle == nsGkAtoms::bgcolor)) {
+ return true;
+ }
+
+ // attribute BGCOLOR on other elements
+ if (attributeOrStyle == nsGkAtoms::bgcolor) {
+ return true;
+ }
+
+ // attributes HEIGHT, WIDTH and NOWRAP on <td> and <th>
+ if ((&aTagName == nsGkAtoms::td || &aTagName == nsGkAtoms::th) &&
+ (attributeOrStyle == nsGkAtoms::height ||
+ attributeOrStyle == nsGkAtoms::width ||
+ attributeOrStyle == nsGkAtoms::nowrap)) {
+ return true;
+ }
+
+ // attributes HEIGHT and WIDTH on <table>
+ if (&aTagName == nsGkAtoms::table && (attributeOrStyle == nsGkAtoms::height ||
+ attributeOrStyle == nsGkAtoms::width)) {
+ return true;
+ }
+
+ // attributes SIZE and WIDTH on <hr>
+ if (&aTagName == nsGkAtoms::hr && (attributeOrStyle == nsGkAtoms::size ||
+ attributeOrStyle == nsGkAtoms::width)) {
+ return true;
+ }
+
+ // attribute TYPE on <ol>, <ul> and <li>
+ if (attributeOrStyle == nsGkAtoms::type &&
+ (&aTagName == nsGkAtoms::ol || &aTagName == nsGkAtoms::ul ||
+ &aTagName == nsGkAtoms::li)) {
+ return true;
+ }
+
+ if (&aTagName == nsGkAtoms::img && (attributeOrStyle == nsGkAtoms::border ||
+ attributeOrStyle == nsGkAtoms::width ||
+ attributeOrStyle == nsGkAtoms::height)) {
+ return true;
+ }
+
+ // other elements that we can align using CSS even if they
+ // can't carry the html ALIGN attribute
+ if (attributeOrStyle == nsGkAtoms::align &&
+ (&aTagName == nsGkAtoms::ul || &aTagName == nsGkAtoms::ol ||
+ &aTagName == nsGkAtoms::dl || &aTagName == nsGkAtoms::li ||
+ &aTagName == nsGkAtoms::dd || &aTagName == nsGkAtoms::dt ||
+ &aTagName == nsGkAtoms::address || &aTagName == nsGkAtoms::pre)) {
+ return true;
+ }
+
+ return false;
+}
+
+// The lowest level above the transaction; adds the CSS declaration
+// "aProperty : aValue" to the inline styles carried by aStyledElement
+
+// static
+nsresult CSSEditUtils::SetCSSPropertyInternal(HTMLEditor& aHTMLEditor,
+ nsStyledElement& aStyledElement,
+ nsAtom& aProperty,
+ const nsAString& aValue,
+ bool aSuppressTxn) {
+ RefPtr<ChangeStyleTransaction> transaction =
+ ChangeStyleTransaction::Create(aStyledElement, aProperty, aValue);
+ if (aSuppressTxn) {
+ nsresult rv = transaction->DoTransaction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "ChangeStyleTransaction::DoTransaction() failed");
+ return rv;
+ }
+ nsresult rv = aHTMLEditor.DoTransactionInternal(transaction);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+ return rv;
+}
+
+// static
+nsresult CSSEditUtils::SetCSSPropertyPixelsWithTransaction(
+ HTMLEditor& aHTMLEditor, nsStyledElement& aStyledElement, nsAtom& aProperty,
+ int32_t aIntValue) {
+ nsAutoString s;
+ s.AppendInt(aIntValue);
+ nsresult rv = SetCSSPropertyWithTransaction(aHTMLEditor, aStyledElement,
+ aProperty, s + u"px"_ns);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyWithTransaction() failed");
+ return rv;
+}
+
+// static
+nsresult CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ nsStyledElement& aStyledElement, const nsAtom& aProperty,
+ int32_t aIntValue) {
+ nsCOMPtr<nsICSSDeclaration> cssDecl = aStyledElement.Style();
+
+ nsAutoCString propertyNameString;
+ aProperty.ToUTF8String(propertyNameString);
+
+ nsAutoCString s;
+ s.AppendInt(aIntValue);
+ s.AppendLiteral("px");
+
+ ErrorResult error;
+ cssDecl->SetProperty(propertyNameString, s, EmptyCString(), error);
+ if (error.Failed()) {
+ NS_WARNING("nsICSSDeclaration::SetProperty() failed");
+ return error.StealNSResult();
+ }
+
+ return NS_OK;
+}
+
+// The lowest level above the transaction; removes the value aValue from the
+// list of values specified for the CSS property aProperty, or totally remove
+// the declaration if this property accepts only one value
+
+// static
+nsresult CSSEditUtils::RemoveCSSPropertyInternal(
+ HTMLEditor& aHTMLEditor, nsStyledElement& aStyledElement, nsAtom& aProperty,
+ const nsAString& aValue, bool aSuppressTxn) {
+ RefPtr<ChangeStyleTransaction> transaction =
+ ChangeStyleTransaction::CreateToRemove(aStyledElement, aProperty, aValue);
+ if (aSuppressTxn) {
+ nsresult rv = transaction->DoTransaction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "ChangeStyleTransaction::DoTransaction() failed");
+ return rv;
+ }
+ nsresult rv = aHTMLEditor.DoTransactionInternal(transaction);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+ return rv;
+}
+
+// static
+nsresult CSSEditUtils::GetSpecifiedProperty(nsIContent& aContent,
+ nsAtom& aCSSProperty,
+ nsAString& aValue) {
+ nsresult rv =
+ GetSpecifiedCSSInlinePropertyBase(aContent, aCSSProperty, aValue);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::GeSpecifiedCSSInlinePropertyBase() failed");
+ return rv;
+}
+
+// static
+nsresult CSSEditUtils::GetComputedProperty(nsIContent& aContent,
+ nsAtom& aCSSProperty,
+ nsAString& aValue) {
+ nsresult rv =
+ GetComputedCSSInlinePropertyBase(aContent, aCSSProperty, aValue);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::GetComputedCSSInlinePropertyBase() failed");
+ return rv;
+}
+
+// static
+nsresult CSSEditUtils::GetComputedCSSInlinePropertyBase(nsIContent& aContent,
+ nsAtom& aCSSProperty,
+ nsAString& aValue) {
+ aValue.Truncate();
+
+ RefPtr<Element> element = aContent.GetAsElementOrParentElement();
+ if (NS_WARN_IF(!element)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Get the all the computed css styles attached to the element node
+ RefPtr<nsComputedDOMStyle> computedDOMStyle = GetComputedStyle(element);
+ if (NS_WARN_IF(!computedDOMStyle)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // from these declarations, get the one we want and that one only
+ //
+ // FIXME(bug 1606994): nsAtomCString copies, we should just keep around the
+ // property id.
+ //
+ // FIXME: Maybe we can avoid copying aValue too, though it's no worse than
+ // what we used to do.
+ nsAutoCString value;
+ computedDOMStyle->GetPropertyValue(nsAtomCString(&aCSSProperty), value);
+ CopyUTF8toUTF16(value, aValue);
+ return NS_OK;
+}
+
+// static
+nsresult CSSEditUtils::GetSpecifiedCSSInlinePropertyBase(nsIContent& aContent,
+ nsAtom& aCSSProperty,
+ nsAString& aValue) {
+ aValue.Truncate();
+
+ RefPtr<Element> element = aContent.GetAsElementOrParentElement();
+ if (NS_WARN_IF(!element)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<DeclarationBlock> decl = element->GetInlineStyleDeclaration();
+ if (!decl) {
+ return NS_OK;
+ }
+
+ // FIXME: Same comments as above.
+ nsCSSPropertyID prop =
+ nsCSSProps::LookupProperty(nsAtomCString(&aCSSProperty));
+ MOZ_ASSERT(prop != eCSSProperty_UNKNOWN);
+
+ nsAutoCString value;
+ decl->GetPropertyValueByID(prop, value);
+ CopyUTF8toUTF16(value, aValue);
+ return NS_OK;
+}
+
+// static
+already_AddRefed<nsComputedDOMStyle> CSSEditUtils::GetComputedStyle(
+ Element* aElement) {
+ MOZ_ASSERT(aElement);
+
+ Document* document = aElement->GetComposedDoc();
+ if (NS_WARN_IF(!document)) {
+ return nullptr;
+ }
+
+ RefPtr<nsComputedDOMStyle> computedDOMStyle = NS_NewComputedDOMStyle(
+ aElement, u""_ns, document, nsComputedDOMStyle::StyleType::All,
+ IgnoreErrors());
+ return computedDOMStyle.forget();
+}
+
+// remove the CSS style "aProperty : aPropertyValue" and possibly remove the
+// whole node if it is a span and if its only attribute is _moz_dirty
+
+// static
+Result<EditorDOMPoint, nsresult>
+CSSEditUtils::RemoveCSSInlineStyleWithTransaction(
+ HTMLEditor& aHTMLEditor, nsStyledElement& aStyledElement, nsAtom* aProperty,
+ const nsAString& aPropertyValue) {
+ // remove the property from the style attribute
+ nsresult rv = RemoveCSSPropertyWithTransaction(aHTMLEditor, aStyledElement,
+ *aProperty, aPropertyValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::RemoveCSSPropertyWithTransaction() failed");
+ return Err(rv);
+ }
+
+ if (!aStyledElement.IsHTMLElement(nsGkAtoms::span) ||
+ HTMLEditUtils::ElementHasAttribute(aStyledElement)) {
+ return EditorDOMPoint();
+ }
+
+ Result<EditorDOMPoint, nsresult> unwrapStyledElementResult =
+ aHTMLEditor.RemoveContainerWithTransaction(aStyledElement);
+ NS_WARNING_ASSERTION(unwrapStyledElementResult.isOk(),
+ "HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapStyledElementResult;
+}
+
+// Get the default browser background color if we need it for
+// GetCSSBackgroundColorState
+
+// static
+void CSSEditUtils::GetDefaultBackgroundColor(nsAString& aColor) {
+ if (MOZ_UNLIKELY(StaticPrefs::editor_use_custom_colors())) {
+ nsresult rv = Preferences::GetString("editor.background_color", aColor);
+ // XXX Why don't you validate the pref value?
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to get editor.background_color");
+ aColor.AssignLiteral("#ffffff"); // Default to white
+ }
+ return;
+ }
+
+ if (Preferences::GetBool("browser.display.use_system_colors", false)) {
+ return;
+ }
+
+ nsresult rv =
+ Preferences::GetString("browser.display.background_color", aColor);
+ // XXX Why don't you validate the pref value?
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to get browser.display.background_color");
+ aColor.AssignLiteral("#ffffff"); // Default to white
+ }
+}
+
+// static
+void CSSEditUtils::ParseLength(const nsAString& aString, float* aValue,
+ nsAtom** aUnit) {
+ if (aString.IsEmpty()) {
+ *aValue = 0;
+ *aUnit = NS_Atomize(aString).take();
+ return;
+ }
+
+ nsAString::const_iterator iter;
+ aString.BeginReading(iter);
+
+ float a = 10.0f, b = 1.0f, value = 0;
+ int8_t sign = 1;
+ int32_t i = 0, j = aString.Length();
+ char16_t c;
+ bool floatingPointFound = false;
+ c = *iter;
+ if (char16_t('-') == c) {
+ sign = -1;
+ iter++;
+ i++;
+ } else if (char16_t('+') == c) {
+ iter++;
+ i++;
+ }
+ while (i < j) {
+ c = *iter;
+ if ((char16_t('0') == c) || (char16_t('1') == c) || (char16_t('2') == c) ||
+ (char16_t('3') == c) || (char16_t('4') == c) || (char16_t('5') == c) ||
+ (char16_t('6') == c) || (char16_t('7') == c) || (char16_t('8') == c) ||
+ (char16_t('9') == c)) {
+ value = (value * a) + (b * (c - char16_t('0')));
+ b = b / 10 * a;
+ } else if (!floatingPointFound && (char16_t('.') == c)) {
+ floatingPointFound = true;
+ a = 1.0f;
+ b = 0.1f;
+ } else
+ break;
+ iter++;
+ i++;
+ }
+ *aValue = value * sign;
+ *aUnit = NS_Atomize(StringTail(aString, j - i)).take();
+}
+
+// static
+nsStaticAtom* CSSEditUtils::GetCSSPropertyAtom(
+ nsCSSEditableProperty aProperty) {
+ switch (aProperty) {
+ case eCSSEditableProperty_background_color:
+ return nsGkAtoms::backgroundColor;
+ case eCSSEditableProperty_background_image:
+ return nsGkAtoms::background_image;
+ case eCSSEditableProperty_border:
+ return nsGkAtoms::border;
+ case eCSSEditableProperty_caption_side:
+ return nsGkAtoms::caption_side;
+ case eCSSEditableProperty_color:
+ return nsGkAtoms::color;
+ case eCSSEditableProperty_float:
+ return nsGkAtoms::_float;
+ case eCSSEditableProperty_font_family:
+ return nsGkAtoms::font_family;
+ case eCSSEditableProperty_font_size:
+ return nsGkAtoms::font_size;
+ case eCSSEditableProperty_font_style:
+ return nsGkAtoms::font_style;
+ case eCSSEditableProperty_font_weight:
+ return nsGkAtoms::fontWeight;
+ case eCSSEditableProperty_height:
+ return nsGkAtoms::height;
+ case eCSSEditableProperty_list_style_type:
+ return nsGkAtoms::list_style_type;
+ case eCSSEditableProperty_margin_left:
+ return nsGkAtoms::marginLeft;
+ case eCSSEditableProperty_margin_right:
+ return nsGkAtoms::marginRight;
+ case eCSSEditableProperty_text_align:
+ return nsGkAtoms::textAlign;
+ case eCSSEditableProperty_text_decoration:
+ return nsGkAtoms::text_decoration;
+ case eCSSEditableProperty_vertical_align:
+ return nsGkAtoms::vertical_align;
+ case eCSSEditableProperty_whitespace:
+ return nsGkAtoms::white_space;
+ case eCSSEditableProperty_width:
+ return nsGkAtoms::width;
+ case eCSSEditableProperty_NONE:
+ // intentionally empty
+ return nullptr;
+ }
+ MOZ_ASSERT_UNREACHABLE("Got unknown property");
+ return nullptr;
+}
+
+// static
+void CSSEditUtils::GetCSSDeclarations(
+ const CSSEquivTable* aEquivTable, const nsAString* aValue,
+ HandlingFor aHandlingFor, nsTArray<CSSDeclaration>& aOutCSSDeclarations) {
+ // clear arrays
+ aOutCSSDeclarations.Clear();
+
+ // if we have an input value, let's use it
+ nsAutoString value, lowerCasedValue;
+ if (aValue) {
+ value.Assign(*aValue);
+ lowerCasedValue.Assign(*aValue);
+ ToLowerCase(lowerCasedValue);
+ }
+
+ for (size_t index = 0;; index++) {
+ const nsCSSEditableProperty cssProperty = aEquivTable[index].cssProperty;
+ if (!cssProperty) {
+ break;
+ }
+ if (aHandlingFor == HandlingFor::SettingStyle ||
+ aEquivTable[index].gettable) {
+ nsAutoString cssValue, cssPropertyString;
+ // find the equivalent css value for the index-th property in
+ // the equivalence table
+ (*aEquivTable[index].processValueFunctor)(
+ (aHandlingFor == HandlingFor::SettingStyle ||
+ aEquivTable[index].caseSensitiveValue)
+ ? &value
+ : &lowerCasedValue,
+ cssValue, aEquivTable[index].defaultValue,
+ aEquivTable[index].prependValue, aEquivTable[index].appendValue);
+ nsStaticAtom* const propertyAtom = GetCSSPropertyAtom(cssProperty);
+ if (MOZ_LIKELY(propertyAtom)) {
+ aOutCSSDeclarations.AppendElement(
+ CSSDeclaration{*propertyAtom, cssValue});
+ }
+ }
+ }
+}
+
+// static
+void CSSEditUtils::GetCSSDeclarations(
+ Element& aElement, const EditorElementStyle& aStyle,
+ const nsAString* aValue, HandlingFor aHandlingFor,
+ nsTArray<CSSDeclaration>& aOutCSSDeclarations) {
+ nsStaticAtom* const htmlProperty =
+ aStyle.IsInlineStyle() ? aStyle.AsInlineStyle().mHTMLProperty : nullptr;
+ const RefPtr<nsAtom> attributeOrStyle =
+ aStyle.IsInlineStyle() ? aStyle.AsInlineStyle().mAttribute
+ : aStyle.Style();
+
+ const auto* equivTable = [&]() -> const CSSEditUtils::CSSEquivTable* {
+ if (nsGkAtoms::b == htmlProperty) {
+ return boldEquivTable;
+ }
+ if (nsGkAtoms::i == htmlProperty) {
+ return italicEquivTable;
+ }
+ if (nsGkAtoms::u == htmlProperty) {
+ return underlineEquivTable;
+ }
+ if (nsGkAtoms::strike == htmlProperty) {
+ return strikeEquivTable;
+ }
+ if (nsGkAtoms::tt == htmlProperty) {
+ return ttEquivTable;
+ }
+ if (!attributeOrStyle) {
+ return nullptr;
+ }
+ if (nsGkAtoms::font == htmlProperty) {
+ if (attributeOrStyle == nsGkAtoms::color) {
+ return fontColorEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::face) {
+ return fontFaceEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::size) {
+ return fontSizeEquivTable;
+ }
+ MOZ_ASSERT(attributeOrStyle == nsGkAtoms::bgcolor);
+ }
+ if (attributeOrStyle == nsGkAtoms::bgcolor) {
+ return bgcolorEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::background) {
+ return backgroundImageEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::text) {
+ return textColorEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::border) {
+ return borderEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::align) {
+ if (aElement.IsHTMLElement(nsGkAtoms::table)) {
+ return tableAlignEquivTable;
+ }
+ if (aElement.IsHTMLElement(nsGkAtoms::hr)) {
+ return hrAlignEquivTable;
+ }
+ if (aElement.IsAnyOfHTMLElements(nsGkAtoms::legend, nsGkAtoms::caption)) {
+ return captionAlignEquivTable;
+ }
+ return textAlignEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::valign) {
+ return verticalAlignEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::nowrap) {
+ return nowrapEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::width) {
+ return widthEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::height ||
+ (aElement.IsHTMLElement(nsGkAtoms::hr) &&
+ attributeOrStyle == nsGkAtoms::size)) {
+ return heightEquivTable;
+ }
+ if (attributeOrStyle == nsGkAtoms::type &&
+ aElement.IsAnyOfHTMLElements(nsGkAtoms::ol, nsGkAtoms::ul,
+ nsGkAtoms::li)) {
+ return listStyleTypeEquivTable;
+ }
+ return nullptr;
+ }();
+ if (equivTable) {
+ GetCSSDeclarations(equivTable, aValue, aHandlingFor, aOutCSSDeclarations);
+ }
+}
+
+// Add to aNode the CSS inline style equivalent to HTMLProperty/aAttribute/
+// aValue for the node, and return in aCount the number of CSS properties set
+// by the call. The Element version returns aCount instead.
+Result<size_t, nsresult> CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction aWithTransaction, HTMLEditor& aHTMLEditor,
+ nsStyledElement& aStyledElement, const EditorElementStyle& aStyleToSet,
+ const nsAString* aValue) {
+ MOZ_DIAGNOSTIC_ASSERT(aStyleToSet.IsCSSSettable(aStyledElement));
+
+ // we can apply the styles only if the node is an element and if we have
+ // an equivalence for the requested HTML style in this implementation
+
+ // Find the CSS equivalence to the HTML style
+ AutoTArray<CSSDeclaration, 4> cssDeclarations;
+ GetCSSDeclarations(aStyledElement, aStyleToSet, aValue,
+ HandlingFor::SettingStyle, cssDeclarations);
+
+ // set the individual CSS inline styles
+ for (const CSSDeclaration& cssDeclaration : cssDeclarations) {
+ nsresult rv = SetCSSPropertyInternal(
+ aHTMLEditor, aStyledElement, MOZ_KnownLive(cssDeclaration.mProperty),
+ cssDeclaration.mValue, aWithTransaction == WithTransaction::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::SetCSSPropertyInternal() failed");
+ return Err(rv);
+ }
+ }
+ return cssDeclarations.Length();
+}
+
+// static
+nsresult CSSEditUtils::RemoveCSSEquivalentToStyle(
+ WithTransaction aWithTransaction, HTMLEditor& aHTMLEditor,
+ nsStyledElement& aStyledElement, const EditorElementStyle& aStyleToRemove,
+ const nsAString* aValue) {
+ MOZ_DIAGNOSTIC_ASSERT(aStyleToRemove.IsCSSRemovable(aStyledElement));
+
+ // we can apply the styles only if the node is an element and if we have
+ // an equivalence for the requested HTML style in this implementation
+
+ // Find the CSS equivalence to the HTML style
+ AutoTArray<CSSDeclaration, 4> cssDeclarations;
+ GetCSSDeclarations(aStyledElement, aStyleToRemove, aValue,
+ HandlingFor::RemovingStyle, cssDeclarations);
+
+ // remove the individual CSS inline styles
+ for (const CSSDeclaration& cssDeclaration : cssDeclarations) {
+ nsresult rv = RemoveCSSPropertyInternal(
+ aHTMLEditor, aStyledElement, MOZ_KnownLive(cssDeclaration.mProperty),
+ cssDeclaration.mValue, aWithTransaction == WithTransaction::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::RemoveCSSPropertyWithoutTransaction() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+// static
+nsresult CSSEditUtils::GetComputedCSSEquivalentTo(
+ Element& aElement, const EditorElementStyle& aStyle, nsAString& aOutValue) {
+ return GetCSSEquivalentTo(aElement, aStyle, aOutValue, StyleType::Computed);
+}
+
+// static
+nsresult CSSEditUtils::GetCSSEquivalentTo(Element& aElement,
+ const EditorElementStyle& aStyle,
+ nsAString& aOutValue,
+ StyleType aStyleType) {
+ MOZ_ASSERT_IF(aStyle.IsInlineStyle(),
+ !aStyle.AsInlineStyle().IsStyleToClearAllInlineStyles());
+ MOZ_DIAGNOSTIC_ASSERT(aStyle.IsCSSSettable(aElement) ||
+ aStyle.IsCSSRemovable(aElement));
+
+ aOutValue.Truncate();
+ AutoTArray<CSSDeclaration, 4> cssDeclarations;
+ GetCSSDeclarations(aElement, aStyle, nullptr, HandlingFor::GettingStyle,
+ cssDeclarations);
+ nsAutoString valueString;
+ for (const CSSDeclaration& cssDeclaration : cssDeclarations) {
+ valueString.Truncate();
+ // retrieve the specified/computed value of the property
+ if (aStyleType == StyleType::Computed) {
+ nsresult rv = GetComputedCSSInlinePropertyBase(
+ aElement, MOZ_KnownLive(cssDeclaration.mProperty), valueString);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::GetComputedCSSInlinePropertyBase() failed");
+ return rv;
+ }
+ } else {
+ nsresult rv = GetSpecifiedCSSInlinePropertyBase(
+ aElement, cssDeclaration.mProperty, valueString);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::GetSpecifiedCSSInlinePropertyBase() failed");
+ return rv;
+ }
+ }
+ // append the value to aOutValue (possibly with a leading white-space)
+ if (!aOutValue.IsEmpty()) {
+ aOutValue.Append(HTMLEditUtils::kSpace);
+ }
+ aOutValue.Append(valueString);
+ }
+ return NS_OK;
+}
+
+// Does the node aContent (or its parent, if it's not an element node) have a
+// CSS style equivalent to the HTML style
+// aHTMLProperty/aAttribute/valueString? The value of aStyleType controls
+// the styles we retrieve: specified or computed. The return value aIsSet is
+// true if the CSS styles are set.
+//
+// The nsIContent variant returns aIsSet instead of using an out parameter, and
+// does not modify aValue.
+
+// static
+Result<bool, nsresult> CSSEditUtils::IsComputedCSSEquivalentTo(
+ const HTMLEditor& aHTMLEditor, nsIContent& aContent,
+ const EditorInlineStyle& aStyle, nsAString& aInOutValue) {
+ return IsCSSEquivalentTo(aHTMLEditor, aContent, aStyle, aInOutValue,
+ StyleType::Computed);
+}
+
+// static
+Result<bool, nsresult> CSSEditUtils::IsSpecifiedCSSEquivalentTo(
+ const HTMLEditor& aHTMLEditor, nsIContent& aContent,
+ const EditorInlineStyle& aStyle, nsAString& aInOutValue) {
+ return IsCSSEquivalentTo(aHTMLEditor, aContent, aStyle, aInOutValue,
+ StyleType::Specified);
+}
+
+// static
+Result<bool, nsresult> CSSEditUtils::IsCSSEquivalentTo(
+ const HTMLEditor& aHTMLEditor, nsIContent& aContent,
+ const EditorInlineStyle& aStyle, nsAString& aInOutValue,
+ StyleType aStyleType) {
+ MOZ_ASSERT(!aStyle.IsStyleToClearAllInlineStyles());
+
+ nsAutoString htmlValueString(aInOutValue);
+ bool isSet = false;
+ // FYI: Cannot use InclusiveAncestorsOfType here because
+ // GetCSSEquivalentTo() may flush pending notifications.
+ for (RefPtr<Element> element = aContent.GetAsElementOrParentElement();
+ element; element = element->GetParentElement()) {
+ nsCOMPtr<nsINode> parentNode = element->GetParentNode();
+ aInOutValue.Assign(htmlValueString);
+ // get the value of the CSS equivalent styles
+ nsresult rv = GetCSSEquivalentTo(*element, aStyle, aInOutValue, aStyleType);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::GetCSSEquivalentToHTMLInlineStyleSetInternal() "
+ "failed");
+ return Err(rv);
+ }
+ if (NS_WARN_IF(parentNode != element->GetParentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // early way out if we can
+ if (aInOutValue.IsEmpty()) {
+ return isSet;
+ }
+
+ if (nsGkAtoms::b == aStyle.mHTMLProperty) {
+ if (aInOutValue.EqualsLiteral("bold")) {
+ isSet = true;
+ } else if (aInOutValue.EqualsLiteral("normal")) {
+ isSet = false;
+ } else if (aInOutValue.EqualsLiteral("bolder")) {
+ isSet = true;
+ aInOutValue.AssignLiteral("bold");
+ } else {
+ int32_t weight = 0;
+ nsresult rvIgnored;
+ nsAutoString value(aInOutValue);
+ weight = value.ToInteger(&rvIgnored);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsAString::ToInteger() failed, but ignored");
+ if (400 < weight) {
+ isSet = true;
+ aInOutValue.AssignLiteral(u"bold");
+ } else {
+ isSet = false;
+ aInOutValue.AssignLiteral(u"normal");
+ }
+ }
+ } else if (nsGkAtoms::i == aStyle.mHTMLProperty) {
+ if (aInOutValue.EqualsLiteral(u"italic") ||
+ aInOutValue.EqualsLiteral(u"oblique")) {
+ isSet = true;
+ }
+ } else if (nsGkAtoms::u == aStyle.mHTMLProperty) {
+ isSet = ChangeStyleTransaction::ValueIncludes(
+ NS_ConvertUTF16toUTF8(aInOutValue), "underline"_ns);
+ } else if (nsGkAtoms::strike == aStyle.mHTMLProperty) {
+ isSet = ChangeStyleTransaction::ValueIncludes(
+ NS_ConvertUTF16toUTF8(aInOutValue), "line-through"_ns);
+ } else if ((nsGkAtoms::font == aStyle.mHTMLProperty &&
+ aStyle.mAttribute == nsGkAtoms::color) ||
+ aStyle.mAttribute == nsGkAtoms::bgcolor) {
+ isSet = htmlValueString.IsEmpty() ||
+ HTMLEditUtils::IsSameCSSColorValue(htmlValueString, aInOutValue);
+ } else if (nsGkAtoms::tt == aStyle.mHTMLProperty) {
+ isSet = StringBeginsWith(aInOutValue, u"monospace"_ns);
+ } else if (nsGkAtoms::font == aStyle.mHTMLProperty &&
+ aStyle.mAttribute == nsGkAtoms::face) {
+ if (!htmlValueString.IsEmpty()) {
+ const char16_t commaSpace[] = {char16_t(','), HTMLEditUtils::kSpace, 0};
+ const char16_t comma[] = {char16_t(','), 0};
+ htmlValueString.ReplaceSubstring(commaSpace, comma);
+ nsAutoString valueStringNorm(aInOutValue);
+ valueStringNorm.ReplaceSubstring(commaSpace, comma);
+ isSet = htmlValueString.Equals(valueStringNorm,
+ nsCaseInsensitiveStringComparator);
+ } else {
+ isSet = true;
+ }
+ return isSet;
+ } else if (aStyle.IsStyleOfFontSize()) {
+ if (htmlValueString.IsEmpty()) {
+ return true;
+ }
+ switch (nsContentUtils::ParseLegacyFontSize(htmlValueString)) {
+ case 1:
+ return aInOutValue.EqualsLiteral("x-small");
+ case 2:
+ return aInOutValue.EqualsLiteral("small");
+ case 3:
+ return aInOutValue.EqualsLiteral("medium");
+ case 4:
+ return aInOutValue.EqualsLiteral("large");
+ case 5:
+ return aInOutValue.EqualsLiteral("x-large");
+ case 6:
+ return aInOutValue.EqualsLiteral("xx-large");
+ case 7:
+ return aInOutValue.EqualsLiteral("xxx-large");
+ }
+ return false;
+ } else if (aStyle.mAttribute == nsGkAtoms::align) {
+ isSet = true;
+ } else {
+ return false;
+ }
+
+ if (!htmlValueString.IsEmpty() &&
+ htmlValueString.Equals(aInOutValue,
+ nsCaseInsensitiveStringComparator)) {
+ isSet = true;
+ }
+
+ if (htmlValueString.EqualsLiteral(u"-moz-editor-invert-value")) {
+ isSet = !isSet;
+ }
+
+ if (isSet) {
+ return true;
+ }
+
+ if (!aStyle.IsStyleOfTextDecoration(
+ EditorInlineStyle::IgnoreSElement::Yes)) {
+ return isSet;
+ }
+
+ // Unfortunately, the value of the text-decoration property is not
+ // inherited. that means that we have to look at ancestors of node to see
+ // if they are underlined.
+ }
+ return isSet;
+}
+
+// static
+Result<bool, nsresult> CSSEditUtils::HaveComputedCSSEquivalentStyles(
+ const HTMLEditor& aHTMLEditor, nsIContent& aContent,
+ const EditorInlineStyle& aStyle) {
+ return HaveCSSEquivalentStyles(aHTMLEditor, aContent, aStyle,
+ StyleType::Computed);
+}
+
+// static
+Result<bool, nsresult> CSSEditUtils::HaveSpecifiedCSSEquivalentStyles(
+ const HTMLEditor& aHTMLEditor, nsIContent& aContent,
+ const EditorInlineStyle& aStyle) {
+ return HaveCSSEquivalentStyles(aHTMLEditor, aContent, aStyle,
+ StyleType::Specified);
+}
+
+// static
+Result<bool, nsresult> CSSEditUtils::HaveCSSEquivalentStyles(
+ const HTMLEditor& aHTMLEditor, nsIContent& aContent,
+ const EditorInlineStyle& aStyle, StyleType aStyleType) {
+ MOZ_ASSERT(!aStyle.IsStyleToClearAllInlineStyles());
+
+ // FYI: Unfortunately, we cannot use InclusiveAncestorsOfType here
+ // because GetCSSEquivalentTo() may flush pending notifications.
+ nsAutoString valueString;
+ for (RefPtr<Element> element = aContent.GetAsElementOrParentElement();
+ element; element = element->GetParentElement()) {
+ nsCOMPtr<nsINode> parentNode = element->GetParentNode();
+ // get the value of the CSS equivalent styles
+ nsresult rv = GetCSSEquivalentTo(*element, aStyle, valueString, aStyleType);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::GetCSSEquivalentToHTMLInlineStyleSetInternal() "
+ "failed");
+ return Err(rv);
+ }
+ if (NS_WARN_IF(parentNode != element->GetParentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (!valueString.IsEmpty()) {
+ return true;
+ }
+
+ if (!aStyle.IsStyleOfTextDecoration(
+ EditorInlineStyle::IgnoreSElement::Yes)) {
+ return false;
+ }
+
+ // Unfortunately, the value of the text-decoration property is not
+ // inherited.
+ // that means that we have to look at ancestors of node to see if they
+ // are underlined.
+ }
+
+ return false;
+}
+
+// ElementsSameStyle compares two elements and checks if they have the same
+// specified CSS declarations in the STYLE attribute
+// The answer is always negative if at least one of them carries an ID or a
+// class
+
+// static
+bool CSSEditUtils::DoStyledElementsHaveSameStyle(
+ nsStyledElement& aStyledElement, nsStyledElement& aOtherStyledElement) {
+ if (aStyledElement.HasAttr(nsGkAtoms::id) ||
+ aOtherStyledElement.HasAttr(nsGkAtoms::id)) {
+ // at least one of the spans carries an ID ; suspect a CSS rule applies to
+ // it and refuse to merge the nodes
+ return false;
+ }
+
+ nsAutoString firstClass, otherClass;
+ bool isElementClassSet =
+ aStyledElement.GetAttr(nsGkAtoms::_class, firstClass);
+ bool isOtherElementClassSet = aOtherStyledElement.GetAttr(
+ kNameSpaceID_None, nsGkAtoms::_class, otherClass);
+ if (isElementClassSet && isOtherElementClassSet) {
+ // both spans carry a class, let's compare them
+ if (!firstClass.Equals(otherClass)) {
+ // WARNING : technically, the comparison just above is questionable :
+ // from a pure HTML/CSS point of view class="a b" is NOT the same than
+ // class="b a" because a CSS rule could test the exact value of the class
+ // attribute to be "a b" for instance ; from a user's point of view, a
+ // wysiwyg editor should probably NOT make any difference. CSS people
+ // need to discuss this issue before any modification.
+ return false;
+ }
+ } else if (isElementClassSet || isOtherElementClassSet) {
+ // one span only carries a class, early way out
+ return false;
+ }
+
+ // XXX If `GetPropertyValue()` won't run script, we can stop using
+ // nsCOMPtr here.
+ nsCOMPtr<nsICSSDeclaration> firstCSSDecl = aStyledElement.Style();
+ if (!firstCSSDecl) {
+ NS_WARNING("nsStyledElement::Style() failed");
+ return false;
+ }
+ nsCOMPtr<nsICSSDeclaration> otherCSSDecl = aOtherStyledElement.Style();
+ if (!otherCSSDecl) {
+ NS_WARNING("nsStyledElement::Style() failed");
+ return false;
+ }
+
+ const uint32_t firstLength = firstCSSDecl->Length();
+ const uint32_t otherLength = otherCSSDecl->Length();
+ if (firstLength != otherLength) {
+ // early way out if we can
+ return false;
+ }
+
+ if (!firstLength) {
+ // no inline style !
+ return true;
+ }
+
+ for (uint32_t i = 0; i < firstLength; i++) {
+ nsAutoCString firstValue, otherValue;
+ nsAutoCString propertyNameString;
+ firstCSSDecl->Item(i, propertyNameString);
+ firstCSSDecl->GetPropertyValue(propertyNameString, firstValue);
+ otherCSSDecl->GetPropertyValue(propertyNameString, otherValue);
+ // FIXME: We need to handle all properties whose values are color.
+ // However, it's too expensive if we keep using string property names.
+ if (propertyNameString.EqualsLiteral("color") ||
+ propertyNameString.EqualsLiteral("background-color")) {
+ if (!HTMLEditUtils::IsSameCSSColorValue(firstValue, otherValue)) {
+ return false;
+ }
+ } else if (!firstValue.Equals(otherValue)) {
+ return false;
+ }
+ }
+ for (uint32_t i = 0; i < otherLength; i++) {
+ nsAutoCString firstValue, otherValue;
+ nsAutoCString propertyNameString;
+ otherCSSDecl->Item(i, propertyNameString);
+ otherCSSDecl->GetPropertyValue(propertyNameString, otherValue);
+ firstCSSDecl->GetPropertyValue(propertyNameString, firstValue);
+ // FIXME: We need to handle all properties whose values are color.
+ // However, it's too expensive if we keep using string property names.
+ if (propertyNameString.EqualsLiteral("color") ||
+ propertyNameString.EqualsLiteral("background-color")) {
+ if (!HTMLEditUtils::IsSameCSSColorValue(firstValue, otherValue)) {
+ return false;
+ }
+ } else if (!firstValue.Equals(otherValue)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/CSSEditUtils.h b/editor/libeditor/CSSEditUtils.h
new file mode 100644
index 0000000000..8df077eb71
--- /dev/null
+++ b/editor/libeditor/CSSEditUtils.h
@@ -0,0 +1,389 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CSSEditUtils_h
+#define CSSEditUtils_h
+
+#include "ChangeStyleTransaction.h" // for ChangeStyleTransaction
+#include "EditorForwards.h"
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsStringFwd.h"
+#include "nsTArray.h" // for nsTArray
+#include "nscore.h" // for nsAString, nsresult, nullptr
+
+class nsComputedDOMStyle;
+class nsAtom;
+class nsIContent;
+class nsICSSDeclaration;
+class nsINode;
+class nsStaticAtom;
+class nsStyledElement;
+
+namespace mozilla {
+namespace dom {
+class Element;
+} // namespace dom
+
+using nsProcessValueFunc = void (*)(const nsAString* aInputString,
+ nsAString& aOutputString,
+ const char* aDefaultValueString,
+ const char* aPrependString,
+ const char* aAppendString);
+
+class CSSEditUtils final {
+ public:
+ // To prevent the class being instantiated
+ CSSEditUtils() = delete;
+
+ enum nsCSSEditableProperty {
+ eCSSEditableProperty_NONE = 0,
+ eCSSEditableProperty_background_color,
+ eCSSEditableProperty_background_image,
+ eCSSEditableProperty_border,
+ eCSSEditableProperty_caption_side,
+ eCSSEditableProperty_color,
+ eCSSEditableProperty_float,
+ eCSSEditableProperty_font_family,
+ eCSSEditableProperty_font_size,
+ eCSSEditableProperty_font_style,
+ eCSSEditableProperty_font_weight,
+ eCSSEditableProperty_height,
+ eCSSEditableProperty_list_style_type,
+ eCSSEditableProperty_margin_left,
+ eCSSEditableProperty_margin_right,
+ eCSSEditableProperty_text_align,
+ eCSSEditableProperty_text_decoration,
+ eCSSEditableProperty_vertical_align,
+ eCSSEditableProperty_whitespace,
+ eCSSEditableProperty_width
+ };
+
+ // Nb: keep these fields in an order that minimizes padding.
+ struct CSSEquivTable {
+ nsCSSEditableProperty cssProperty;
+ bool gettable;
+ bool caseSensitiveValue;
+ nsProcessValueFunc processValueFunctor;
+ const char* defaultValue;
+ const char* prependValue;
+ const char* appendValue;
+ };
+
+ /**
+ * Adds/remove a CSS declaration to the STYLE attribute carried by a given
+ * element.
+ *
+ * @param aHTMLEditor [IN] An HTMLEditor instance
+ * @param aStyledElement [IN] A DOM styled element.
+ * @param aProperty [IN] An atom containing the CSS property to set.
+ * @param aValue [IN] A string containing the value of the CSS
+ * property.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
+ SetCSSPropertyWithTransaction(HTMLEditor& aHTMLEditor,
+ nsStyledElement& aStyledElement,
+ nsAtom& aProperty, const nsAString& aValue) {
+ return SetCSSPropertyInternal(aHTMLEditor, aStyledElement, aProperty,
+ aValue, false);
+ }
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
+ SetCSSPropertyPixelsWithTransaction(HTMLEditor& aHTMLEditor,
+ nsStyledElement& aStyledElement,
+ nsAtom& aProperty, int32_t aIntValue);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
+ SetCSSPropertyPixelsWithoutTransaction(nsStyledElement& aStyledElement,
+ const nsAtom& aProperty,
+ int32_t aIntValue);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
+ RemoveCSSPropertyWithTransaction(HTMLEditor& aHTMLEditor,
+ nsStyledElement& aStyledElement,
+ nsAtom& aProperty,
+ const nsAString& aPropertyValue) {
+ return RemoveCSSPropertyInternal(aHTMLEditor, aStyledElement, aProperty,
+ aPropertyValue, false);
+ }
+
+ /**
+ * Gets the specified/computed style value of a CSS property for a given
+ * node (or its element ancestor if it is not an element).
+ *
+ * @param aContent [IN] A DOM node.
+ * @param aProperty [IN] An atom containing the CSS property to get.
+ * @param aPropertyValue [OUT] The retrieved value of the property.
+ */
+ static nsresult GetSpecifiedProperty(nsIContent& aContent,
+ nsAtom& aCSSProperty, nsAString& aValue);
+ MOZ_CAN_RUN_SCRIPT static nsresult GetComputedProperty(nsIContent& aContent,
+ nsAtom& aCSSProperty,
+ nsAString& aValue);
+
+ /**
+ * Removes a CSS property from the specified declarations in STYLE attribute
+ * and removes the node if it is an useless span.
+ *
+ * @param aHTMLEditor [IN] An HTMLEditor instance
+ * @param aStyledElement [IN] The styled element we want to remove a style
+ * from.
+ * @param aProperty [IN] The CSS property atom to remove.
+ * @param aPropertyValue [IN] The value of the property we have to remove
+ * if the property accepts more than one value.
+ * @return A candidate point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMPoint, nsresult>
+ RemoveCSSInlineStyleWithTransaction(HTMLEditor& aHTMLEditor,
+ nsStyledElement& aStyledElement,
+ nsAtom* aProperty,
+ const nsAString& aPropertyValue);
+
+ /**
+ * Get the default browser background color if we need it for
+ * GetCSSBackgroundColorState().
+ *
+ * @param aColor [OUT] The default color as it is defined in prefs.
+ */
+ static void GetDefaultBackgroundColor(nsAString& aColor);
+
+ /**
+ * Returns the list of values for the CSS equivalences to
+ * the passed HTML style for the passed node.
+ *
+ * @param aElement [IN] A DOM node.
+ * @param aStyle [IN] The style to get the values.
+ * @param aValueString [OUT] The list of CSS values.
+ */
+ MOZ_CAN_RUN_SCRIPT static nsresult GetComputedCSSEquivalentTo(
+ dom::Element& aElement, const EditorElementStyle& aStyle,
+ nsAString& aOutValue);
+
+ /**
+ * Does the node aNode (or his parent if it is not an element node) carries
+ * the CSS equivalent styles to the HTML style for this node ?
+ *
+ * @param aHTMLEditor [IN] An HTMLEditor instance
+ * @param aContent [IN] A DOM node.
+ * @param aStyle [IN] The style to check.
+ * @param aInOutValue [IN/OUT] Input value is used for initial value of the
+ * result, out value is the list of CSS values.
+ * @return A boolean being true if the css properties are
+ * not same as initial value.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<bool, nsresult>
+ IsComputedCSSEquivalentTo(const HTMLEditor& aHTMLEditor, nsIContent& aContent,
+ const EditorInlineStyle& aStyle,
+ nsAString& aInOutValue);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT_BOUNDARY static Result<bool, nsresult>
+ IsSpecifiedCSSEquivalentTo(const HTMLEditor& aHTMLEditor,
+ nsIContent& aContent,
+ const EditorInlineStyle& aStyle,
+ nsAString& aInOutValue);
+
+ /**
+ * This is a kind of Is*CSSEquivalentTo.
+ * Is*CSSEquivalentTo returns whether the properties aren't same as initial
+ * value. But this method returns whether the properties aren't set.
+ * If node is <span style="font-weight: normal"/>,
+ * - Is(Computed|Specified)CSSEquivalentTo returns false.
+ * - Have(Computed|Specified)CSSEquivalentStyles returns true.
+ *
+ * @param aHTMLEditor [IN] An HTMLEditor instance
+ * @param aContent [IN] A DOM node.
+ * @param aStyle [IN] The style to check.
+ * @return A boolean being true if the css properties are
+ * not set.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<bool, nsresult>
+ HaveComputedCSSEquivalentStyles(const HTMLEditor& aHTMLEditor,
+ nsIContent& aContent,
+ const EditorInlineStyle& aStyle);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT_BOUNDARY static Result<bool, nsresult>
+ HaveSpecifiedCSSEquivalentStyles(const HTMLEditor& aHTMLEditor,
+ nsIContent& aContent,
+ const EditorInlineStyle& aStyle);
+
+ /**
+ * Adds to the node the CSS inline styles equivalent to the HTML style
+ * and return the number of CSS properties set by the call.
+ *
+ * @param aHTMLEditor [IN} An HTMLEditor instance
+ * @param aNode [IN] A DOM node.
+ * @param aStyleToSet [IN] The style to set. Can be EditorInlineStyle.
+ * @param aValue [IN] The attribute or style value of aStyleToSet.
+ *
+ * @return The number of CSS properties set by the call.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<size_t, nsresult>
+ SetCSSEquivalentToStyle(WithTransaction aWithTransaction,
+ HTMLEditor& aHTMLEditor,
+ nsStyledElement& aStyledElement,
+ const EditorElementStyle& aStyleToSet,
+ const nsAString* aValue);
+
+ /**
+ * Removes from the node the CSS inline styles equivalent to the HTML style.
+ *
+ * @param aHTMLEditor [IN} An HTMLEditor instance
+ * @param aStyledElement [IN] A DOM Element (must not be null).
+ * @param aStyleToRemove [IN] The style to remove. Can be EditorInlineStyle.
+ * @param aAttribute [IN] An atom to an attribute name or nullptr if
+ * irrelevant.
+ * @param aValue [IN] The attribute value.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult RemoveCSSEquivalentToStyle(
+ WithTransaction aWithTransaction, HTMLEditor& aHTMLEditor,
+ nsStyledElement& aStyledElement, const EditorElementStyle& aStyleToRemove,
+ const nsAString* aValue);
+
+ /**
+ * Parses a "xxxx.xxxxxuuu" string where x is a digit and u an alpha char.
+ *
+ * @param aString [IN] Input string to parse.
+ * @param aValue [OUT] Numeric part.
+ * @param aUnit [OUT] Unit part.
+ */
+ static void ParseLength(const nsAString& aString, float* aValue,
+ nsAtom** aUnit);
+
+ /**
+ * DoStyledElementsHaveSameStyle compares two elements and checks if they have
+ * the same specified CSS declarations in the STYLE attribute. The answer is
+ * always false if at least one of them carries an ID or a class.
+ *
+ * @param aStyledElement [IN] A styled element.
+ * @param aOtherStyledElement [IN] The other styled element.
+ * @return true if the two elements are considered to
+ * have same styles.
+ */
+ static bool DoStyledElementsHaveSameStyle(
+ nsStyledElement& aStyledElement, nsStyledElement& aOtherStyledElement);
+
+ /**
+ * Gets the computed style for a given element. Can return null.
+ */
+ static already_AddRefed<nsComputedDOMStyle> GetComputedStyle(
+ dom::Element* aElement);
+
+ private:
+ enum class StyleType { Specified, Computed };
+
+ /**
+ * Retrieves the CSS property atom from an enum.
+ *
+ * @param aProperty The enum value for the property.
+ * @return The corresponding atom.
+ */
+ static nsStaticAtom* GetCSSPropertyAtom(nsCSSEditableProperty aProperty);
+
+ struct CSSDeclaration {
+ nsStaticAtom& mProperty;
+ nsString const mValue;
+ };
+
+ /**
+ * Retrieves the CSS declarations for aEquivTable.
+ *
+ * @param aEquivTable The equivalence table.
+ * @param aValue Optional. If specified, may return only
+ * matching declarations to this value (depends on
+ * the style, check how is aInputString of
+ * nsProcessValueFunc for the details).
+ * @param aHandlingFor What's the purpose of calling this.
+ * @param aOutCSSDeclarations [OUT] The array of CSS declarations.
+ */
+ enum class HandlingFor { GettingStyle, SettingStyle, RemovingStyle };
+ static void GetCSSDeclarations(const CSSEquivTable* aEquivTable,
+ const nsAString* aValue,
+ HandlingFor aHandlingFor,
+ nsTArray<CSSDeclaration>& aOutCSSDeclarations);
+
+ /**
+ * Retrieves the CSS declarations equivalent to the given aStyle/aValue on
+ * aElement.
+ *
+ * @param aElement The DOM node.
+ * @param aStyle The style to get equivelent CSS properties and
+ * values.
+ * @param aValue Optional. If specified, may return only
+ * matching declarations to this value (depends on
+ * the style, check how is aInputString of
+ * nsProcessValueFunc for the details).
+ * @param aHandlingFor What's the purpose of calling this.
+ * @param aOutCSSDeclarations [OUT] The array of CSS declarations.
+ */
+ static void GetCSSDeclarations(dom::Element& aElement,
+ const EditorElementStyle& aStyle,
+ const nsAString* aValue,
+ HandlingFor aHandlingFor,
+ nsTArray<CSSDeclaration>& aOutCSSDeclarations);
+
+ /**
+ * Back-end for GetSpecifiedProperty and GetComputedProperty.
+ *
+ * @param aNode [IN] A DOM node.
+ * @param aProperty [IN] A CSS property.
+ * @param aValue [OUT] The retrieved value for this property.
+ */
+ MOZ_CAN_RUN_SCRIPT static nsresult GetComputedCSSInlinePropertyBase(
+ nsIContent& aContent, nsAtom& aCSSProperty, nsAString& aValue);
+ static nsresult GetSpecifiedCSSInlinePropertyBase(nsIContent& aContent,
+ nsAtom& aCSSProperty,
+ nsAString& aValue);
+
+ /**
+ * Those methods are wrapped with corresponding methods which do not have
+ * "Internal" in their names. Don't use these methods directly even if
+ * you want to use one of them in this class.
+ * Note that these methods may run scrip only when StyleType is Computed.
+ */
+ MOZ_CAN_RUN_SCRIPT static nsresult GetCSSEquivalentTo(
+ dom::Element& aElement, const EditorElementStyle& aStyle,
+ nsAString& aOutValue, StyleType aStyleType);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<bool, nsresult>
+ IsCSSEquivalentTo(const HTMLEditor& aHTMLEditor, nsIContent& aContent,
+ const EditorInlineStyle& aStyle, nsAString& aInOutValue,
+ StyleType aStyleType);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<bool, nsresult>
+ HaveCSSEquivalentStyles(const HTMLEditor& aHTMLEditor, nsIContent& aContent,
+ const EditorInlineStyle& aStyle,
+ StyleType aStyleType);
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult RemoveCSSPropertyInternal(
+ HTMLEditor& aHTMLEditor, nsStyledElement& aStyledElement,
+ nsAtom& aProperty, const nsAString& aPropertyValue,
+ bool aSuppressTxn = false);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult SetCSSPropertyInternal(
+ HTMLEditor& aHTMLEditor, nsStyledElement& aStyledElement,
+ nsAtom& aProperty, const nsAString& aValue, bool aSuppressTxn = false);
+
+ /**
+ * Answers true if the given aStyle on aElement or an element whose name is
+ * aTagName has a CSS equivalence in this implementation.
+ *
+ * @param aTagName [IN] Tag name of an element.
+ * @param aElement [IN] An element.
+ * @param aStyle [IN] The style you want to check.
+ * @return A boolean saying if the tag/attribute has a CSS
+ * equiv.
+ */
+ [[nodiscard]] static bool IsCSSEditableStyle(
+ const nsAtom& aTagName, const EditorElementStyle& aStyle);
+ [[nodiscard]] static bool IsCSSEditableStyle(
+ const dom::Element& aElement, const EditorElementStyle& aStyle);
+
+ friend class EditorElementStyle; // for IsCSSEditableStyle
+};
+
+#define NS_EDITOR_INDENT_INCREMENT_IN 0.4134f
+#define NS_EDITOR_INDENT_INCREMENT_CM 1.05f
+#define NS_EDITOR_INDENT_INCREMENT_MM 10.5f
+#define NS_EDITOR_INDENT_INCREMENT_PT 29.76f
+#define NS_EDITOR_INDENT_INCREMENT_PC 2.48f
+#define NS_EDITOR_INDENT_INCREMENT_EM 3
+#define NS_EDITOR_INDENT_INCREMENT_EX 6
+#define NS_EDITOR_INDENT_INCREMENT_PX 40
+#define NS_EDITOR_INDENT_INCREMENT_PERCENT 4
+
+} // namespace mozilla
+
+#endif // #ifndef CSSEditUtils_h
diff --git a/editor/libeditor/ChangeAttributeTransaction.cpp b/editor/libeditor/ChangeAttributeTransaction.cpp
new file mode 100644
index 0000000000..5c1e2eecd1
--- /dev/null
+++ b/editor/libeditor/ChangeAttributeTransaction.cpp
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChangeAttributeTransaction.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/ToString.h"
+#include "mozilla/dom/Element.h" // for Element
+
+#include "nsAString.h"
+#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc.
+
+namespace mozilla {
+
+using namespace dom;
+
+// static
+already_AddRefed<ChangeAttributeTransaction> ChangeAttributeTransaction::Create(
+ Element& aElement, nsAtom& aAttribute, const nsAString& aValue) {
+ RefPtr<ChangeAttributeTransaction> transaction =
+ new ChangeAttributeTransaction(aElement, aAttribute, &aValue);
+ return transaction.forget();
+}
+
+// static
+already_AddRefed<ChangeAttributeTransaction>
+ChangeAttributeTransaction::CreateToRemove(Element& aElement,
+ nsAtom& aAttribute) {
+ RefPtr<ChangeAttributeTransaction> transaction =
+ new ChangeAttributeTransaction(aElement, aAttribute, nullptr);
+ return transaction.forget();
+}
+
+ChangeAttributeTransaction::ChangeAttributeTransaction(Element& aElement,
+ nsAtom& aAttribute,
+ const nsAString* aValue)
+ : EditTransactionBase(),
+ mElement(&aElement),
+ mAttribute(&aAttribute),
+ mValue(aValue ? *aValue : u""_ns),
+ mRemoveAttribute(!aValue),
+ mAttributeWasSet(false) {}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const ChangeAttributeTransaction& aTransaction) {
+ aStream << "{ mElement=" << aTransaction.mElement.get();
+ if (aTransaction.mElement) {
+ aStream << " (" << *aTransaction.mElement << ")";
+ }
+ aStream << ", mAttribute=" << nsAtomCString(aTransaction.mAttribute).get()
+ << ", mValue=\"" << NS_ConvertUTF16toUTF8(aTransaction.mValue).get()
+ << "\", mUndoValue=\""
+ << NS_ConvertUTF16toUTF8(aTransaction.mUndoValue).get()
+ << "\", mRemoveAttribute="
+ << (aTransaction.mRemoveAttribute ? "true" : "false")
+ << ", mAttributeWasSet="
+ << (aTransaction.mAttributeWasSet ? "true" : "false") << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ChangeAttributeTransaction,
+ EditTransactionBase, mElement)
+
+NS_IMPL_ADDREF_INHERITED(ChangeAttributeTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(ChangeAttributeTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ChangeAttributeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP ChangeAttributeTransaction::DoTransaction() {
+ // Need to get the current value of the attribute and save it, and set
+ // mAttributeWasSet
+ mAttributeWasSet = mElement->GetAttr(mAttribute, mUndoValue);
+
+ // XXX: hack until attribute-was-set code is implemented
+ if (!mUndoValue.IsEmpty()) {
+ mAttributeWasSet = true;
+ }
+ // XXX: end hack
+
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p ChangeAttributeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ // Now set the attribute to the new value
+ if (mRemoveAttribute) {
+ OwningNonNull<Element> element = *mElement;
+ nsresult rv = element->UnsetAttr(kNameSpaceID_None, mAttribute, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::UnsetAttr() failed");
+ return rv;
+ }
+
+ OwningNonNull<Element> element = *mElement;
+ nsresult rv = element->SetAttr(kNameSpaceID_None, mAttribute, mValue, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr() failed");
+ return rv;
+}
+
+NS_IMETHODIMP ChangeAttributeTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p ChangeAttributeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mElement)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ if (mAttributeWasSet) {
+ OwningNonNull<Element> element = *mElement;
+ nsresult rv =
+ element->SetAttr(kNameSpaceID_None, mAttribute, mUndoValue, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr() failed");
+ return rv;
+ }
+ OwningNonNull<Element> element = *mElement;
+ nsresult rv = element->UnsetAttr(kNameSpaceID_None, mAttribute, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::UnsetAttr() failed");
+ return rv;
+}
+
+NS_IMETHODIMP ChangeAttributeTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p ChangeAttributeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mElement)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ if (mRemoveAttribute) {
+ OwningNonNull<Element> element = *mElement;
+ nsresult rv = element->UnsetAttr(kNameSpaceID_None, mAttribute, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::UnsetAttr() failed");
+ return rv;
+ }
+
+ OwningNonNull<Element> element = *mElement;
+ nsresult rv = element->SetAttr(kNameSpaceID_None, mAttribute, mValue, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr() failed");
+ return rv;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/ChangeAttributeTransaction.h b/editor/libeditor/ChangeAttributeTransaction.h
new file mode 100644
index 0000000000..b21a64f221
--- /dev/null
+++ b/editor/libeditor/ChangeAttributeTransaction.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChangeAttributeTransaction_h
+#define ChangeAttributeTransaction_h
+
+#include "EditTransactionBase.h" // base class
+
+#include "mozilla/Attributes.h" // override
+#include "nsCOMPtr.h" // nsCOMPtr members
+#include "nsCycleCollectionParticipant.h" // NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED
+#include "nsISupportsImpl.h" // NS_DECL_ISUPPORTS_INHERITED
+#include "nsString.h" // nsString members
+
+class nsAtom;
+
+namespace mozilla {
+namespace dom {
+class Element;
+} // namespace dom
+
+/**
+ * A transaction that changes an attribute of a content node. This transaction
+ * covers add, remove, and change attribute.
+ */
+class ChangeAttributeTransaction final : public EditTransactionBase {
+ protected:
+ ChangeAttributeTransaction(dom::Element& aElement, nsAtom& aAttribute,
+ const nsAString* aValue);
+
+ public:
+ /**
+ * Creates a change attribute transaction to set an attribute to something.
+ * This method never returns nullptr.
+ *
+ * @param aElement The element whose attribute will be changed.
+ * @param aAttribute The name of the attribute to change.
+ * @param aValue The new value for aAttribute.
+ */
+ static already_AddRefed<ChangeAttributeTransaction> Create(
+ dom::Element& aElement, nsAtom& aAttribute, const nsAString& aValue);
+
+ /**
+ * Creates a change attribute transaction to remove an attribute. This
+ * method never returns nullptr.
+ *
+ * @param aElement The element whose attribute will be changed.
+ * @param aAttribute The name of the attribute to remove.
+ */
+ static already_AddRefed<ChangeAttributeTransaction> CreateToRemove(
+ dom::Element& aElement, nsAtom& aAttribute);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ChangeAttributeTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(ChangeAttributeTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+
+ friend std::ostream& operator<<(
+ std::ostream& aStream, const ChangeAttributeTransaction& aTransaction);
+
+ private:
+ virtual ~ChangeAttributeTransaction() = default;
+
+ // The element to operate upon
+ nsCOMPtr<dom::Element> mElement;
+
+ // The attribute to change
+ RefPtr<nsAtom> mAttribute;
+
+ // The value to set the attribute to (ignored if mRemoveAttribute==true)
+ nsString mValue;
+
+ // The value to set the attribute to for undo
+ nsString mUndoValue;
+
+ // True if the operation is to remove mAttribute from mElement
+ bool mRemoveAttribute;
+
+ // True if the mAttribute was set on mElement at the time of execution
+ bool mAttributeWasSet;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef ChangeAttributeTransaction_h
diff --git a/editor/libeditor/ChangeStyleTransaction.cpp b/editor/libeditor/ChangeStyleTransaction.cpp
new file mode 100644
index 0000000000..edd6ac29d7
--- /dev/null
+++ b/editor/libeditor/ChangeStyleTransaction.cpp
@@ -0,0 +1,333 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChangeStyleTransaction.h"
+
+#include "HTMLEditUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ToString.h"
+#include "mozilla/dom/Element.h" // for Element
+#include "nsAString.h" // for nsAString::Append, etc.
+#include "nsCRT.h" // for nsCRT::IsAsciiSpace
+#include "nsDebug.h" // for NS_WARNING, etc.
+#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc.
+#include "nsGkAtoms.h" // for nsGkAtoms, etc.
+#include "nsICSSDeclaration.h" // for nsICSSDeclaration.
+#include "nsLiteralString.h" // for NS_LITERAL_STRING, etc.
+#include "nsReadableUtils.h" // for ToNewUnicode
+#include "nsString.h" // for nsAutoString, nsString, etc.
+#include "nsStyledElement.h" // for nsStyledElement.
+#include "nsUnicharUtils.h" // for nsCaseInsensitiveStringComparator
+
+namespace mozilla {
+
+using namespace dom;
+
+// static
+already_AddRefed<ChangeStyleTransaction> ChangeStyleTransaction::Create(
+ nsStyledElement& aStyledElement, nsAtom& aProperty,
+ const nsAString& aValue) {
+ RefPtr<ChangeStyleTransaction> transaction =
+ new ChangeStyleTransaction(aStyledElement, aProperty, aValue, false);
+ return transaction.forget();
+}
+
+// static
+already_AddRefed<ChangeStyleTransaction> ChangeStyleTransaction::CreateToRemove(
+ nsStyledElement& aStyledElement, nsAtom& aProperty,
+ const nsAString& aValue) {
+ RefPtr<ChangeStyleTransaction> transaction =
+ new ChangeStyleTransaction(aStyledElement, aProperty, aValue, true);
+ return transaction.forget();
+}
+
+ChangeStyleTransaction::ChangeStyleTransaction(nsStyledElement& aStyledElement,
+ nsAtom& aProperty,
+ const nsAString& aValue,
+ bool aRemove)
+ : EditTransactionBase(),
+ mStyledElement(&aStyledElement),
+ mProperty(&aProperty),
+ mRemoveProperty(aRemove),
+ mUndoAttributeWasSet(false),
+ mRedoAttributeWasSet(false) {
+ CopyUTF16toUTF8(aValue, mValue);
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const ChangeStyleTransaction& aTransaction) {
+ aStream << "{ mStyledElement=" << aTransaction.mStyledElement.get();
+ if (aTransaction.mStyledElement) {
+ aStream << " (" << *aTransaction.mStyledElement << ")";
+ }
+ aStream << ", mProperty=" << nsAtomCString(aTransaction.mProperty).get()
+ << ", mValue=\"" << aTransaction.mValue.get() << "\", mUndoValue=\""
+ << aTransaction.mUndoValue.get()
+ << "\", mRedoValue=" << aTransaction.mRedoValue.get()
+ << ", mRemoveProperty="
+ << (aTransaction.mRemoveProperty ? "true" : "false")
+ << ", mUndoAttributeWasSet="
+ << (aTransaction.mUndoAttributeWasSet ? "true" : "false")
+ << ", mRedoAttributeWasSet="
+ << (aTransaction.mRedoAttributeWasSet ? "true" : "false") << " }";
+ return aStream;
+}
+
+#define kNullCh ('\0')
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ChangeStyleTransaction, EditTransactionBase,
+ mStyledElement)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ChangeStyleTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMPL_ADDREF_INHERITED(ChangeStyleTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(ChangeStyleTransaction, EditTransactionBase)
+
+// Answers true if aValue is in the string list of white-space separated values
+// aValueList.
+bool ChangeStyleTransaction::ValueIncludes(const nsACString& aValueList,
+ const nsACString& aValue) {
+ nsAutoCString valueList(aValueList);
+ bool result = false;
+
+ // put an extra null at the end
+ valueList.Append(kNullCh);
+
+ char* start = valueList.BeginWriting();
+ char* end = start;
+
+ while (kNullCh != *start) {
+ while (kNullCh != *start && nsCRT::IsAsciiSpace(*start)) {
+ // skip leading space
+ start++;
+ }
+ end = start;
+
+ while (kNullCh != *end && !nsCRT::IsAsciiSpace(*end)) {
+ // look for space or end
+ end++;
+ }
+ // end string here
+ *end = kNullCh;
+
+ if (start < end) {
+ if (aValue.Equals(nsDependentCString(start),
+ nsCaseInsensitiveCStringComparator)) {
+ result = true;
+ break;
+ }
+ }
+ start = ++end;
+ }
+ return result;
+}
+
+NS_IMETHODIMP ChangeStyleTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p ChangeStyleTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mStyledElement)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ OwningNonNull<nsStyledElement> styledElement = *mStyledElement;
+ nsCOMPtr<nsICSSDeclaration> cssDecl = styledElement->Style();
+
+ // FIXME(bug 1606994): Using atoms forces a string copy here which is not
+ // great.
+ nsAutoCString propertyNameString;
+ mProperty->ToUTF8String(propertyNameString);
+
+ mUndoAttributeWasSet = mStyledElement->HasAttr(nsGkAtoms::style);
+
+ nsAutoCString values;
+ cssDecl->GetPropertyValue(propertyNameString, values);
+ mUndoValue.Assign(values);
+
+ if (mRemoveProperty) {
+ nsAutoCString returnString;
+ if (mProperty == nsGkAtoms::text_decoration) {
+ BuildTextDecorationValueToRemove(values, mValue, values);
+ if (values.IsEmpty()) {
+ ErrorResult error;
+ cssDecl->RemoveProperty(propertyNameString, returnString, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("nsICSSDeclaration::RemoveProperty() failed");
+ return error.StealNSResult();
+ }
+ } else {
+ ErrorResult error;
+ nsAutoCString priority;
+ cssDecl->GetPropertyPriority(propertyNameString, priority);
+ cssDecl->SetProperty(propertyNameString, values, priority, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("nsICSSDeclaration::SetProperty() failed");
+ return error.StealNSResult();
+ }
+ }
+ } else {
+ ErrorResult error;
+ cssDecl->RemoveProperty(propertyNameString, returnString, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("nsICSSDeclaration::RemoveProperty() failed");
+ return error.StealNSResult();
+ }
+ }
+ } else {
+ nsAutoCString priority;
+ cssDecl->GetPropertyPriority(propertyNameString, priority);
+ if (mProperty == nsGkAtoms::text_decoration) {
+ BuildTextDecorationValueToSet(values, mValue, values);
+ } else {
+ values.Assign(mValue);
+ }
+ ErrorResult error;
+ cssDecl->SetProperty(propertyNameString, values, priority, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("nsICSSDeclaration::SetProperty() failed");
+ return error.StealNSResult();
+ }
+ }
+
+ // Let's be sure we don't keep an empty style attribute
+ uint32_t length = cssDecl->Length();
+ if (!length) {
+ nsresult rv =
+ styledElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::style, true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Element::UnsetAttr(nsGkAtoms::style) failed");
+ return rv;
+ }
+ } else {
+ mRedoAttributeWasSet = true;
+ }
+
+ cssDecl->GetPropertyValue(propertyNameString, mRedoValue);
+ return NS_OK;
+}
+
+nsresult ChangeStyleTransaction::SetStyle(bool aAttributeWasSet,
+ nsACString& aValue) {
+ if (NS_WARN_IF(!mStyledElement)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (aAttributeWasSet) {
+ OwningNonNull<nsStyledElement> styledElement = *mStyledElement;
+
+ // The style attribute was not empty, let's recreate the declaration
+ nsAutoCString propertyNameString;
+ mProperty->ToUTF8String(propertyNameString);
+
+ nsCOMPtr<nsICSSDeclaration> cssDecl = styledElement->Style();
+
+ ErrorResult error;
+ if (aValue.IsEmpty()) {
+ // An empty value means we have to remove the property
+ nsAutoCString returnString;
+ cssDecl->RemoveProperty(propertyNameString, returnString, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("nsICSSDeclaration::RemoveProperty() failed");
+ return error.StealNSResult();
+ }
+ }
+ // Let's recreate the declaration as it was
+ nsAutoCString priority;
+ cssDecl->GetPropertyPriority(propertyNameString, priority);
+ cssDecl->SetProperty(propertyNameString, aValue, priority, error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "nsICSSDeclaration::SetProperty() failed");
+ return error.StealNSResult();
+ }
+
+ OwningNonNull<nsStyledElement> styledElement = *mStyledElement;
+ nsresult rv =
+ styledElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::style, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Element::UnsetAttr(nsGkAtoms::style) failed");
+ return rv;
+}
+
+NS_IMETHODIMP ChangeStyleTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p ChangeStyleTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ nsresult rv = SetStyle(mUndoAttributeWasSet, mUndoValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "ChangeStyleTransaction::SetStyle() failed");
+ return rv;
+}
+
+NS_IMETHODIMP ChangeStyleTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p ChangeStyleTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ nsresult rv = SetStyle(mRedoAttributeWasSet, mRedoValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "ChangeStyleTransaction::SetStyle() failed");
+ return rv;
+}
+
+// static
+void ChangeStyleTransaction::BuildTextDecorationValueToSet(
+ const nsACString& aCurrentValues, const nsACString& aAddingValues,
+ nsACString& aOutValues) {
+ const bool underline = ValueIncludes(aCurrentValues, "underline"_ns) ||
+ ValueIncludes(aAddingValues, "underline"_ns);
+ const bool overline = ValueIncludes(aCurrentValues, "overline"_ns) ||
+ ValueIncludes(aAddingValues, "overline"_ns);
+ const bool lineThrough = ValueIncludes(aCurrentValues, "line-through"_ns) ||
+ ValueIncludes(aAddingValues, "line-through"_ns);
+ // FYI: Don't refer aCurrentValues which may refer same instance as
+ // aOutValues.
+ BuildTextDecorationValue(underline, overline, lineThrough, aOutValues);
+}
+
+// static
+void ChangeStyleTransaction::BuildTextDecorationValueToRemove(
+ const nsACString& aCurrentValues, const nsACString& aRemovingValues,
+ nsACString& aOutValues) {
+ const bool underline = ValueIncludes(aCurrentValues, "underline"_ns) &&
+ !ValueIncludes(aRemovingValues, "underline"_ns);
+ const bool overline = ValueIncludes(aCurrentValues, "overline"_ns) &&
+ !ValueIncludes(aRemovingValues, "overline"_ns);
+ const bool lineThrough = ValueIncludes(aCurrentValues, "line-through"_ns) &&
+ !ValueIncludes(aRemovingValues, "line-through"_ns);
+ // FYI: Don't refer aCurrentValues which may refer same instance as
+ // aOutValues.
+ BuildTextDecorationValue(underline, overline, lineThrough, aOutValues);
+}
+
+void ChangeStyleTransaction::BuildTextDecorationValue(bool aUnderline,
+ bool aOverline,
+ bool aLineThrough,
+ nsACString& aOutValues) {
+ // We should build text-decoration(-line) value as same as Blink for
+ // compatibility. Blink sets text-decoration-line to the values in the
+ // following order. Blink drops `blink` and other styles like color and
+ // style. For keeping the code simple, let's use the lossy behavior.
+ aOutValues.Truncate();
+ if (aUnderline) {
+ aOutValues.AssignLiteral("underline");
+ }
+ if (aOverline) {
+ if (!aOutValues.IsEmpty()) {
+ aOutValues.Append(HTMLEditUtils::kSpace);
+ }
+ aOutValues.AppendLiteral("overline");
+ }
+ if (aLineThrough) {
+ if (!aOutValues.IsEmpty()) {
+ aOutValues.Append(HTMLEditUtils::kSpace);
+ }
+ aOutValues.AppendLiteral("line-through");
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/ChangeStyleTransaction.h b/editor/libeditor/ChangeStyleTransaction.h
new file mode 100644
index 0000000000..359cf3f255
--- /dev/null
+++ b/editor/libeditor/ChangeStyleTransaction.h
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChangeStyleTransaction_h
+#define ChangeStyleTransaction_h
+
+#include "EditTransactionBase.h" // base class
+
+#include "nsCOMPtr.h" // nsCOMPtr members
+#include "nsCycleCollectionParticipant.h" // various macros
+#include "nsString.h" // nsString members
+
+class nsAtom;
+class nsStyledElement;
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+} // namespace dom
+
+/**
+ * A transaction that changes the value of a CSS inline style of a content
+ * node. This transaction covers add, remove, and change a property's value.
+ */
+class ChangeStyleTransaction final : public EditTransactionBase {
+ protected:
+ ChangeStyleTransaction(nsStyledElement& aStyledElement, nsAtom& aProperty,
+ const nsAString& aValue, bool aRemove);
+
+ public:
+ /**
+ * Creates a change style transaction. This never returns nullptr.
+ *
+ * @param aStyledElement The node whose style attribute will be changed.
+ * @param aProperty The name of the property to change.
+ * @param aValue New value for aProperty.
+ */
+ static already_AddRefed<ChangeStyleTransaction> Create(
+ nsStyledElement& aStyledElement, nsAtom& aProperty,
+ const nsAString& aValue);
+
+ /**
+ * Creates a change style transaction. This never returns nullptr.
+ *
+ * @param aStyledElement The node whose style attribute will be changed.
+ * @param aProperty The name of the property to change.
+ * @param aValue The value to remove from aProperty.
+ */
+ static already_AddRefed<ChangeStyleTransaction> CreateToRemove(
+ nsStyledElement& aStyledElement, nsAtom& aProperty,
+ const nsAString& aValue);
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ChangeStyleTransaction,
+ EditTransactionBase)
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(ChangeStyleTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+
+ /**
+ * Returns true if the list of white-space separated values contains aValue
+ *
+ * @param aValueList [IN] a list of white-space separated values
+ * @param aValue [IN] the value to look for in the list
+ * @return true if the value is in the list of values
+ */
+ static bool ValueIncludes(const nsACString& aValueList,
+ const nsACString& aValue);
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const ChangeStyleTransaction& aTransaction);
+
+ private:
+ virtual ~ChangeStyleTransaction() = default;
+
+ /**
+ * Build new text-decoration value to set/remove specific values to/from the
+ * rule which already has aCurrentValues.
+ */
+ void BuildTextDecorationValueToSet(const nsACString& aCurrentValues,
+ const nsACString& aAddingValues,
+ nsACString& aOutValues);
+ void BuildTextDecorationValueToRemove(const nsACString& aCurrentValues,
+ const nsACString& aRemovingValues,
+ nsACString& aOutValues);
+
+ /**
+ * Helper method for above methods.
+ */
+ void BuildTextDecorationValue(bool aUnderline, bool aOverline,
+ bool aLineThrough, nsACString& aOutValues);
+
+ /**
+ * If the boolean is true and if the value is not the empty string,
+ * set the property in the transaction to that value; if the value
+ * is empty, remove the property from element's styles. If the boolean
+ * is false, just remove the style attribute.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetStyle(bool aAttributeWasSet,
+ nsACString& aValue);
+
+ // The element to operate upon.
+ RefPtr<nsStyledElement> mStyledElement;
+
+ // The CSS property to change.
+ RefPtr<nsAtom> mProperty;
+
+ // The value to set the property to (ignored if mRemoveProperty==true).
+ nsCString mValue;
+
+ // The value to set the property to for undo.
+ nsCString mUndoValue;
+ // The value to set the property to for redo.
+ nsCString mRedoValue;
+
+ // true if the operation is to remove mProperty from mElement.
+ bool mRemoveProperty;
+
+ // True if the style attribute was present and not empty before DoTransaction.
+ bool mUndoAttributeWasSet;
+ // True if the style attribute is present and not empty after DoTransaction.
+ bool mRedoAttributeWasSet;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef ChangeStyleTransaction_h
diff --git a/editor/libeditor/CompositionTransaction.cpp b/editor/libeditor/CompositionTransaction.cpp
new file mode 100644
index 0000000000..81df042558
--- /dev/null
+++ b/editor/libeditor/CompositionTransaction.cpp
@@ -0,0 +1,435 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositionTransaction.h"
+
+#include "mozilla/EditorBase.h" // mEditorBase
+#include "mozilla/Logging.h"
+#include "mozilla/SelectionState.h" // RangeUpdater
+#include "mozilla/TextComposition.h" // TextComposition
+#include "mozilla/ToString.h"
+#include "mozilla/dom/Selection.h" // local var
+#include "mozilla/dom/Text.h" // mTextNode
+#include "nsAString.h" // params
+#include "nsDebug.h" // for NS_ASSERTION, etc
+#include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc
+#include "nsRange.h" // local var
+#include "nsISelectionController.h" // for nsISelectionController constants
+#include "nsQueryObject.h" // for do_QueryObject
+
+namespace mozilla {
+
+using namespace dom;
+
+// static
+already_AddRefed<CompositionTransaction> CompositionTransaction::Create(
+ EditorBase& aEditorBase, const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert) {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ TextComposition* composition = aEditorBase.GetComposition();
+ MOZ_RELEASE_ASSERT(composition);
+ // XXX Actually, we get different text node and offset from editor in some
+ // cases. If composition stores text node, we should use it and offset
+ // in it.
+ EditorDOMPointInText pointToInsert;
+ if (Text* textNode = composition->GetContainerTextNode()) {
+ pointToInsert.Set(textNode, composition->XPOffsetInTextNode());
+ NS_WARNING_ASSERTION(
+ pointToInsert.GetContainerAs<Text>() ==
+ composition->GetContainerTextNode(),
+ "The editor tries to insert composition string into different node");
+ NS_WARNING_ASSERTION(
+ pointToInsert.Offset() == composition->XPOffsetInTextNode(),
+ "The editor tries to insert composition string into different offset");
+ } else {
+ pointToInsert = aPointToInsert;
+ }
+ RefPtr<CompositionTransaction> transaction =
+ new CompositionTransaction(aEditorBase, aStringToInsert, pointToInsert);
+ return transaction.forget();
+}
+
+CompositionTransaction::CompositionTransaction(
+ EditorBase& aEditorBase, const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert)
+ : mTextNode(aPointToInsert.ContainerAs<Text>()),
+ mOffset(aPointToInsert.Offset()),
+ mReplaceLength(aEditorBase.GetComposition()->XPLengthInTextNode()),
+ mRanges(aEditorBase.GetComposition()->GetRanges()),
+ mStringToInsert(aStringToInsert),
+ mEditorBase(&aEditorBase),
+ mFixed(false) {
+ MOZ_ASSERT(mTextNode->TextLength() >= mOffset);
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const CompositionTransaction& aTransaction) {
+ aStream << "{ mTextNode=" << aTransaction.mTextNode.get();
+ if (aTransaction.mTextNode) {
+ aStream << " (" << *aTransaction.mTextNode << ")";
+ }
+ aStream << ", mOffset=" << aTransaction.mOffset
+ << ", mReplaceLength=" << aTransaction.mReplaceLength
+ << ", mRanges={ Length()=" << aTransaction.mRanges->Length() << " }"
+ << ", mStringToInsert=\""
+ << NS_ConvertUTF16toUTF8(aTransaction.mStringToInsert).get() << "\""
+ << ", mEditorBase=" << aTransaction.mEditorBase.get() << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction, EditTransactionBase,
+ mEditorBase, mTextNode)
+// mRangeList can't lead to cycles
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+NS_IMPL_ADDREF_INHERITED(CompositionTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(CompositionTransaction, EditTransactionBase)
+
+NS_IMETHODIMP CompositionTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Fail before making any changes if there's no selection controller
+ if (NS_WARN_IF(!mEditorBase->GetSelectionController())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<Text> textNode = *mTextNode;
+
+ // Advance caret: This requires the presentation shell to get the selection.
+ if (mReplaceLength == 0) {
+ ErrorResult error;
+ editorBase->DoInsertText(textNode, mOffset, mStringToInsert, error);
+ if (error.Failed()) {
+ NS_WARNING("EditorBase::DoInsertText() failed");
+ return error.StealNSResult();
+ }
+ editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
+ mStringToInsert.Length());
+ } else {
+ // If composition string is split to multiple text nodes, we should put
+ // whole new composition string to the first text node and remove the
+ // compostion string in other nodes.
+ // TODO: This should be handled by `TextComposition` because this assumes
+ // that composition string has never touched by JS. However, it
+ // would occur if the web app is a corrabolation software which
+ // multiple users can modify anyware in an editor.
+ // TODO: And if composition starts from a following text node, the offset
+ // here is outdated and it will cause inserting composition string
+ // **before** the proper point from point of view of the users.
+ uint32_t replaceableLength = textNode->TextLength() - mOffset;
+ ErrorResult error;
+ editorBase->DoReplaceText(textNode, mOffset, mReplaceLength,
+ mStringToInsert, error);
+ if (error.Failed()) {
+ NS_WARNING("EditorBase::DoReplaceText() failed");
+ return error.StealNSResult();
+ }
+
+ // Don't use RangeUpdaterRef().SelAdjReplaceText() here because undoing
+ // this transaction will remove whole composition string. Therefore,
+ // selection should be restored at start of composition string.
+ // XXX Perhaps, this is a bug of our selection managemnt at undoing.
+ editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, mOffset,
+ replaceableLength);
+ // But some ranges which after the composition string should be restored
+ // as-is.
+ editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
+ mStringToInsert.Length());
+
+ if (replaceableLength < mReplaceLength) {
+ // XXX Perhaps, scanning following sibling text nodes with composition
+ // string length which we know is wrong because there may be
+ // non-empty text nodes which are inserted by JS. Instead, we
+ // should remove all text in the ranges of IME selections.
+ uint32_t remainLength = mReplaceLength - replaceableLength;
+ IgnoredErrorResult ignoredError;
+ for (nsIContent* nextSibling = textNode->GetNextSibling();
+ nextSibling && nextSibling->IsText() && remainLength;
+ nextSibling = nextSibling->GetNextSibling()) {
+ OwningNonNull<Text> followingTextNode =
+ *static_cast<Text*>(nextSibling);
+ uint32_t textLength = followingTextNode->TextLength();
+ editorBase->DoDeleteText(followingTextNode, 0, remainLength,
+ ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "EditorBase::DoDeleteText() failed, but ignored");
+ ignoredError.SuppressException();
+ // XXX Needs to check whether the text is deleted as expected.
+ editorBase->RangeUpdaterRef().SelAdjDeleteText(followingTextNode, 0,
+ remainLength);
+ remainLength -= textLength;
+ }
+ }
+ }
+
+ nsresult rv = SetSelectionForRanges();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CompositionTransaction::SetSelectionForRanges() failed");
+
+ if (TextComposition* composition = editorBase->GetComposition()) {
+ composition->OnUpdateCompositionInEditor(mStringToInsert, textNode,
+ mOffset);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP CompositionTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<Text> textNode = *mTextNode;
+ IgnoredErrorResult error;
+ editorBase->DoDeleteText(textNode, mOffset, mStringToInsert.Length(), error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("EditorBase::DoDeleteText() failed");
+ return error.StealNSResult();
+ }
+
+ // set the selection to the insertion point where the string was removed
+ editorBase->CollapseSelectionTo(EditorRawDOMPoint(textNode, mOffset), error);
+ NS_ASSERTION(!error.Failed(), "EditorBase::CollapseSelectionTo() failed");
+ return error.StealNSResult();
+}
+
+NS_IMETHODIMP CompositionTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+ return DoTransaction();
+}
+
+NS_IMETHODIMP CompositionTransaction::Merge(nsITransaction* aOtherTransaction,
+ bool* aDidMerge) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p CompositionTransaction::%s(aOtherTransaction=%p) this=%s", this,
+ __FUNCTION__, aOtherTransaction, ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!aOtherTransaction) || NS_WARN_IF(!aDidMerge)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aDidMerge = false;
+
+ // Check to make sure we aren't fixed, if we are then nothing gets merged.
+ if (mFixed) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p CompositionTransaction::%s returned false due to fixed", this,
+ __FUNCTION__));
+ return NS_OK;
+ }
+
+ RefPtr<EditTransactionBase> otherTransactionBase =
+ aOtherTransaction->GetAsEditTransactionBase();
+ if (!otherTransactionBase) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p CompositionTransaction::%s returned false due to not edit "
+ "transaction",
+ this, __FUNCTION__));
+ return NS_OK;
+ }
+
+ // If aTransaction is another CompositionTransaction then merge it
+ CompositionTransaction* otherCompositionTransaction =
+ otherTransactionBase->GetAsCompositionTransaction();
+ if (!otherCompositionTransaction) {
+ return NS_OK;
+ }
+
+ // We merge the next IME transaction by adopting its insert string.
+ mStringToInsert = otherCompositionTransaction->mStringToInsert;
+ mRanges = otherCompositionTransaction->mRanges;
+ *aDidMerge = true;
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p CompositionTransaction::%s returned true", this, __FUNCTION__));
+ return NS_OK;
+}
+
+void CompositionTransaction::MarkFixed() { mFixed = true; }
+
+/* ============ private methods ================== */
+
+nsresult CompositionTransaction::SetSelectionForRanges() {
+ if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<Text> textNode = *mTextNode;
+ RefPtr<TextRangeArray> ranges = mRanges;
+ nsresult rv = SetIMESelection(editorBase, textNode, mOffset,
+ mStringToInsert.Length(), ranges);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CompositionTransaction::SetIMESelection() failed");
+ return rv;
+}
+
+// static
+nsresult CompositionTransaction::SetIMESelection(
+ EditorBase& aEditorBase, Text* aTextNode, uint32_t aOffsetInNode,
+ uint32_t aLengthOfCompositionString, const TextRangeArray* aRanges) {
+ RefPtr<Selection> selection = aEditorBase.GetSelection();
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ SelectionBatcher selectionBatcher(selection, __FUNCTION__);
+
+ // First, remove all selections of IME composition.
+ static const RawSelectionType kIMESelections[] = {
+ nsISelectionController::SELECTION_IME_RAWINPUT,
+ nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT,
+ nsISelectionController::SELECTION_IME_CONVERTEDTEXT,
+ nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT};
+
+ nsCOMPtr<nsISelectionController> selectionController =
+ aEditorBase.GetSelectionController();
+ if (NS_WARN_IF(!selectionController)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ IgnoredErrorResult ignoredError;
+ for (uint32_t i = 0; i < ArrayLength(kIMESelections); ++i) {
+ RefPtr<Selection> selectionOfIME =
+ selectionController->GetSelection(kIMESelections[i]);
+ if (!selectionOfIME) {
+ NS_WARNING("nsISelectionController::GetSelection() failed");
+ continue;
+ }
+ selectionOfIME->RemoveAllRanges(ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "Selection::RemoveAllRanges() failed, but ignored");
+ ignoredError.SuppressException();
+ }
+
+ // Set caret position and selection of IME composition with TextRangeArray.
+ bool setCaret = false;
+ uint32_t countOfRanges = aRanges ? aRanges->Length() : 0;
+
+#ifdef DEBUG
+ // Bounds-checking on debug builds
+ uint32_t maxOffset = aTextNode->Length();
+#endif
+
+ // NOTE: composition string may be truncated when it's committed and
+ // maxlength attribute value doesn't allow input of all text of this
+ // composition.
+ nsresult rv = NS_OK;
+ for (uint32_t i = 0; i < countOfRanges; ++i) {
+ const TextRange& textRange = aRanges->ElementAt(i);
+
+ // Caret needs special handling since its length may be 0 and if it's not
+ // specified explicitly, we need to handle it ourselves later.
+ if (textRange.mRangeType == TextRangeType::eCaret) {
+ NS_ASSERTION(!setCaret, "The ranges already has caret position");
+ NS_ASSERTION(!textRange.Length(),
+ "EditorBase doesn't support wide caret");
+ CheckedUint32 caretOffset(aOffsetInNode);
+ caretOffset +=
+ std::min(textRange.mStartOffset, aLengthOfCompositionString);
+ MOZ_ASSERT(caretOffset.isValid());
+ MOZ_ASSERT(caretOffset.value() <= maxOffset);
+ rv = selection->CollapseInLimiter(aTextNode, caretOffset.value());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Selection::CollapseInLimiter() failed, but might be ignored");
+ setCaret = setCaret || NS_SUCCEEDED(rv);
+ if (!setCaret) {
+ continue;
+ }
+ // If caret range is specified explicitly, we should show the caret if
+ // it should be so.
+ aEditorBase.HideCaret(false);
+ continue;
+ }
+
+ // If the clause length is 0, it should be a bug.
+ if (!textRange.Length()) {
+ NS_WARNING("Any clauses must not be empty");
+ continue;
+ }
+
+ RefPtr<nsRange> clauseRange;
+ CheckedUint32 startOffset = aOffsetInNode;
+ startOffset += std::min(textRange.mStartOffset, aLengthOfCompositionString);
+ MOZ_ASSERT(startOffset.isValid());
+ MOZ_ASSERT(startOffset.value() <= maxOffset);
+ CheckedUint32 endOffset = aOffsetInNode;
+ endOffset += std::min(textRange.mEndOffset, aLengthOfCompositionString);
+ MOZ_ASSERT(endOffset.isValid());
+ MOZ_ASSERT(endOffset.value() >= startOffset.value());
+ MOZ_ASSERT(endOffset.value() <= maxOffset);
+ clauseRange = nsRange::Create(aTextNode, startOffset.value(), aTextNode,
+ endOffset.value(), IgnoreErrors());
+ if (!clauseRange) {
+ NS_WARNING("nsRange::Create() failed, but might be ignored");
+ break;
+ }
+
+ // Set the range of the clause to selection.
+ RefPtr<Selection> selectionOfIME = selectionController->GetSelection(
+ ToRawSelectionType(textRange.mRangeType));
+ if (!selectionOfIME) {
+ NS_WARNING(
+ "nsISelectionController::GetSelection() failed, but might be "
+ "ignored");
+ break;
+ }
+
+ IgnoredErrorResult ignoredError;
+ selectionOfIME->AddRangeAndSelectFramesAndNotifyListeners(*clauseRange,
+ ignoredError);
+ if (ignoredError.Failed()) {
+ NS_WARNING(
+ "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed, but "
+ "might be ignored");
+ break;
+ }
+
+ // Set the style of the clause.
+ rv = selectionOfIME->SetTextRangeStyle(clauseRange, textRange.mRangeStyle);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Selection::SetTextRangeStyle() failed, but might be ignored");
+ break; // but this is unexpected...
+ }
+ }
+
+ // If the ranges doesn't include explicit caret position, let's set the
+ // caret to the end of composition string.
+ if (!setCaret) {
+ CheckedUint32 caretOffset = aOffsetInNode;
+ caretOffset += aLengthOfCompositionString;
+ MOZ_ASSERT(caretOffset.isValid());
+ MOZ_ASSERT(caretOffset.value() <= maxOffset);
+ rv = selection->CollapseInLimiter(aTextNode, caretOffset.value());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Selection::CollapseInLimiter() failed");
+
+ // If caret range isn't specified explicitly, we should hide the caret.
+ // Hiding the caret benefits a Windows build (see bug 555642 comment #6).
+ // However, when there is no range, we should keep showing caret.
+ if (countOfRanges) {
+ aEditorBase.HideCaret(true);
+ }
+ }
+
+ return rv;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/CompositionTransaction.h b/editor/libeditor/CompositionTransaction.h
new file mode 100644
index 0000000000..c780aa3624
--- /dev/null
+++ b/editor/libeditor/CompositionTransaction.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CompositionTransaction_h
+#define CompositionTransaction_h
+
+#include "EditTransactionBase.h" // base class
+
+#include "EditorForwards.h"
+
+#include "mozilla/WeakPtr.h"
+#include "nsCycleCollectionParticipant.h" // various macros
+#include "nsString.h" // mStringToInsert
+
+namespace mozilla {
+class TextComposition;
+class TextRangeArray;
+
+namespace dom {
+class Text;
+} // namespace dom
+
+/**
+ * CompositionTransaction stores all edit for a composition, i.e.,
+ * from compositionstart event to compositionend event. E.g., inserting a
+ * composition string, modifying the composition string or its IME selection
+ * ranges and commit or cancel the composition.
+ */
+class CompositionTransaction final : public EditTransactionBase,
+ public SupportsWeakPtr {
+ protected:
+ CompositionTransaction(EditorBase& aEditorBase,
+ const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert);
+
+ public:
+ /**
+ * Creates a composition transaction. aEditorBase must not return from
+ * GetComposition() while calling this method. Note that this method will
+ * update text node information of aEditorBase.mComposition.
+ *
+ * @param aEditorBase The editor which has composition.
+ * @param aStringToInsert The new composition string to insert. This may
+ * be different from actual composition string.
+ * E.g., password editor can hide the character
+ * with a different character.
+ * @param aPointToInsert The insertion point.
+ */
+ static already_AddRefed<CompositionTransaction> Create(
+ EditorBase& aEditorBase, const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert);
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CompositionTransaction,
+ EditTransactionBase)
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(CompositionTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+ NS_IMETHOD Merge(nsITransaction* aOtherTransaction, bool* aDidMerge) override;
+
+ void MarkFixed();
+
+ MOZ_CAN_RUN_SCRIPT static nsresult SetIMESelection(
+ EditorBase& aEditorBase, dom::Text* aTextNode, uint32_t aOffsetInNode,
+ uint32_t aLengthOfCompositionString, const TextRangeArray* aRanges);
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const CompositionTransaction& aTransaction);
+
+ private:
+ virtual ~CompositionTransaction() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult SetSelectionForRanges();
+
+ // The text element to operate upon.
+ RefPtr<dom::Text> mTextNode;
+
+ // The offsets into mTextNode where the insertion should be placed.
+ uint32_t mOffset;
+
+ uint32_t mReplaceLength;
+
+ // The range list.
+ RefPtr<TextRangeArray> mRanges;
+
+ // The text to insert into mTextNode at mOffset.
+ nsString mStringToInsert;
+
+ // The editor, which is used to get the selection controller.
+ RefPtr<EditorBase> mEditorBase;
+
+ bool mFixed;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef CompositionTransaction_h
diff --git a/editor/libeditor/DeleteContentTransactionBase.cpp b/editor/libeditor/DeleteContentTransactionBase.cpp
new file mode 100644
index 0000000000..a18dcac690
--- /dev/null
+++ b/editor/libeditor/DeleteContentTransactionBase.cpp
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeleteContentTransactionBase.h"
+
+#include "EditorBase.h"
+
+namespace mozilla {
+
+DeleteContentTransactionBase::DeleteContentTransactionBase(
+ EditorBase& aEditorBase)
+ : mEditorBase(&aEditorBase) {}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteContentTransactionBase,
+ EditTransactionBase, mEditorBase)
+
+NS_IMPL_ADDREF_INHERITED(DeleteContentTransactionBase, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(DeleteContentTransactionBase, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteContentTransactionBase)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+} // namespace mozilla
diff --git a/editor/libeditor/DeleteContentTransactionBase.h b/editor/libeditor/DeleteContentTransactionBase.h
new file mode 100644
index 0000000000..484c80a7ac
--- /dev/null
+++ b/editor/libeditor/DeleteContentTransactionBase.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DeleteContentTransactionBase_h
+#define DeleteContentTransactionBase_h
+
+#include "EditTransactionBase.h"
+
+#include "EditorForwards.h"
+
+#include "mozilla/RefPtr.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+/**
+ * An abstract transaction that removes text or node.
+ */
+class DeleteContentTransactionBase : public EditTransactionBase {
+ public:
+ /**
+ * Return a point to put caret if the transaction instance has an idea.
+ */
+ virtual EditorDOMPoint SuggestPointToPutCaret() const = 0;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteContentTransactionBase,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(
+ DeleteContentTransactionBase)
+
+ protected:
+ explicit DeleteContentTransactionBase(EditorBase& aEditorBase);
+ ~DeleteContentTransactionBase() = default;
+
+ RefPtr<EditorBase> mEditorBase;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef DeleteContentTransactionBase_h
diff --git a/editor/libeditor/DeleteMultipleRangesTransaction.cpp b/editor/libeditor/DeleteMultipleRangesTransaction.cpp
new file mode 100644
index 0000000000..cb735f0478
--- /dev/null
+++ b/editor/libeditor/DeleteMultipleRangesTransaction.cpp
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeleteMultipleRangesTransaction.h"
+
+#include "DeleteContentTransactionBase.h"
+#include "DeleteRangeTransaction.h"
+#include "EditorBase.h"
+#include "EditorDOMPoint.h"
+#include "EditTransactionBase.h"
+
+#include "nsDebug.h"
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteMultipleRangesTransaction,
+ EditAggregateTransaction)
+
+NS_IMPL_ADDREF_INHERITED(DeleteMultipleRangesTransaction,
+ EditAggregateTransaction)
+NS_IMPL_RELEASE_INHERITED(DeleteMultipleRangesTransaction,
+ EditAggregateTransaction)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteMultipleRangesTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditAggregateTransaction)
+
+NS_IMETHODIMP DeleteMultipleRangesTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteMultipleRangesTransaction::%s this={ mName=%s } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ nsresult rv = EditAggregateTransaction::DoTransaction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditAggregateTransaction::DoTransaction() failed");
+
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteMultipleRangesTransaction::%s this={ mName=%s } "
+ "End==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return rv;
+}
+
+NS_IMETHODIMP DeleteMultipleRangesTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteMultipleRangesTransaction::%s this={ mName=%s } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+
+ nsresult rv = EditAggregateTransaction::UndoTransaction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditAggregateTransaction::UndoTransaction() failed");
+
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteMultipleRangesTransaction::%s this={ mName=%s } "
+ "End==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return rv;
+}
+
+NS_IMETHODIMP DeleteMultipleRangesTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteMultipleRangesTransaction::%s this={ mName=%s } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+
+ nsresult rv = EditAggregateTransaction::RedoTransaction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditAggregateTransaction::RedoTransaction() failed");
+
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteMultipleRangesTransaction::%s this={ mName=%s } "
+ "End==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return rv;
+}
+
+void DeleteMultipleRangesTransaction::AppendChild(
+ DeleteContentTransactionBase& aTransaction) {
+ mChildren.AppendElement(aTransaction);
+}
+
+void DeleteMultipleRangesTransaction::AppendChild(
+ DeleteRangeTransaction& aTransaction) {
+ mChildren.AppendElement(aTransaction);
+}
+
+EditorDOMPoint DeleteMultipleRangesTransaction::SuggestPointToPutCaret() const {
+ for (const OwningNonNull<EditTransactionBase>& transaction :
+ Reversed(mChildren)) {
+ if (const DeleteContentTransactionBase* deleteContentTransaction =
+ transaction->GetAsDeleteContentTransactionBase()) {
+ EditorDOMPoint pointToPutCaret =
+ deleteContentTransaction->SuggestPointToPutCaret();
+ if (pointToPutCaret.IsSet()) {
+ return pointToPutCaret;
+ }
+ continue;
+ }
+ if (const DeleteRangeTransaction* deleteRangeTransaction =
+ transaction->GetAsDeleteRangeTransaction()) {
+ EditorDOMPoint pointToPutCaret =
+ deleteRangeTransaction->SuggestPointToPutCaret();
+ if (pointToPutCaret.IsSet()) {
+ return pointToPutCaret;
+ }
+ continue;
+ }
+ MOZ_ASSERT_UNREACHABLE(
+ "Child transactions must be DeleteContentTransactionBase or "
+ "DeleteRangeTransaction");
+ }
+ return EditorDOMPoint();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/DeleteMultipleRangesTransaction.h b/editor/libeditor/DeleteMultipleRangesTransaction.h
new file mode 100644
index 0000000000..3af24ba297
--- /dev/null
+++ b/editor/libeditor/DeleteMultipleRangesTransaction.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DeleteMultipleRangesTransactionBase_h
+#define DeleteMultipleRangesTransactionBase_h
+
+#include "DeleteContentTransactionBase.h"
+#include "EditAggregateTransaction.h"
+
+#include "EditorForwards.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+/**
+ * An abstract transaction that removes text or node.
+ */
+class DeleteMultipleRangesTransaction final : public EditAggregateTransaction {
+ public:
+ static already_AddRefed<DeleteMultipleRangesTransaction> Create() {
+ RefPtr<DeleteMultipleRangesTransaction> transaction =
+ new DeleteMultipleRangesTransaction();
+ return transaction.forget();
+ }
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteMultipleRangesTransaction,
+ EditAggregateTransaction)
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(
+ DeleteMultipleRangesTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() final;
+
+ void AppendChild(DeleteContentTransactionBase& aTransaction);
+ void AppendChild(DeleteRangeTransaction& aTransaction);
+
+ /**
+ * Return latest caret point suggestion of child transaction.
+ */
+ EditorDOMPoint SuggestPointToPutCaret() const;
+
+ protected:
+ ~DeleteMultipleRangesTransaction() = default;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef DeleteMultipleRangesTransaction_h
diff --git a/editor/libeditor/DeleteNodeTransaction.cpp b/editor/libeditor/DeleteNodeTransaction.cpp
new file mode 100644
index 0000000000..31d5335b9b
--- /dev/null
+++ b/editor/libeditor/DeleteNodeTransaction.cpp
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeleteNodeTransaction.h"
+
+#include "EditorBase.h"
+#include "EditorDOMPoint.h"
+#include "HTMLEditUtils.h"
+#include "SelectionState.h" // RangeUpdater
+#include "TextEditor.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/ToString.h"
+
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsAString.h"
+
+namespace mozilla {
+
+// static
+already_AddRefed<DeleteNodeTransaction> DeleteNodeTransaction::MaybeCreate(
+ EditorBase& aEditorBase, nsIContent& aContentToDelete) {
+ RefPtr<DeleteNodeTransaction> transaction =
+ new DeleteNodeTransaction(aEditorBase, aContentToDelete);
+ if (NS_WARN_IF(!transaction->CanDoIt())) {
+ return nullptr;
+ }
+ return transaction.forget();
+}
+
+DeleteNodeTransaction::DeleteNodeTransaction(EditorBase& aEditorBase,
+ nsIContent& aContentToDelete)
+ : DeleteContentTransactionBase(aEditorBase),
+ mContentToDelete(&aContentToDelete),
+ mParentNode(aContentToDelete.GetParentNode()) {
+ MOZ_DIAGNOSTIC_ASSERT_IF(
+ aEditorBase.IsHTMLEditor(),
+ HTMLEditUtils::IsRemovableNode(aContentToDelete) ||
+ // It's okay to delete text node if it's added by `HTMLEditor` since
+ // remaining it may be noisy for the users.
+ (aContentToDelete.IsText() &&
+ aContentToDelete.HasFlag(NS_MAYBE_MODIFIED_FREQUENTLY)));
+ NS_ASSERTION(
+ !aEditorBase.IsHTMLEditor() ||
+ HTMLEditUtils::IsRemovableNode(aContentToDelete),
+ "Deleting non-editable text node, please write a test for this!!");
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const DeleteNodeTransaction& aTransaction) {
+ aStream << "{ mContentToDelete=" << aTransaction.mContentToDelete.get();
+ if (aTransaction.mContentToDelete) {
+ aStream << " (" << *aTransaction.mContentToDelete << ")";
+ }
+ aStream << ", mParentNode=" << aTransaction.mParentNode.get();
+ if (aTransaction.mParentNode) {
+ aStream << " (" << *aTransaction.mParentNode << ")";
+ }
+ aStream << ", mRefContent=" << aTransaction.mRefContent.get();
+ if (aTransaction.mRefContent) {
+ aStream << " (" << *aTransaction.mRefContent << ")";
+ }
+ aStream << ", mEditorBase=" << aTransaction.mEditorBase.get() << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteNodeTransaction,
+ DeleteContentTransactionBase,
+ mContentToDelete, mParentNode, mRefContent)
+
+NS_IMPL_ADDREF_INHERITED(DeleteNodeTransaction, DeleteContentTransactionBase)
+NS_IMPL_RELEASE_INHERITED(DeleteNodeTransaction, DeleteContentTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteNodeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(DeleteContentTransactionBase)
+
+bool DeleteNodeTransaction::CanDoIt() const {
+ if (NS_WARN_IF(!mContentToDelete) || NS_WARN_IF(!mEditorBase) ||
+ !mParentNode) {
+ return false;
+ }
+ return mEditorBase->IsTextEditor() ||
+ HTMLEditUtils::IsSimplyEditableNode(*mParentNode);
+}
+
+NS_IMETHODIMP DeleteNodeTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!CanDoIt())) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT_IF(mEditorBase->IsTextEditor(), !mContentToDelete->IsText());
+
+ // Remember which child mContentToDelete was (by remembering which child was
+ // next). Note that mRefContent can be nullptr.
+ mRefContent = mContentToDelete->GetNextSibling();
+
+ // give range updater a chance. SelAdjDeleteNode() needs to be called
+ // *before* we do the action, unlike some of the other RangeItem update
+ // methods.
+ mEditorBase->RangeUpdaterRef().SelAdjDeleteNode(*mContentToDelete);
+
+ OwningNonNull<nsINode> parentNode = *mParentNode;
+ OwningNonNull<nsIContent> contentToDelete = *mContentToDelete;
+ ErrorResult error;
+ parentNode->RemoveChild(contentToDelete, error);
+ NS_WARNING_ASSERTION(!error.Failed(), "nsINode::RemoveChild() failed");
+ return error.StealNSResult();
+}
+
+EditorDOMPoint DeleteNodeTransaction::SuggestPointToPutCaret() const {
+ return EditorDOMPoint();
+}
+
+NS_IMETHODIMP DeleteNodeTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!CanDoIt())) {
+ // This is a legal state, the transaction is a no-op.
+ return NS_OK;
+ }
+ ErrorResult error;
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<nsINode> parentNode = *mParentNode;
+ OwningNonNull<nsIContent> contentToDelete = *mContentToDelete;
+ nsCOMPtr<nsIContent> refContent = mRefContent;
+ // XXX Perhaps, we should check `refContent` is a child of `parentNode`,
+ // and if it's not, we should stop undoing or something.
+ parentNode->InsertBefore(contentToDelete, refContent, error);
+ // InsertBefore() may call MightThrowJSException() even if there is no error.
+ // We don't need the flag here.
+ error.WouldReportJSException();
+ if (error.Failed()) {
+ NS_WARNING("nsINode::InsertBefore() failed");
+ return error.StealNSResult();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP DeleteNodeTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!CanDoIt())) {
+ // This is a legal state, the transaction is a no-op.
+ return NS_OK;
+ }
+
+ mEditorBase->RangeUpdaterRef().SelAdjDeleteNode(*mContentToDelete);
+
+ OwningNonNull<nsINode> parentNode = *mParentNode;
+ OwningNonNull<nsIContent> contentToDelete = *mContentToDelete;
+ ErrorResult error;
+ parentNode->RemoveChild(contentToDelete, error);
+ NS_WARNING_ASSERTION(!error.Failed(), "nsINode::RemoveChild() failed");
+ return error.StealNSResult();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/DeleteNodeTransaction.h b/editor/libeditor/DeleteNodeTransaction.h
new file mode 100644
index 0000000000..bcae89d174
--- /dev/null
+++ b/editor/libeditor/DeleteNodeTransaction.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DeleteNodeTransaction_h
+#define DeleteNodeTransaction_h
+
+#include "DeleteContentTransactionBase.h"
+
+#include "EditorForwards.h"
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+/**
+ * A transaction that deletes a single element
+ */
+class DeleteNodeTransaction final : public DeleteContentTransactionBase {
+ protected:
+ DeleteNodeTransaction(EditorBase& aEditorBase, nsIContent& aContentToDelete);
+
+ public:
+ /**
+ * Creates a delete node transaction instance. This returns nullptr if
+ * it cannot remove the node from its parent.
+ *
+ * @param aEditorBase The editor.
+ * @param aContentToDelete The node to be removed from the DOM tree.
+ */
+ static already_AddRefed<DeleteNodeTransaction> MaybeCreate(
+ EditorBase& aEditorBase, nsIContent& aContentToDelete);
+
+ /**
+ * CanDoIt() returns true if there are enough members and can modify the
+ * parent. Otherwise, false.
+ */
+ bool CanDoIt() const;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteNodeTransaction,
+ DeleteContentTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(DeleteNodeTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() final;
+
+ EditorDOMPoint SuggestPointToPutCaret() const final;
+
+ nsIContent* GetContent() const { return mContentToDelete; }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const DeleteNodeTransaction& aTransaction);
+
+ protected:
+ virtual ~DeleteNodeTransaction() = default;
+
+ // The element to delete.
+ nsCOMPtr<nsIContent> mContentToDelete;
+
+ // Parent of node to delete.
+ nsCOMPtr<nsINode> mParentNode;
+
+ // Next sibling to remember for undo/redo purposes.
+ nsCOMPtr<nsIContent> mRefContent;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef DeleteNodeTransaction_h
diff --git a/editor/libeditor/DeleteRangeTransaction.cpp b/editor/libeditor/DeleteRangeTransaction.cpp
new file mode 100644
index 0000000000..6262f31fd0
--- /dev/null
+++ b/editor/libeditor/DeleteRangeTransaction.cpp
@@ -0,0 +1,399 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeleteRangeTransaction.h"
+
+#include "DeleteContentTransactionBase.h"
+#include "DeleteNodeTransaction.h"
+#include "DeleteTextTransaction.h"
+#include "EditTransactionBase.h"
+#include "EditorBase.h"
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "HTMLEditUtils.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/Logging.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/RangeBoundary.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/ToString.h"
+#include "mozilla/dom/Selection.h"
+
+#include "nsAtom.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsAString.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+using EditorType = EditorUtils::EditorType;
+
+DeleteRangeTransaction::DeleteRangeTransaction(EditorBase& aEditorBase,
+ const nsRange& aRangeToDelete)
+ : mEditorBase(&aEditorBase), mRangeToDelete(aRangeToDelete.CloneRange()) {}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteRangeTransaction,
+ EditAggregateTransaction, mEditorBase,
+ mRangeToDelete, mPointToPutCaret)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteRangeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditAggregateTransaction)
+
+void DeleteRangeTransaction::AppendChild(
+ DeleteContentTransactionBase& aTransaction) {
+ mChildren.AppendElement(aTransaction);
+}
+
+nsresult
+DeleteRangeTransaction::MaybeExtendDeletingRangeWithSurroundingWhitespace(
+ nsRange& aRange) const {
+ if (!mEditorBase->mEditActionData->SelectionCreatedByDoubleclick() ||
+ !StaticPrefs::
+ editor_word_select_delete_space_after_doubleclick_selection()) {
+ return NS_OK;
+ }
+ EditorRawDOMPoint startPoint(aRange.StartRef());
+ EditorRawDOMPoint endPoint(aRange.EndRef());
+ const bool maybeRangeStartsAfterWhiteSpace =
+ startPoint.IsInTextNode() && !startPoint.IsStartOfContainer();
+ const bool maybeRangeEndsAtWhiteSpace =
+ endPoint.IsInTextNode() && !endPoint.IsEndOfContainer();
+
+ if (!maybeRangeStartsAfterWhiteSpace && !maybeRangeEndsAtWhiteSpace) {
+ // no whitespace before or after word => nothing to do here.
+ return NS_OK;
+ }
+
+ const bool precedingCharIsWhitespace =
+ maybeRangeStartsAfterWhiteSpace
+ ? startPoint.IsPreviousCharASCIISpaceOrNBSP()
+ : false;
+ const bool trailingCharIsWhitespace =
+ maybeRangeEndsAtWhiteSpace ? endPoint.IsCharASCIISpaceOrNBSP() : false;
+
+ // if possible, try to remove the preceding whitespace
+ // so the caret is at the end of the previous word.
+ if (precedingCharIsWhitespace) {
+ // "one [two]", "one [two] three" or "one [two], three"
+ ErrorResult err;
+ aRange.SetStart(startPoint.PreviousPoint(), err);
+ if (auto rv = err.StealNSResult(); NS_FAILED(rv)) {
+ NS_WARNING(
+ "DeleteRangeTransaction::"
+ "MaybeExtendDeletingRangeWithSurroundingWhitespace"
+ " failed to update the start of the deleting range");
+ return rv;
+ }
+ } else if (trailingCharIsWhitespace) {
+ // "[one] two"
+ ErrorResult err;
+ aRange.SetEnd(endPoint.NextPoint(), err);
+ if (auto rv = err.StealNSResult(); NS_FAILED(rv)) {
+ NS_WARNING(
+ "DeleteRangeTransaction::"
+ "MaybeExtendDeletingRangeWithSurroundingWhitespace"
+ " failed to update the end of the deleting range");
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP DeleteRangeTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteRangeTransaction::%s this={ mName=%s } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+
+ if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mRangeToDelete)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Swap mRangeToDelete out into a stack variable, so we make sure to null it
+ // out on return from this function. Once this function returns, we no longer
+ // need mRangeToDelete, and keeping it alive in the long term slows down all
+ // DOM mutations because it's observing them.
+ RefPtr<nsRange> rangeToDelete;
+ rangeToDelete.swap(mRangeToDelete);
+
+ MaybeExtendDeletingRangeWithSurroundingWhitespace(*rangeToDelete);
+
+ // build the child transactions
+ // XXX We should move this to the constructor. Then, we don't need to make
+ // this class has mRangeToDelete with nullptr since transaction instances
+ // may be created a lot and live long.
+ {
+ EditorRawDOMRange extendedRange(*rangeToDelete);
+ MOZ_ASSERT(extendedRange.IsPositionedAndValid());
+
+ if (extendedRange.InSameContainer()) {
+ // the selection begins and ends in the same node
+ nsresult rv = AppendTransactionsToDeleteIn(extendedRange);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "DeleteRangeTransaction::AppendTransactionsToDeleteIn() failed");
+ return rv;
+ }
+ } else {
+ // If the range ends at end of a node, we may need to extend the range to
+ // make ContentSubtreeIterator iterates close tag of the unnecessary nodes
+ // in AppendTransactionsToDeleteNodesEntirelyIn.
+ for (EditorRawDOMPoint endOfRange = extendedRange.EndRef();
+ endOfRange.IsInContentNode() && endOfRange.IsEndOfContainer() &&
+ endOfRange.GetContainer() != extendedRange.StartRef().GetContainer();
+ endOfRange = extendedRange.EndRef()) {
+ extendedRange.SetEnd(
+ EditorRawDOMPoint::After(*endOfRange.ContainerAs<nsIContent>()));
+ }
+
+ // the selection ends in a different node from where it started. delete
+ // the relevant content in the start node
+ nsresult rv = AppendTransactionToDeleteText(extendedRange.StartRef(),
+ nsIEditor::eNext);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "DeleteRangeTransaction::AppendTransactionToDeleteText() failed");
+ return rv;
+ }
+ // delete the intervening nodes
+ rv = AppendTransactionsToDeleteNodesWhoseEndBoundaryIn(extendedRange);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "DeleteRangeTransaction::"
+ "AppendTransactionsToDeleteNodesWhoseEndBoundaryIn() failed");
+ return rv;
+ }
+ // delete the relevant content in the end node
+ rv = AppendTransactionToDeleteText(extendedRange.EndRef(),
+ nsIEditor::ePrevious);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "DeleteRangeTransaction::AppendTransactionToDeleteText() failed");
+ return rv;
+ }
+ }
+ }
+
+ // if we've successfully built this aggregate transaction, then do it.
+ nsresult rv = EditAggregateTransaction::DoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditAggregateTransaction::DoTransaction() failed");
+ return rv;
+ }
+
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteRangeTransaction::%s this={ mName=%s } "
+ "End==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+
+ mPointToPutCaret = rangeToDelete->StartRef();
+ if (MOZ_UNLIKELY(!mPointToPutCaret.IsSetAndValid())) {
+ for (const OwningNonNull<EditTransactionBase>& transaction :
+ Reversed(mChildren)) {
+ if (const DeleteContentTransactionBase* deleteContentTransaction =
+ transaction->GetAsDeleteContentTransactionBase()) {
+ mPointToPutCaret = deleteContentTransaction->SuggestPointToPutCaret();
+ if (mPointToPutCaret.IsSetAndValid()) {
+ break;
+ }
+ continue;
+ }
+ MOZ_ASSERT_UNREACHABLE(
+ "Child transactions must be DeleteContentTransactionBase");
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP DeleteRangeTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteRangeTransaction::%s this={ mName=%s } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+
+ nsresult rv = EditAggregateTransaction::UndoTransaction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditAggregateTransaction::UndoTransaction() failed");
+
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteRangeTransaction::%s this={ mName=%s } "
+ "End==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return rv;
+}
+
+NS_IMETHODIMP DeleteRangeTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteRangeTransaction::%s this={ mName=%s } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+
+ nsresult rv = EditAggregateTransaction::RedoTransaction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditAggregateTransaction::RedoTransaction() failed");
+
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteRangeTransaction::%s this={ mName=%s } "
+ "End==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return rv;
+}
+
+nsresult DeleteRangeTransaction::AppendTransactionsToDeleteIn(
+ const EditorRawDOMRange& aRangeToDelete) {
+ if (NS_WARN_IF(!aRangeToDelete.IsPositionedAndValid())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ MOZ_ASSERT(aRangeToDelete.InSameContainer());
+
+ if (NS_WARN_IF(!mEditorBase)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // see what kind of node we have
+ if (Text* textNode = aRangeToDelete.StartRef().GetContainerAs<Text>()) {
+ if (mEditorBase->IsHTMLEditor() &&
+ NS_WARN_IF(
+ !EditorUtils::IsEditableContent(*textNode, EditorType::HTML))) {
+ // Just ignore to append the transaction for non-editable node.
+ return NS_OK;
+ }
+ // if the node is a chardata node, then delete chardata content
+ uint32_t textLengthToDelete;
+ if (aRangeToDelete.Collapsed()) {
+ textLengthToDelete = 1;
+ } else {
+ textLengthToDelete =
+ aRangeToDelete.EndRef().Offset() - aRangeToDelete.StartRef().Offset();
+ MOZ_DIAGNOSTIC_ASSERT(textLengthToDelete > 0);
+ }
+
+ RefPtr<DeleteTextTransaction> deleteTextTransaction =
+ DeleteTextTransaction::MaybeCreate(*mEditorBase, *textNode,
+ aRangeToDelete.StartRef().Offset(),
+ textLengthToDelete);
+ // If the text node isn't editable, it should be never undone/redone.
+ // So, the transaction shouldn't be recorded.
+ if (!deleteTextTransaction) {
+ NS_WARNING("DeleteTextTransaction::MaybeCreate() failed");
+ return NS_ERROR_FAILURE;
+ }
+ AppendChild(*deleteTextTransaction);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mEditorBase->IsHTMLEditor());
+
+ // Even if we detect invalid range, we should ignore it for removing
+ // specified range's nodes as far as possible.
+ // XXX This is super expensive. Probably, we should make
+ // DeleteNodeTransaction() can treat multiple siblings.
+ for (nsIContent* child = aRangeToDelete.StartRef().GetChild();
+ child && child != aRangeToDelete.EndRef().GetChild();
+ child = child->GetNextSibling()) {
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*child))) {
+ continue; // Should we abort?
+ }
+ RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
+ DeleteNodeTransaction::MaybeCreate(*mEditorBase, *child);
+ if (deleteNodeTransaction) {
+ AppendChild(*deleteNodeTransaction);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult DeleteRangeTransaction::AppendTransactionToDeleteText(
+ const EditorRawDOMPoint& aMaybePointInText, nsIEditor::EDirection aAction) {
+ if (NS_WARN_IF(!aMaybePointInText.IsSetAndValid())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!mEditorBase)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (!aMaybePointInText.IsInTextNode()) {
+ return NS_OK;
+ }
+
+ // If the node is a text node, then delete text before or after the point.
+ Text& textNode = *aMaybePointInText.ContainerAs<Text>();
+ uint32_t startOffset, numToDelete;
+ if (nsIEditor::eNext == aAction) {
+ startOffset = aMaybePointInText.Offset();
+ numToDelete = textNode.TextDataLength() - startOffset;
+ } else {
+ startOffset = 0;
+ numToDelete = aMaybePointInText.Offset();
+ }
+
+ if (!numToDelete) {
+ return NS_OK;
+ }
+
+ RefPtr<DeleteTextTransaction> deleteTextTransaction =
+ DeleteTextTransaction::MaybeCreate(*mEditorBase, textNode, startOffset,
+ numToDelete);
+ // If the text node isn't editable, it should be never undone/redone.
+ // So, the transaction shouldn't be recorded.
+ if (MOZ_UNLIKELY(!deleteTextTransaction)) {
+ NS_WARNING("DeleteTextTransaction::MaybeCreate() failed");
+ return NS_ERROR_FAILURE;
+ }
+ AppendChild(*deleteTextTransaction);
+ return NS_OK;
+}
+
+nsresult
+DeleteRangeTransaction::AppendTransactionsToDeleteNodesWhoseEndBoundaryIn(
+ const EditorRawDOMRange& aRangeToDelete) {
+ if (NS_WARN_IF(!mEditorBase)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ ContentSubtreeIterator subtreeIter;
+ nsresult rv = subtreeIter.Init(aRangeToDelete.StartRef().ToRawRangeBoundary(),
+ aRangeToDelete.EndRef().ToRawRangeBoundary());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("ContentSubtreeIterator::Init() failed");
+ return rv;
+ }
+
+ for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
+ nsINode* node = subtreeIter.GetCurrentNode();
+ if (NS_WARN_IF(!node) || NS_WARN_IF(!node->IsContent())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*node->AsContent()))) {
+ continue;
+ }
+ RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
+ DeleteNodeTransaction::MaybeCreate(*mEditorBase, *node->AsContent());
+ if (deleteNodeTransaction) {
+ AppendChild(*deleteNodeTransaction);
+ }
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/DeleteRangeTransaction.h b/editor/libeditor/DeleteRangeTransaction.h
new file mode 100644
index 0000000000..623b513944
--- /dev/null
+++ b/editor/libeditor/DeleteRangeTransaction.h
@@ -0,0 +1,156 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DeleteRangeTransaction_h
+#define DeleteRangeTransaction_h
+
+#include "DeleteContentTransactionBase.h"
+#include "EditAggregateTransaction.h"
+
+#include "EditorBase.h"
+#include "EditorDOMPoint.h"
+#include "EditorForwards.h"
+
+#include "mozilla/RefPtr.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h"
+#include "nsIEditor.h"
+#include "nsISupportsImpl.h"
+#include "nsRange.h"
+#include "nscore.h"
+
+class nsINode;
+
+namespace mozilla {
+
+/**
+ * A transaction that deletes an entire range in the content tree
+ */
+class DeleteRangeTransaction final : public EditAggregateTransaction {
+ protected:
+ DeleteRangeTransaction(EditorBase& aEditorBase,
+ const nsRange& aRangeToDelete);
+
+ public:
+ /**
+ * Creates a delete range transaction. This never returns nullptr.
+ *
+ * @param aEditorBase The object providing basic editing operations.
+ * @param aRangeToDelete The range to delete.
+ */
+ static already_AddRefed<DeleteRangeTransaction> Create(
+ EditorBase& aEditorBase, const nsRange& aRangeToDelete) {
+ RefPtr<DeleteRangeTransaction> transaction =
+ new DeleteRangeTransaction(aEditorBase, aRangeToDelete);
+ return transaction.forget();
+ }
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteRangeTransaction,
+ EditAggregateTransaction)
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(DeleteRangeTransaction)
+
+ void AppendChild(DeleteContentTransactionBase& aTransaction);
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+
+ /**
+ * Return a good point to put caret after calling DoTransaction().
+ */
+ EditorDOMPoint SuggestPointToPutCaret() const {
+ return mPointToPutCaret.IsSetAndValid() ? mPointToPutCaret
+ : EditorDOMPoint();
+ }
+
+ protected:
+ /**
+ * Extend the range by adding a surrounding whitespace character to the range
+ * that is about to be deleted. This method depends on the pref
+ * `layout.word_select.delete_space_after_doubleclick_selection`.
+ *
+ * Considered cases:
+ * "one [two] three" -> "one [two ]three" -> "one three"
+ * "[one] two" -> "[one ]two" -> "two"
+ * "one [two]" -> "one[ two]" -> "one"
+ * "one [two], three" -> "one[ two], three" -> "one, three"
+ * "one [two]" -> "one [ two]" -> "one "
+ *
+ * @param aRange [inout] The range that is about to be deleted.
+ * @return NS_OK, unless nsRange::SetStart / ::SetEnd fails.
+ */
+ nsresult MaybeExtendDeletingRangeWithSurroundingWhitespace(
+ nsRange& aRange) const;
+
+ /**
+ * AppendTransactionsToDeleteIn() creates a DeleteTextTransaction or some
+ * DeleteNodeTransactions to remove text or nodes between aStart and aEnd
+ * and appends the created transactions to the array.
+ *
+ * @param aRangeToDelete Must be positioned, valid and in same container.
+ * @return Returns NS_OK in most cases.
+ * When the arguments are invalid, returns
+ * NS_ERROR_INVALID_ARG.
+ * When mEditorBase isn't available, returns
+ * NS_ERROR_NOT_AVAILABLE.
+ * When created DeleteTextTransaction cannot do its
+ * transaction, returns NS_ERROR_FAILURE.
+ * Note that even if one of created DeleteNodeTransaction
+ * cannot do its transaction, this returns NS_OK.
+ */
+ nsresult AppendTransactionsToDeleteIn(
+ const EditorRawDOMRange& aRangeToDelete);
+
+ /**
+ * AppendTransactionsToDeleteNodesWhoseEndBoundaryIn() creates
+ * DeleteNodeTransaction instances to remove nodes whose end is in the range
+ * (in other words, its end tag is in the range if it's an element) and append
+ * them to the array.
+ *
+ * @param aRangeToDelete Must be positioned and valid.
+ */
+ nsresult AppendTransactionsToDeleteNodesWhoseEndBoundaryIn(
+ const EditorRawDOMRange& aRangeToDelete);
+
+ /**
+ * AppendTransactionToDeleteText() creates a DeleteTextTransaction to delete
+ * text between start of aPoint.GetContainer() and aPoint or aPoint and end of
+ * aPoint.GetContainer() and appends the created transaction to the array.
+ *
+ * @param aMaybePointInText Must be set and valid. If the point is not
+ * in a text node, this method does nothing.
+ * @param aAction If nsIEditor::eNext, this method creates a transaction
+ * to delete text from aPoint to the end of the data node.
+ * Otherwise, this method creates a transaction to delete
+ * text from start of the data node to aPoint.
+ * @return Returns NS_OK in most cases.
+ * When the arguments are invalid, returns
+ * NS_ERROR_INVALID_ARG.
+ * When mEditorBase isn't available, returns
+ * NS_ERROR_NOT_AVAILABLE.
+ * When created DeleteTextTransaction cannot do its
+ * transaction, returns NS_ERROR_FAILURE.
+ * Note that even if no character will be deleted,
+ * this returns NS_OK.
+ */
+ nsresult AppendTransactionToDeleteText(
+ const EditorRawDOMPoint& aMaybePointInText,
+ nsIEditor::EDirection aAction);
+
+ // The editor for this transaction.
+ RefPtr<EditorBase> mEditorBase;
+
+ // P1 in the range. This is only non-null until DoTransaction is called and
+ // we convert it into child transactions.
+ RefPtr<nsRange> mRangeToDelete;
+
+ EditorDOMPoint mPointToPutCaret;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef DeleteRangeTransaction_h
diff --git a/editor/libeditor/DeleteTextTransaction.cpp b/editor/libeditor/DeleteTextTransaction.cpp
new file mode 100644
index 0000000000..070cf28f9c
--- /dev/null
+++ b/editor/libeditor/DeleteTextTransaction.cpp
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeleteTextTransaction.h"
+
+#include "EditorBase.h"
+#include "EditorDOMPoint.h"
+#include "HTMLEditUtils.h"
+#include "SelectionState.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/Selection.h"
+
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsISupportsImpl.h"
+#include "nsAString.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+// static
+already_AddRefed<DeleteTextTransaction> DeleteTextTransaction::MaybeCreate(
+ EditorBase& aEditorBase, Text& aTextNode, uint32_t aOffset,
+ uint32_t aLengthToDelete) {
+ RefPtr<DeleteTextTransaction> transaction = new DeleteTextTransaction(
+ aEditorBase, aTextNode, aOffset, aLengthToDelete);
+ return transaction.forget();
+}
+
+// static
+already_AddRefed<DeleteTextTransaction>
+DeleteTextTransaction::MaybeCreateForPreviousCharacter(EditorBase& aEditorBase,
+ Text& aTextNode,
+ uint32_t aOffset) {
+ if (NS_WARN_IF(!aOffset)) {
+ return nullptr;
+ }
+
+ nsAutoString data;
+ aTextNode.GetData(data);
+ if (NS_WARN_IF(data.IsEmpty())) {
+ return nullptr;
+ }
+
+ uint32_t length = 1;
+ uint32_t offset = aOffset - 1;
+ if (offset && NS_IS_SURROGATE_PAIR(data[offset - 1], data[offset])) {
+ ++length;
+ --offset;
+ }
+ return DeleteTextTransaction::MaybeCreate(aEditorBase, aTextNode, offset,
+ length);
+}
+
+// static
+already_AddRefed<DeleteTextTransaction>
+DeleteTextTransaction::MaybeCreateForNextCharacter(EditorBase& aEditorBase,
+ Text& aTextNode,
+ uint32_t aOffset) {
+ nsAutoString data;
+ aTextNode.GetData(data);
+ if (NS_WARN_IF(aOffset >= data.Length()) || NS_WARN_IF(data.IsEmpty())) {
+ return nullptr;
+ }
+
+ uint32_t length = 1;
+ if (aOffset + 1 < data.Length() &&
+ NS_IS_SURROGATE_PAIR(data[aOffset], data[aOffset + 1])) {
+ ++length;
+ }
+ return DeleteTextTransaction::MaybeCreate(aEditorBase, aTextNode, aOffset,
+ length);
+}
+
+DeleteTextTransaction::DeleteTextTransaction(EditorBase& aEditorBase,
+ Text& aTextNode, uint32_t aOffset,
+ uint32_t aLengthToDelete)
+ : DeleteContentTransactionBase(aEditorBase),
+ mTextNode(&aTextNode),
+ mOffset(aOffset),
+ mLengthToDelete(aLengthToDelete) {
+ NS_ASSERTION(mTextNode->Length() >= aOffset + aLengthToDelete,
+ "Trying to delete more characters than in node");
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const DeleteTextTransaction& aTransaction) {
+ aStream << "{ mTextNode=" << aTransaction.mTextNode.get();
+ if (aTransaction.mTextNode) {
+ aStream << " (" << *aTransaction.mTextNode << ")";
+ }
+ aStream << ", mOffset=" << aTransaction.mOffset
+ << ", mLengthToDelete=" << aTransaction.mLengthToDelete
+ << ", mDeletedText=\""
+ << NS_ConvertUTF16toUTF8(aTransaction.mDeletedText).get() << "\""
+ << ", mEditorBase=" << aTransaction.mEditorBase.get() << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteTextTransaction,
+ DeleteContentTransactionBase, mTextNode)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteTextTransaction)
+NS_INTERFACE_MAP_END_INHERITING(DeleteContentTransactionBase)
+
+bool DeleteTextTransaction::CanDoIt() const {
+ if (NS_WARN_IF(!mTextNode) || NS_WARN_IF(!mEditorBase)) {
+ return false;
+ }
+ return mEditorBase->IsTextEditor() ||
+ HTMLEditUtils::IsSimplyEditableNode(*mTextNode);
+}
+
+NS_IMETHODIMP DeleteTextTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteTextTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!CanDoIt())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Get the text that we're about to delete
+ IgnoredErrorResult error;
+ mTextNode->SubstringData(mOffset, mLengthToDelete, mDeletedText, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("Text::SubstringData() failed");
+ return error.StealNSResult();
+ }
+
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<Text> textNode = *mTextNode;
+ editorBase->DoDeleteText(textNode, mOffset, mLengthToDelete, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("EditorBase::DoDeleteText() failed");
+ return error.StealNSResult();
+ }
+
+ editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, mOffset,
+ mLengthToDelete);
+ return NS_OK;
+}
+
+EditorDOMPoint DeleteTextTransaction::SuggestPointToPutCaret() const {
+ if (NS_WARN_IF(!mTextNode) || NS_WARN_IF(!mTextNode->IsInComposedDoc())) {
+ return EditorDOMPoint();
+ }
+ return EditorDOMPoint(mTextNode, mOffset);
+}
+
+// XXX: We may want to store the selection state and restore it properly. Was
+// it an insertion point or an extended selection?
+NS_IMETHODIMP DeleteTextTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteTextTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!CanDoIt())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ RefPtr<EditorBase> editorBase = mEditorBase;
+ RefPtr<Text> textNode = mTextNode;
+ ErrorResult error;
+ editorBase->DoInsertText(*textNode, mOffset, mDeletedText, error);
+ NS_WARNING_ASSERTION(!error.Failed(), "EditorBase::DoInsertText() failed");
+ return error.StealNSResult();
+}
+
+NS_IMETHODIMP DeleteTextTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p DeleteTextTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+ nsresult rv = DoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("DeleteTextTransaction::DoTransaction() failed");
+ return rv;
+ }
+ if (!mEditorBase || !mEditorBase->AllowsTransactionsToChangeSelection()) {
+ return NS_OK;
+ }
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ rv = editorBase->CollapseSelectionTo(SuggestPointToPutCaret());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/DeleteTextTransaction.h b/editor/libeditor/DeleteTextTransaction.h
new file mode 100644
index 0000000000..9d572efa21
--- /dev/null
+++ b/editor/libeditor/DeleteTextTransaction.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DeleteTextTransaction_h
+#define DeleteTextTransaction_h
+
+#include "DeleteContentTransactionBase.h"
+
+#include "EditorForwards.h"
+
+#include "mozilla/dom/Text.h"
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h"
+#include "nsString.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+/**
+ * A transaction that removes text from a content node.
+ */
+class DeleteTextTransaction final : public DeleteContentTransactionBase {
+ protected:
+ DeleteTextTransaction(EditorBase& aEditorBase, dom::Text& aTextNode,
+ uint32_t aOffset, uint32_t aLengthToDelete);
+
+ public:
+ /**
+ * Creates a delete text transaction to remove given range. This returns
+ * nullptr if it cannot modify the text node.
+ *
+ * @param aEditorBase The provider of basic editing operations.
+ * @param aTextNode The content node to remove text from.
+ * @param aOffset The location in aElement to begin the deletion.
+ * @param aLenthToDelete The length to delete.
+ */
+ static already_AddRefed<DeleteTextTransaction> MaybeCreate(
+ EditorBase& aEditorBase, dom::Text& aTextNode, uint32_t aOffset,
+ uint32_t aLengthToDelete);
+
+ /**
+ * Creates a delete text transaction to remove a previous or next character.
+ * Those methods MAY return nullptr.
+ *
+ * @param aEditorBase The provider of basic editing operations.
+ * @param aTextNode The content node to remove text from.
+ * @param aOffset The location in aElement to begin the deletion.
+ */
+ static already_AddRefed<DeleteTextTransaction>
+ MaybeCreateForPreviousCharacter(EditorBase& aEditorBase, dom::Text& aTextNode,
+ uint32_t aOffset);
+ static already_AddRefed<DeleteTextTransaction> MaybeCreateForNextCharacter(
+ EditorBase& aEditorBase, dom::Text& aTextNode, uint32_t aOffset);
+
+ /**
+ * CanDoIt() returns true if there are enough members and can modify the
+ * text. Otherwise, false.
+ */
+ bool CanDoIt() const;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteTextTransaction,
+ DeleteContentTransactionBase)
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(DeleteTextTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() final;
+
+ EditorDOMPoint SuggestPointToPutCaret() const final;
+
+ dom::Text* GetText() const { return mTextNode; }
+ uint32_t Offset() const { return mOffset; }
+ uint32_t LengthToDelete() const { return mLengthToDelete; }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const DeleteTextTransaction& aTransaction);
+
+ protected:
+ // The CharacterData node to operate upon.
+ RefPtr<dom::Text> mTextNode;
+
+ // The offset into mTextNode where the deletion is to take place.
+ uint32_t mOffset;
+
+ // The length to delete.
+ uint32_t mLengthToDelete;
+
+ // The text that was deleted.
+ nsString mDeletedText;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef DeleteTextTransaction_h
diff --git a/editor/libeditor/EditAction.h b/editor/libeditor/EditAction.h
new file mode 100644
index 0000000000..6b900b0587
--- /dev/null
+++ b/editor/libeditor/EditAction.h
@@ -0,0 +1,867 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditAction_h
+#define mozilla_EditAction_h
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/StaticPrefs_dom.h"
+
+namespace mozilla {
+
+/**
+ * EditAction indicates which operation or command causes running the methods
+ * of editors.
+ */
+enum class EditAction {
+ // eNone indicates no edit action is being handled.
+ eNone,
+
+ // eNotEditing indicates that something is retrieved, doing something at
+ // destroying or focus move etc, i.e., not edit action is being handled but
+ // editor is doing something.
+ eNotEditing,
+
+ // eInitializing indicates that the editor instance is being initialized.
+ eInitializing,
+
+ // eInsertText indicates to insert some characters.
+ eInsertText,
+
+ // eInsertParagraphSeparator indicates to insert a paragraph separator such
+ // as <p>, <div>.
+ eInsertParagraphSeparator,
+
+ // eInsertLineBreak indicates to insert \n into TextEditor or a <br> element
+ // in HTMLEditor.
+ eInsertLineBreak,
+
+ // eDeleteSelection indicates to delete selected content or content around
+ // caret if selection is collapsed.
+ eDeleteSelection,
+
+ // eDeleteBackward indicates to remove previous character element of caret.
+ // This may be set even when Selection is not collapsed.
+ eDeleteBackward,
+
+ // eDeleteForward indicates to remove next character or element of caret.
+ // This may be set even when Selection is not collapsed.
+ eDeleteForward,
+
+ // eDeleteWordBackward indicates to remove previous word. If caret is in
+ // a word, remove characters between word start and caret.
+ // This may be set even when Selection is not collapsed.
+ eDeleteWordBackward,
+
+ // eDeleteWordForward indicates to remove next word. If caret is in a
+ // word, remove characters between caret and word end.
+ // This may be set even when Selection is not collapsed.
+ eDeleteWordForward,
+
+ // eDeleteToBeginningOfSoftLine indicates to remove characters between
+ // caret and previous visual line break.
+ // This may be set even when Selection is not collapsed.
+ eDeleteToBeginningOfSoftLine,
+
+ // eDeleteToEndOfSoftLine indicates to remove characters between caret and
+ // next visual line break.
+ // This may be set even when Selection is not collapsed.
+ eDeleteToEndOfSoftLine,
+
+ // eDeleteByDrag indicates to remove selection by dragging the content
+ // to different place.
+ eDeleteByDrag,
+
+ // eStartComposition indicates that user starts composition.
+ eStartComposition,
+
+ // eUpdateComposition indicates that user updates composition with
+ // new non-empty composition string and IME selections.
+ eUpdateComposition,
+
+ // eCommitComposition indicates that user commits composition.
+ eCommitComposition,
+
+ // eCancelComposition indicates that user cancels composition.
+ eCancelComposition,
+
+ // eDeleteByComposition indicates that user starts composition with
+ // empty string and there was selected content.
+ eDeleteByComposition,
+
+ // eUndo/eRedo indicate to undo/redo a transaction.
+ eUndo,
+ eRedo,
+
+ // eSetTextDirection indicates that setting text direction (LTR or RTL).
+ eSetTextDirection,
+
+ // eCut indicates to delete selected content and copy it to the clipboard.
+ eCut,
+
+ // eCopy indicates to copy selected content to the clipboard.
+ eCopy,
+
+ // ePaste indicates to paste clipboard data.
+ ePaste,
+
+ // ePasteAsQuotation indicates to paste clipboard data as quotation.
+ ePasteAsQuotation,
+
+ // eDrop indicates that user drops dragging item into the editor.
+ eDrop,
+
+ // eIndent indicates that to indent selected line(s).
+ eIndent,
+
+ // eOutdent indicates that to outdent selected line(s).
+ eOutdent,
+
+ // eReplaceText indicates to replace a part of range in editor with
+ // specific text. For example, user select a correct word in suggestions
+ // of spellchecker or a suggestion in list of autocomplete.
+ eReplaceText,
+
+ // eInsertTableRowElement indicates to insert table rows (i.e., <tr>
+ // elements).
+ eInsertTableRowElement,
+
+ // eRemoveTableRowElement indicates to remove table row elements.
+ eRemoveTableRowElement,
+
+ // eInsertTableColumn indicates to insert cell elements to each row.
+ eInsertTableColumn,
+
+ // eRemoveTableColumn indicates to remove cell elements from each row.
+ eRemoveTableColumn,
+
+ // eResizingElement indicates that user starts to resize or keep resizing
+ // with dragging a resizer which is provided by Gecko.
+ eResizingElement,
+
+ // eResizeElement indicates that user resizes an element size with finishing
+ // dragging a resizer which is provided by Gecko.
+ eResizeElement,
+
+ // eMovingElement indicates that user starts to move or keep moving an
+ // element with grabber which is provided by Gecko.
+ eMovingElement,
+
+ // eMoveElement indicates that user finishes moving an element with grabber
+ // which is provided by Gecko.
+ eMoveElement,
+
+ // The following edit actions are not user's operation. They are caused
+ // by if UI does something or web apps does something with JS.
+
+ // eUnknown indicates some special edit actions, e.g., batching of some
+ // nsI*Editor method calls. This shouldn't be set while handling a user
+ // operation.
+ eUnknown,
+
+ // eSetAttribute indicates to set attribute value of an element node.
+ eSetAttribute,
+
+ // eRemoveAttribute indicates to remove attribute from an element node.
+ eRemoveAttribute,
+
+ // eInsertNode indicates to insert a node into the tree.
+ eInsertNode,
+
+ // eDeleteNode indicates to remove a node form the tree.
+ eRemoveNode,
+
+ // eInsertBlockElement indicates to insert a block-level element like <div>,
+ // <pre>, <li>, <dd> etc.
+ eInsertBlockElement,
+
+ // eInsertHorizontalRuleElement indicates to insert a <hr> element.
+ eInsertHorizontalRuleElement,
+
+ // eInsertLinkElement indicates to insert an anchor element which has
+ // href attribute.
+ eInsertLinkElement,
+
+ // eInsertUnorderedListElement and eInsertOrderedListElement indicate to
+ // insert <ul> or <ol> element.
+ eInsertUnorderedListElement,
+ eInsertOrderedListElement,
+
+ // eRemoveUnorderedListElement and eRemoveOrderedListElement indicate to
+ // remove <ul> or <ol> element.
+ eRemoveUnorderedListElement,
+ eRemoveOrderedListElement,
+
+ // eRemoveListElement indicates to remove <ul>, <ol> and/or <dl> element.
+ eRemoveListElement,
+
+ // eInsertBlockquoteElement indicates to insert a <blockquote> element.
+ eInsertBlockquoteElement,
+
+ // eNormalizeTable indicates to normalize table. E.g., if a row does
+ // not have enough number of cells, inserts empty cells.
+ eNormalizeTable,
+
+ // eRemoveTableElement indicates to remove <table> element.
+ eRemoveTableElement,
+
+ // eRemoveTableCellContents indicates to remove any children in a table
+ // cell element.
+ eDeleteTableCellContents,
+
+ // eInsertTableCellElement indicates to insert table cell elements (i.e.,
+ // <td> or <th>).
+ eInsertTableCellElement,
+
+ // eRemoveTableCellEelement indicates to remove table cell elements.
+ eRemoveTableCellElement,
+
+ // eJoinTableCellElements indicates to join table cell elements.
+ eJoinTableCellElements,
+
+ // eSplitTableCellElement indicates to split table cell elements.
+ eSplitTableCellElement,
+
+ // eSetTableCellElementType indicates to set table cell element type to
+ // <td> or <th>.
+ eSetTableCellElementType,
+
+ // Those edit actions are mapped to the methods in nsITableEditor which
+ // access table layout information.
+ eSelectTableCell,
+ eSelectTableRow,
+ eSelectTableColumn,
+ eSelectTable,
+ eSelectAllTableCells,
+ eGetCellIndexes,
+ eGetTableSize,
+ eGetCellAt,
+ eGetCellDataAt,
+ eGetFirstRow,
+ eGetSelectedOrParentTableElement,
+ eGetSelectedCellsType,
+ eGetFirstSelectedCellInTable,
+ eGetSelectedCells,
+
+ // eSetInlineStyleProperty indicates to set CSS another inline style property
+ // which is not defined below.
+ eSetInlineStyleProperty,
+
+ // eRemoveInlineStyleProperty indicates to remove a CSS text property which
+ // is not defined below.
+ eRemoveInlineStyleProperty,
+
+ // <b> or font-weight.
+ eSetFontWeightProperty,
+ eRemoveFontWeightProperty,
+
+ // <i> or text-style: italic/oblique.
+ eSetTextStyleProperty,
+ eRemoveTextStyleProperty,
+
+ // <u> or text-decoration: underline.
+ eSetTextDecorationPropertyUnderline,
+ eRemoveTextDecorationPropertyUnderline,
+
+ // <strike> or text-decoration: line-through.
+ eSetTextDecorationPropertyLineThrough,
+ eRemoveTextDecorationPropertyLineThrough,
+
+ // <sup> or text-align: super.
+ eSetVerticalAlignPropertySuper,
+ eRemoveVerticalAlignPropertySuper,
+
+ // <sub> or text-align: sub.
+ eSetVerticalAlignPropertySub,
+ eRemoveVerticalAlignPropertySub,
+
+ // <font face="foo"> or font-family.
+ eSetFontFamilyProperty,
+ eRemoveFontFamilyProperty,
+
+ // <font color="foo"> or color.
+ eSetColorProperty,
+ eRemoveColorProperty,
+
+ // <span style="background-color: foo">
+ eSetBackgroundColorPropertyInline,
+ eRemoveBackgroundColorPropertyInline,
+
+ // eRemoveAllInlineStyleProperties indicates to remove all CSS inline
+ // style properties.
+ eRemoveAllInlineStyleProperties,
+
+ // eIncrementFontSize indicates to increment font-size.
+ eIncrementFontSize,
+
+ // eDecrementFontSize indicates to decrement font-size.
+ eDecrementFontSize,
+
+ // eSetAlignment indicates to set alignment of selected content but different
+ // from the following.
+ eSetAlignment,
+
+ // eAlign* and eJustify indicates to align contents in block with left
+ // edge, right edge, center or justify the text.
+ eAlignLeft,
+ eAlignRight,
+ eAlignCenter,
+ eJustify,
+
+ // eSetBackgroundColor indicates to set background color.
+ eSetBackgroundColor,
+
+ // eSetPositionToAbsoluteOrStatic indicates to set position property value
+ // to "absolute" or "static".
+ eSetPositionToAbsoluteOrStatic,
+
+ // eIncreaseOrDecreaseZIndex indicates to change z-index of an element.
+ eIncreaseOrDecreaseZIndex,
+
+ // eEnableOrDisableCSS indicates to enable or disable CSS mode of HTMLEditor.
+ eEnableOrDisableCSS,
+
+ // eEnableOrDisableAbsolutePositionEditor indicates to enable or disable
+ // absolute positioned element editing UI.
+ eEnableOrDisableAbsolutePositionEditor,
+
+ // eEnableOrDisableResizer indicates to enable or disable resizers of
+ // <img>, <table> and absolutely positioned element.
+ eEnableOrDisableResizer,
+
+ // eEnableOrDisableInlineTableEditingUI indicates to enable or disable
+ // inline table editing UI.
+ eEnableOrDisableInlineTableEditingUI,
+
+ // eSetCharacterSet indicates to set character-set of the document.
+ eSetCharacterSet,
+
+ // eSetWrapWidth indicates to set wrap width.
+ eSetWrapWidth,
+
+ // eRewrap indicates to rewrap for current wrap width.
+ eRewrap,
+
+ // eSetText indicates to set new text of TextEditor, e.g., setting
+ // HTMLInputElement.value.
+ eSetText,
+
+ // eSetHTML indicates to set body of HTMLEditor.
+ eSetHTML,
+
+ // eInsertHTML indicates to insert HTML source code.
+ eInsertHTML,
+
+ // eHidePassword indicates that editor hides password with mask characters.
+ eHidePassword,
+
+ // eCreatePaddingBRElementForEmptyEditor indicates that editor wants to
+ // create a padding <br> element for empty editor after it modifies its
+ // content.
+ eCreatePaddingBRElementForEmptyEditor,
+};
+
+// This is int32_t instead of int16_t because nsIInlineSpellChecker.idl's
+// spellCheckAfterEditorChange is defined to take it as a long.
+// TODO: Make each name eFoo and investigate whether the numeric values
+// still have some meaning.
+enum class EditSubAction : int32_t {
+ // eNone indicates not edit sub-action is being handled. This is useful
+ // of initial value of member variables.
+ eNone,
+
+ // eUndo and eRedo indicate entire actions of undo/redo operation.
+ eUndo,
+ eRedo,
+
+ // eInsertNode indicates to insert a new node into the DOM tree.
+ eInsertNode,
+
+ // eCreateNode indicates to create a new node and insert it into the DOM tree.
+ eCreateNode,
+
+ // eDeleteNode indicates to remove a node from the DOM tree.
+ eDeleteNode,
+
+ // eMoveNode indicates to move a node connected in the DOM tree to different
+ // place.
+ eMoveNode,
+
+ // eSplitNode indicates to split a node to 2 nodes.
+ eSplitNode,
+
+ // eJoinNodes indicates to join 2 nodes.
+ eJoinNodes,
+
+ // eDeleteText indicates to delete some characters form a text node.
+ eDeleteText,
+
+ // eInsertText indicates to insert some characters.
+ eInsertText,
+
+ // eInsertTextComingFromIME indicates to insert or update composition string
+ // with new text which is new composition string or commit string.
+ eInsertTextComingFromIME,
+
+ // eDeleteSelectedContent indicates to remove selected content.
+ eDeleteSelectedContent,
+
+ // eSetTextProperty indicates to set a style from text.
+ eSetTextProperty,
+
+ // eRemoveTextProperty indicates to remove a style from text.
+ eRemoveTextProperty,
+
+ // eRemoveAllTextProperties indicate to remove all styles from text.
+ eRemoveAllTextProperties,
+
+ // eComputeTextToOutput indicates to compute the editor value as plain text
+ // or something requested format.
+ eComputeTextToOutput,
+
+ // eSetText indicates to set editor value to new value.
+ eSetText,
+
+ // eInsertLineBreak indicates to insert a line break, <br> or \n to break
+ // current line.
+ eInsertLineBreak,
+
+ // eInsertParagraphSeparator indicates to insert paragraph separator, <br> or
+ // \n at least to break current line in HTMLEditor.
+ eInsertParagraphSeparator,
+
+ // eCreateOrChangeList indicates to create new list or change existing list
+ // type.
+ eCreateOrChangeList,
+
+ // eIndent and eOutdent indicates to indent or outdent the target with
+ // using <blockquote>, <ul>, <ol> or just margin of start edge.
+ eIndent,
+ eOutdent,
+
+ // eSetOrClearAlignment aligns content or clears alignment with align
+ // attribute or text-align.
+ eSetOrClearAlignment,
+
+ // eCreateOrRemoveBlock creates new block or removes existing block and
+ // move its descendants to where the block was.
+ eCreateOrRemoveBlock,
+
+ // eFormatBlockForHTMLCommand wraps selected lines into format block, replaces
+ // format blocks around selection with new format block or deletes format
+ // blocks around selection.
+ eFormatBlockForHTMLCommand,
+
+ // eMergeBlockContents is not an actual sub-action, but this is used by
+ // HTMLEditor::MoveBlock() to request special handling in
+ // HTMLEditor::MaybeSplitElementsAtEveryBRElement().
+ eMergeBlockContents,
+
+ // eRemoveList removes specific type of list but keep its content.
+ eRemoveList,
+
+ // eCreateOrChangeDefinitionListItem indicates to format current hard line(s)
+ // `<dd>` or `<dt>`. This may cause creating or changing existing list
+ // element to new `<dl>` element.
+ eCreateOrChangeDefinitionListItem,
+
+ // eInsertElement indicates to insert an element.
+ eInsertElement,
+
+ // eInsertQuotation indicates to insert an element and make it "quoted text".
+ eInsertQuotation,
+
+ // eInsertQuotedText indicates to insert text which has already been quoted.
+ eInsertQuotedText,
+
+ // ePasteHTMLContent indicates to paste HTML content in clipboard.
+ ePasteHTMLContent,
+
+ // eInsertHTMLSource indicates to create a document fragment from given HTML
+ // source and insert into the DOM tree. So, this is similar to innerHTML.
+ eInsertHTMLSource,
+
+ // eReplaceHeadWithHTMLSource indicates to create a document fragment from
+ // given HTML source and replace content of <head> with it.
+ eReplaceHeadWithHTMLSource,
+
+ // eSetPositionToAbsolute and eSetPositionToStatic indicates to set position
+ // property to absolute or static.
+ eSetPositionToAbsolute,
+ eSetPositionToStatic,
+
+ // eDecreaseZIndex and eIncreaseZIndex indicate to decrease and increase
+ // z-index value.
+ eDecreaseZIndex,
+ eIncreaseZIndex,
+
+ // eCreatePaddingBRElementForEmptyEditor indicates to create a padding <br>
+ // element for empty editor.
+ eCreatePaddingBRElementForEmptyEditor,
+};
+
+// You can use this macro as:
+// case NS_EDIT_ACTION_CASES_ACCESSING_TABLE_DATA_WITHOUT_EDITING:
+// clang-format off
+#define NS_EDIT_ACTION_CASES_ACCESSING_TABLE_DATA_WITHOUT_EDITING \
+ mozilla::EditAction::eSelectTableCell: \
+ case mozilla::EditAction::eSelectTableRow: \
+ case mozilla::EditAction::eSelectTableColumn: \
+ case mozilla::EditAction::eSelectTable: \
+ case mozilla::EditAction::eSelectAllTableCells: \
+ case mozilla::EditAction::eGetCellIndexes: \
+ case mozilla::EditAction::eGetTableSize: \
+ case mozilla::EditAction::eGetCellAt: \
+ case mozilla::EditAction::eGetCellDataAt: \
+ case mozilla::EditAction::eGetFirstRow: \
+ case mozilla::EditAction::eGetSelectedOrParentTableElement: \
+ case mozilla::EditAction::eGetSelectedCellsType: \
+ case mozilla::EditAction::eGetFirstSelectedCellInTable: \
+ case mozilla::EditAction::eGetSelectedCells
+// clang-format on
+
+inline EditorInputType ToInputType(EditAction aEditAction) {
+ switch (aEditAction) {
+ case EditAction::eInsertText:
+ return EditorInputType::eInsertText;
+ case EditAction::eReplaceText:
+ return EditorInputType::eInsertReplacementText;
+ case EditAction::eInsertLineBreak:
+ return EditorInputType::eInsertLineBreak;
+ case EditAction::eInsertParagraphSeparator:
+ return EditorInputType::eInsertParagraph;
+ case EditAction::eInsertOrderedListElement:
+ case EditAction::eRemoveOrderedListElement:
+ return EditorInputType::eInsertOrderedList;
+ case EditAction::eInsertUnorderedListElement:
+ case EditAction::eRemoveUnorderedListElement:
+ return EditorInputType::eInsertUnorderedList;
+ case EditAction::eInsertHorizontalRuleElement:
+ return EditorInputType::eInsertHorizontalRule;
+ case EditAction::eDrop:
+ return EditorInputType::eInsertFromDrop;
+ case EditAction::ePaste:
+ return EditorInputType::eInsertFromPaste;
+ case EditAction::ePasteAsQuotation:
+ return EditorInputType::eInsertFromPasteAsQuotation;
+ case EditAction::eUpdateComposition:
+ return EditorInputType::eInsertCompositionText;
+ case EditAction::eCommitComposition:
+ if (StaticPrefs::dom_input_events_conform_to_level_1()) {
+ return EditorInputType::eInsertCompositionText;
+ }
+ return EditorInputType::eInsertFromComposition;
+ case EditAction::eCancelComposition:
+ if (StaticPrefs::dom_input_events_conform_to_level_1()) {
+ return EditorInputType::eInsertCompositionText;
+ }
+ return EditorInputType::eDeleteCompositionText;
+ case EditAction::eDeleteByComposition:
+ if (StaticPrefs::dom_input_events_conform_to_level_1()) {
+ // XXX Or EditorInputType::eDeleteContent? I don't know which IME may
+ // causes this situation.
+ return EditorInputType::eInsertCompositionText;
+ }
+ return EditorInputType::eDeleteByComposition;
+ case EditAction::eInsertLinkElement:
+ return EditorInputType::eInsertLink;
+ case EditAction::eDeleteWordBackward:
+ return EditorInputType::eDeleteWordBackward;
+ case EditAction::eDeleteWordForward:
+ return EditorInputType::eDeleteWordForward;
+ case EditAction::eDeleteToBeginningOfSoftLine:
+ return EditorInputType::eDeleteSoftLineBackward;
+ case EditAction::eDeleteToEndOfSoftLine:
+ return EditorInputType::eDeleteSoftLineForward;
+ case EditAction::eDeleteByDrag:
+ return EditorInputType::eDeleteByDrag;
+ case EditAction::eCut:
+ return EditorInputType::eDeleteByCut;
+ case EditAction::eDeleteSelection:
+ case EditAction::eRemoveTableRowElement:
+ case EditAction::eRemoveTableColumn:
+ case EditAction::eRemoveTableElement:
+ case EditAction::eDeleteTableCellContents:
+ case EditAction::eRemoveTableCellElement:
+ return EditorInputType::eDeleteContent;
+ case EditAction::eDeleteBackward:
+ return EditorInputType::eDeleteContentBackward;
+ case EditAction::eDeleteForward:
+ return EditorInputType::eDeleteContentForward;
+ case EditAction::eUndo:
+ return EditorInputType::eHistoryUndo;
+ case EditAction::eRedo:
+ return EditorInputType::eHistoryRedo;
+ case EditAction::eSetFontWeightProperty:
+ case EditAction::eRemoveFontWeightProperty:
+ return EditorInputType::eFormatBold;
+ case EditAction::eSetTextStyleProperty:
+ case EditAction::eRemoveTextStyleProperty:
+ return EditorInputType::eFormatItalic;
+ case EditAction::eSetTextDecorationPropertyUnderline:
+ case EditAction::eRemoveTextDecorationPropertyUnderline:
+ return EditorInputType::eFormatUnderline;
+ case EditAction::eSetTextDecorationPropertyLineThrough:
+ case EditAction::eRemoveTextDecorationPropertyLineThrough:
+ return EditorInputType::eFormatStrikeThrough;
+ case EditAction::eSetVerticalAlignPropertySuper:
+ case EditAction::eRemoveVerticalAlignPropertySuper:
+ return EditorInputType::eFormatSuperscript;
+ case EditAction::eSetVerticalAlignPropertySub:
+ case EditAction::eRemoveVerticalAlignPropertySub:
+ return EditorInputType::eFormatSubscript;
+ case EditAction::eJustify:
+ return EditorInputType::eFormatJustifyFull;
+ case EditAction::eAlignCenter:
+ return EditorInputType::eFormatJustifyCenter;
+ case EditAction::eAlignRight:
+ return EditorInputType::eFormatJustifyRight;
+ case EditAction::eAlignLeft:
+ return EditorInputType::eFormatJustifyLeft;
+ case EditAction::eIndent:
+ return EditorInputType::eFormatIndent;
+ case EditAction::eOutdent:
+ return EditorInputType::eFormatOutdent;
+ case EditAction::eRemoveAllInlineStyleProperties:
+ return EditorInputType::eFormatRemove;
+ case EditAction::eSetTextDirection:
+ return EditorInputType::eFormatSetBlockTextDirection;
+ case EditAction::eSetBackgroundColorPropertyInline:
+ case EditAction::eRemoveBackgroundColorPropertyInline:
+ return EditorInputType::eFormatBackColor;
+ case EditAction::eSetColorProperty:
+ case EditAction::eRemoveColorProperty:
+ return EditorInputType::eFormatFontColor;
+ case EditAction::eSetFontFamilyProperty:
+ case EditAction::eRemoveFontFamilyProperty:
+ return EditorInputType::eFormatFontName;
+ default:
+ return EditorInputType::eUnknown;
+ }
+}
+
+inline bool MayEditActionDeleteAroundCollapsedSelection(
+ const EditAction aEditAction) {
+ switch (aEditAction) {
+ case EditAction::eDeleteSelection:
+ case EditAction::eDeleteBackward:
+ case EditAction::eDeleteForward:
+ case EditAction::eDeleteWordBackward:
+ case EditAction::eDeleteWordForward:
+ case EditAction::eDeleteToBeginningOfSoftLine:
+ case EditAction::eDeleteToEndOfSoftLine:
+ return true;
+ default:
+ return false;
+ }
+}
+
+inline bool IsEditActionInOrderToEditSomething(const EditAction aEditAction) {
+ switch (aEditAction) {
+ case EditAction::eNotEditing:
+ case NS_EDIT_ACTION_CASES_ACCESSING_TABLE_DATA_WITHOUT_EDITING:
+ return false;
+ default:
+ return true;
+ }
+}
+
+inline bool IsEditActionTableEditing(const EditAction aEditAction) {
+ switch (aEditAction) {
+ case EditAction::eInsertTableRowElement:
+ case EditAction::eRemoveTableRowElement:
+ case EditAction::eInsertTableColumn:
+ case EditAction::eRemoveTableColumn:
+ case EditAction::eRemoveTableElement:
+ case EditAction::eRemoveTableCellElement:
+ case EditAction::eDeleteTableCellContents:
+ case EditAction::eInsertTableCellElement:
+ case EditAction::eJoinTableCellElements:
+ case EditAction::eSplitTableCellElement:
+ case EditAction::eSetTableCellElementType:
+ return true;
+ default:
+ return false;
+ }
+}
+
+inline bool MayEditActionDeleteSelection(const EditAction aEditAction) {
+ switch (aEditAction) {
+ case EditAction::eNone:
+ case EditAction::eNotEditing:
+ case EditAction::eInitializing:
+ case NS_EDIT_ACTION_CASES_ACCESSING_TABLE_DATA_WITHOUT_EDITING:
+ return false;
+
+ // EditActions modifying around selection.
+ case EditAction::eInsertText:
+ case EditAction::eInsertParagraphSeparator:
+ case EditAction::eInsertLineBreak:
+ case EditAction::eDeleteSelection:
+ case EditAction::eDeleteBackward:
+ case EditAction::eDeleteForward:
+ case EditAction::eDeleteWordBackward:
+ case EditAction::eDeleteWordForward:
+ case EditAction::eDeleteToBeginningOfSoftLine:
+ case EditAction::eDeleteToEndOfSoftLine:
+ case EditAction::eDeleteByDrag:
+ return true;
+
+ case EditAction::eStartComposition:
+ return false;
+
+ case EditAction::eUpdateComposition:
+ case EditAction::eCommitComposition:
+ case EditAction::eCancelComposition:
+ case EditAction::eDeleteByComposition:
+ return true;
+
+ case EditAction::eUndo:
+ case EditAction::eRedo:
+ case EditAction::eSetTextDirection:
+ return false;
+
+ case EditAction::eCut:
+ return true;
+
+ case EditAction::eCopy:
+ return false;
+
+ case EditAction::ePaste:
+ case EditAction::ePasteAsQuotation:
+ return true;
+
+ case EditAction::eDrop:
+ return false; // Not deleting selection at drop.
+
+ // EditActions changing format around selection.
+ case EditAction::eIndent:
+ case EditAction::eOutdent:
+ return false;
+
+ // EditActions inserting or deleting something at specified position.
+ case EditAction::eInsertTableRowElement:
+ case EditAction::eRemoveTableRowElement:
+ case EditAction::eInsertTableColumn:
+ case EditAction::eRemoveTableColumn:
+ case EditAction::eResizingElement:
+ case EditAction::eResizeElement:
+ case EditAction::eMovingElement:
+ case EditAction::eMoveElement:
+ case EditAction::eUnknown:
+ case EditAction::eSetAttribute:
+ case EditAction::eRemoveAttribute:
+ case EditAction::eRemoveNode:
+ case EditAction::eInsertBlockElement:
+ return false;
+
+ // EditActions inserting someting around selection or replacing selection
+ // with something.
+ case EditAction::eReplaceText:
+ case EditAction::eInsertNode:
+ case EditAction::eInsertHorizontalRuleElement:
+ return true;
+
+ // EditActions changing format around selection or inserting or deleting
+ // something at specific position.
+ case EditAction::eInsertLinkElement:
+ case EditAction::eInsertUnorderedListElement:
+ case EditAction::eInsertOrderedListElement:
+ case EditAction::eRemoveUnorderedListElement:
+ case EditAction::eRemoveOrderedListElement:
+ case EditAction::eRemoveListElement:
+ case EditAction::eInsertBlockquoteElement:
+ case EditAction::eNormalizeTable:
+ case EditAction::eRemoveTableElement:
+ case EditAction::eRemoveTableCellElement:
+ case EditAction::eDeleteTableCellContents:
+ case EditAction::eInsertTableCellElement:
+ case EditAction::eJoinTableCellElements:
+ case EditAction::eSplitTableCellElement:
+ case EditAction::eSetTableCellElementType:
+ case EditAction::eSetInlineStyleProperty:
+ case EditAction::eRemoveInlineStyleProperty:
+ case EditAction::eSetFontWeightProperty:
+ case EditAction::eRemoveFontWeightProperty:
+ case EditAction::eSetTextStyleProperty:
+ case EditAction::eRemoveTextStyleProperty:
+ case EditAction::eSetTextDecorationPropertyUnderline:
+ case EditAction::eRemoveTextDecorationPropertyUnderline:
+ case EditAction::eSetTextDecorationPropertyLineThrough:
+ case EditAction::eRemoveTextDecorationPropertyLineThrough:
+ case EditAction::eSetVerticalAlignPropertySuper:
+ case EditAction::eRemoveVerticalAlignPropertySuper:
+ case EditAction::eSetVerticalAlignPropertySub:
+ case EditAction::eRemoveVerticalAlignPropertySub:
+ case EditAction::eSetFontFamilyProperty:
+ case EditAction::eRemoveFontFamilyProperty:
+ case EditAction::eSetColorProperty:
+ case EditAction::eRemoveColorProperty:
+ case EditAction::eSetBackgroundColorPropertyInline:
+ case EditAction::eRemoveBackgroundColorPropertyInline:
+ case EditAction::eRemoveAllInlineStyleProperties:
+ case EditAction::eIncrementFontSize:
+ case EditAction::eDecrementFontSize:
+ case EditAction::eSetAlignment:
+ case EditAction::eAlignLeft:
+ case EditAction::eAlignRight:
+ case EditAction::eAlignCenter:
+ case EditAction::eJustify:
+ case EditAction::eSetBackgroundColor:
+ case EditAction::eSetPositionToAbsoluteOrStatic:
+ case EditAction::eIncreaseOrDecreaseZIndex:
+ return false;
+
+ // EditActions controlling editor feature or state.
+ case EditAction::eEnableOrDisableCSS:
+ case EditAction::eEnableOrDisableAbsolutePositionEditor:
+ case EditAction::eEnableOrDisableResizer:
+ case EditAction::eEnableOrDisableInlineTableEditingUI:
+ case EditAction::eSetCharacterSet:
+ case EditAction::eSetWrapWidth:
+ return false;
+
+ case EditAction::eRewrap:
+ case EditAction::eSetText:
+ case EditAction::eSetHTML:
+ case EditAction::eInsertHTML:
+ return true;
+
+ case EditAction::eHidePassword:
+ case EditAction::eCreatePaddingBRElementForEmptyEditor:
+ return false;
+ }
+ return false;
+}
+
+inline bool MayEditActionRequireLayout(const EditAction aEditAction) {
+ switch (aEditAction) {
+ // Table editing require layout information for referring table cell data
+ // such as row/column number and rowspan/colspan.
+ case EditAction::eInsertTableRowElement:
+ case EditAction::eRemoveTableRowElement:
+ case EditAction::eInsertTableColumn:
+ case EditAction::eRemoveTableColumn:
+ case EditAction::eRemoveTableElement:
+ case EditAction::eRemoveTableCellElement:
+ case EditAction::eDeleteTableCellContents:
+ case EditAction::eInsertTableCellElement:
+ case EditAction::eJoinTableCellElements:
+ case EditAction::eSplitTableCellElement:
+ case EditAction::eSetTableCellElementType:
+ case NS_EDIT_ACTION_CASES_ACCESSING_TABLE_DATA_WITHOUT_EDITING:
+ return true;
+ default:
+ return false;
+ }
+}
+
+} // namespace mozilla
+
+inline bool operator!(const mozilla::EditSubAction& aEditSubAction) {
+ return aEditSubAction == mozilla::EditSubAction::eNone;
+}
+
+#endif // #ifdef mozilla_EditAction_h
diff --git a/editor/libeditor/EditAggregateTransaction.cpp b/editor/libeditor/EditAggregateTransaction.cpp
new file mode 100644
index 0000000000..d5be20ac00
--- /dev/null
+++ b/editor/libeditor/EditAggregateTransaction.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditAggregateTransaction.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/ReverseIterator.h" // for Reversed
+#include "nsAString.h"
+#include "nsAtom.h"
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsError.h" // for NS_OK, etc.
+#include "nsGkAtoms.h"
+#include "nsISupportsUtils.h" // for NS_ADDREF
+#include "nsString.h" // for nsAutoString
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(EditAggregateTransaction,
+ EditTransactionBase, mChildren)
+
+NS_IMPL_ADDREF_INHERITED(EditAggregateTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(EditAggregateTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditAggregateTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP EditAggregateTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p EditAggregateTransaction::%s this={ mName=%s, mChildren=%zu } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get(),
+ mChildren.Length()));
+ // FYI: It's legal (but not very useful) to have an empty child list.
+ for (const OwningNonNull<EditTransactionBase>& childTransaction :
+ CopyableAutoTArray<OwningNonNull<EditTransactionBase>, 10>(mChildren)) {
+ nsresult rv = MOZ_KnownLive(childTransaction)->DoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditTransactionBase::DoTransaction() failed");
+ return rv;
+ }
+ }
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p EditAggregateTransaction::%s this={ mName=%s } "
+ "End================================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditAggregateTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p EditAggregateTransaction::%s this={ mName=%s, mChildren=%zu } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get(),
+ mChildren.Length()));
+ // FYI: It's legal (but not very useful) to have an empty child list.
+ // Undo goes through children backwards.
+ const CopyableAutoTArray<OwningNonNull<EditTransactionBase>, 10> children(
+ mChildren);
+ for (const OwningNonNull<EditTransactionBase>& childTransaction :
+ Reversed(children)) {
+ nsresult rv = MOZ_KnownLive(childTransaction)->UndoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditTransactionBase::UndoTransaction() failed");
+ return rv;
+ }
+ }
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p EditAggregateTransaction::%s this={ mName=%s } "
+ "End================================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditAggregateTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p EditAggregateTransaction::%s this={ mName=%s, mChildren=%zu } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get(),
+ mChildren.Length()));
+ // It's legal (but not very useful) to have an empty child list.
+ const CopyableAutoTArray<OwningNonNull<EditTransactionBase>, 10> children(
+ mChildren);
+ for (const OwningNonNull<EditTransactionBase>& childTransaction : children) {
+ nsresult rv = MOZ_KnownLive(childTransaction)->RedoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditTransactionBase::RedoTransaction() failed");
+ return rv;
+ }
+ }
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p EditAggregateTransaction::%s this={ mName=%s } "
+ "End================================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditAggregateTransaction::Merge(nsITransaction* aOtherTransaction,
+ bool* aDidMerge) {
+ if (aDidMerge) {
+ *aDidMerge = false;
+ }
+ if (mChildren.IsEmpty()) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p EditAggregateTransaction::%s this={ mName=%s } returned false "
+ "due to no children",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+ }
+ // FIXME: Is this really intended not to loop? It looks like the code
+ // that used to be here sort of intended to loop, but didn't.
+ return mChildren[0]->Merge(aOtherTransaction, aDidMerge);
+}
+
+nsAtom* EditAggregateTransaction::GetName() const { return mName; }
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditAggregateTransaction.h b/editor/libeditor/EditAggregateTransaction.h
new file mode 100644
index 0000000000..2017d4f7ff
--- /dev/null
+++ b/editor/libeditor/EditAggregateTransaction.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EditAggregateTransaction_h
+#define EditAggregateTransaction_h
+
+#include "EditTransactionBase.h"
+
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/RefPtr.h"
+
+#include "nsAtom.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTArray.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+/**
+ * base class for all document editing transactions that require aggregation.
+ * provides a list of child transactions.
+ */
+class EditAggregateTransaction : public EditTransactionBase {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(EditAggregateTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+ NS_IMETHOD Merge(nsITransaction* aOtherTransaction, bool* aDidMerge) override;
+
+ /**
+ * Get the name assigned to this transaction.
+ */
+ nsAtom* GetName() const;
+
+ const nsTArray<OwningNonNull<EditTransactionBase>>& ChildTransactions()
+ const {
+ return mChildren;
+ }
+
+ protected:
+ EditAggregateTransaction() = default;
+ virtual ~EditAggregateTransaction() = default;
+
+ nsTArray<OwningNonNull<EditTransactionBase>> mChildren;
+ RefPtr<nsAtom> mName;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef EditAggregateTransaction_h
diff --git a/editor/libeditor/EditTransactionBase.cpp b/editor/libeditor/EditTransactionBase.cpp
new file mode 100644
index 0000000000..d2e286c656
--- /dev/null
+++ b/editor/libeditor/EditTransactionBase.cpp
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditTransactionBase.h"
+
+#include "mozilla/Logging.h"
+
+#include "ChangeAttributeTransaction.h"
+#include "ChangeStyleTransaction.h"
+#include "CompositionTransaction.h"
+#include "DeleteContentTransactionBase.h"
+#include "DeleteMultipleRangesTransaction.h"
+#include "DeleteNodeTransaction.h"
+#include "DeleteRangeTransaction.h"
+#include "DeleteTextTransaction.h"
+#include "EditAggregateTransaction.h"
+#include "InsertNodeTransaction.h"
+#include "InsertTextTransaction.h"
+#include "JoinNodesTransaction.h"
+#include "MoveNodeTransaction.h"
+#include "PlaceholderTransaction.h"
+#include "ReplaceTextTransaction.h"
+#include "SplitNodeTransaction.h"
+
+#include "nsError.h"
+#include "nsISupports.h"
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(EditTransactionBase)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_0(EditTransactionBase)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditTransactionBase)
+ // We don't have anything to traverse, but some of our subclasses do.
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditTransactionBase)
+ NS_INTERFACE_MAP_ENTRY(nsITransaction)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITransaction)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(EditTransactionBase)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(EditTransactionBase)
+
+NS_IMETHODIMP EditTransactionBase::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info, ("%p %s", this, __FUNCTION__));
+ return DoTransaction();
+}
+
+NS_IMETHODIMP EditTransactionBase::GetIsTransient(bool* aIsTransient) {
+ MOZ_LOG(GetLogModule(), LogLevel::Verbose,
+ ("%p %s returned false", this, __FUNCTION__));
+ *aIsTransient = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditTransactionBase::Merge(nsITransaction* aOtherTransaction,
+ bool* aDidMerge) {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p %s(aOtherTransaction=%p) returned false", this, __FUNCTION__,
+ aOtherTransaction));
+ *aDidMerge = false;
+ return NS_OK;
+}
+
+// static
+LogModule* EditTransactionBase::GetLogModule() {
+ static LazyLogModule sLog("EditorTransaction");
+ return static_cast<LogModule*>(sLog);
+}
+
+#define NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(aClass) \
+ aClass* EditTransactionBase::GetAs##aClass() { return nullptr; } \
+ const aClass* EditTransactionBase::GetAs##aClass() const { return nullptr; }
+
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(ChangeAttributeTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(ChangeStyleTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(CompositionTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(DeleteContentTransactionBase)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(DeleteMultipleRangesTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(DeleteNodeTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(DeleteRangeTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(DeleteTextTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(EditAggregateTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(InsertNodeTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(InsertTextTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(JoinNodesTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(MoveNodeTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(PlaceholderTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(ReplaceTextTransaction)
+NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS(SplitNodeTransaction)
+
+#undef NS_IMPL_EDITTRANSACTIONBASE_GETASMETHODS
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditTransactionBase.h b/editor/libeditor/EditTransactionBase.h
new file mode 100644
index 0000000000..50666bd577
--- /dev/null
+++ b/editor/libeditor/EditTransactionBase.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditTransactionBase_h
+#define mozilla_EditTransactionBase_h
+
+#include "mozilla/EditorForwards.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nsITransaction.h"
+#include "nscore.h"
+
+already_AddRefed<mozilla::EditTransactionBase>
+nsITransaction::GetAsEditTransactionBase() {
+ RefPtr<mozilla::EditTransactionBase> editTransactionBase;
+ return NS_SUCCEEDED(
+ GetAsEditTransactionBase(getter_AddRefs(editTransactionBase)))
+ ? editTransactionBase.forget()
+ : nullptr;
+}
+
+namespace mozilla {
+class LogModule;
+
+#define NS_DECL_GETASTRANSACTION_BASE(aClass) \
+ virtual aClass* GetAs##aClass(); \
+ virtual const aClass* GetAs##aClass() const;
+
+/**
+ * Base class for all document editing transactions.
+ */
+class EditTransactionBase : public nsITransaction {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(EditTransactionBase, nsITransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction(void) override;
+ NS_IMETHOD GetIsTransient(bool* aIsTransient) override;
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override;
+ NS_IMETHOD GetAsEditTransactionBase(
+ EditTransactionBase** aEditTransactionBase) final {
+ MOZ_ASSERT(aEditTransactionBase);
+ MOZ_ASSERT(!*aEditTransactionBase);
+ *aEditTransactionBase = do_AddRef(this).take();
+ return NS_OK;
+ }
+
+ NS_DECL_GETASTRANSACTION_BASE(ChangeAttributeTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(ChangeStyleTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(CompositionTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(DeleteContentTransactionBase)
+ NS_DECL_GETASTRANSACTION_BASE(DeleteMultipleRangesTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(DeleteNodeTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(DeleteRangeTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(DeleteTextTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(EditAggregateTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(InsertNodeTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(InsertTextTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(JoinNodesTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(MoveNodeTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(PlaceholderTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(ReplaceTextTransaction)
+ NS_DECL_GETASTRANSACTION_BASE(SplitNodeTransaction)
+
+ protected:
+ virtual ~EditTransactionBase() = default;
+
+ static LogModule* GetLogModule();
+};
+
+#undef NS_DECL_GETASTRANSACTION_BASE
+
+} // namespace mozilla
+
+#define NS_DECL_EDITTRANSACTIONBASE \
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD DoTransaction() override; \
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD UndoTransaction() override;
+
+#define NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(aClass) \
+ aClass* GetAs##aClass() final { return this; } \
+ const aClass* GetAs##aClass() const final { return this; }
+
+#endif // #ifndef mozilla_EditTransactionBase_h
diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp
new file mode 100644
index 0000000000..22452c59d2
--- /dev/null
+++ b/editor/libeditor/EditorBase.cpp
@@ -0,0 +1,6908 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditorBase.h"
+
+#include <stdio.h> // for nullptr, stdout
+#include <string.h> // for strcmp
+
+#include "AutoRangeArray.h" // for AutoRangeArray
+#include "ChangeAttributeTransaction.h"
+#include "CompositionTransaction.h"
+#include "DeleteContentTransactionBase.h"
+#include "DeleteMultipleRangesTransaction.h"
+#include "DeleteNodeTransaction.h"
+#include "DeleteRangeTransaction.h"
+#include "DeleteTextTransaction.h"
+#include "EditAction.h" // for EditSubAction
+#include "EditorDOMPoint.h" // for EditorDOMPoint
+#include "EditorUtils.h" // for various helper classes.
+#include "EditTransactionBase.h" // for EditTransactionBase
+#include "EditorEventListener.h" // for EditorEventListener
+#include "HTMLEditor.h" // for HTMLEditor
+#include "HTMLEditorInlines.h"
+#include "HTMLEditUtils.h" // for HTMLEditUtils
+#include "InsertNodeTransaction.h" // for InsertNodeTransaction
+#include "InsertTextTransaction.h" // for InsertTextTransaction
+#include "JoinNodesTransaction.h" // for JoinNodesTransaction
+#include "PlaceholderTransaction.h" // for PlaceholderTransaction
+#include "SplitNodeTransaction.h" // for SplitNodeTransaction
+#include "TextEditor.h" // for TextEditor
+
+#include "ErrorList.h"
+#include "gfxFontUtils.h" // for gfxFontUtils
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/BidiEmbeddingLevel.h"
+#include "mozilla/BasePrincipal.h" // for BasePrincipal
+#include "mozilla/CheckedInt.h" // for CheckedInt
+#include "mozilla/ComposerCommandsUpdater.h" // for ComposerCommandsUpdater
+#include "mozilla/ContentEvents.h" // for InternalClipboardEvent
+#include "mozilla/DebugOnly.h" // for DebugOnly
+#include "mozilla/EditorSpellCheck.h" // for EditorSpellCheck
+#include "mozilla/Encoding.h" // for Encoding (used in Document::GetDocumentCharacterSet)
+#include "mozilla/EventDispatcher.h" // for EventChainPreVisitor, etc.
+#include "mozilla/FlushType.h" // for FlushType::Frames
+#include "mozilla/IMEContentObserver.h" // for IMEContentObserver
+#include "mozilla/IMEStateManager.h" // for IMEStateManager
+#include "mozilla/InputEventOptions.h" // for InputEventOptions
+#include "mozilla/IntegerRange.h" // for IntegerRange
+#include "mozilla/InternalMutationEvent.h" // for NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED
+#include "mozilla/mozalloc.h" // for operator new, etc.
+#include "mozilla/mozInlineSpellChecker.h" // for mozInlineSpellChecker
+#include "mozilla/mozSpellChecker.h" // for mozSpellChecker
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/PresShell.h" // for PresShell
+#include "mozilla/RangeBoundary.h" // for RawRangeBoundary, RangeBoundary
+#include "mozilla/Services.h" // for GetObserverService
+#include "mozilla/StaticPrefs_bidi.h" // for StaticPrefs::bidi_*
+#include "mozilla/StaticPrefs_dom.h" // for StaticPrefs::dom_*
+#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
+#include "mozilla/StaticPrefs_layout.h" // for StaticPrefs::layout_*
+#include "mozilla/TextComposition.h" // for TextComposition
+#include "mozilla/TextControlElement.h" // for TextControlElement
+#include "mozilla/TextInputListener.h" // for TextInputListener
+#include "mozilla/TextServicesDocument.h" // for TextServicesDocument
+#include "mozilla/TextEvents.h"
+#include "mozilla/TransactionManager.h" // for TransactionManager
+#include "mozilla/dom/AbstractRange.h" // for AbstractRange
+#include "mozilla/dom/Attr.h" // for Attr
+#include "mozilla/dom/BrowsingContext.h" // for BrowsingContext
+#include "mozilla/dom/CharacterData.h" // for CharacterData
+#include "mozilla/dom/DataTransfer.h" // for DataTransfer
+#include "mozilla/dom/Document.h" // for Document
+#include "mozilla/dom/DocumentInlines.h" // for GetObservingPresShell
+#include "mozilla/dom/DragEvent.h" // for DragEvent
+#include "mozilla/dom/Element.h" // for Element, nsINode::AsElement
+#include "mozilla/dom/EventTarget.h" // for EventTarget
+#include "mozilla/dom/HTMLBodyElement.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/Selection.h" // for Selection, etc.
+#include "mozilla/dom/StaticRange.h" // for StaticRange
+#include "mozilla/dom/Text.h"
+#include "mozilla/dom/Event.h"
+#include "nsAString.h" // for nsAString::Length, etc.
+#include "nsCCUncollectableMarker.h" // for nsCCUncollectableMarker
+#include "nsCaret.h" // for nsCaret
+#include "nsCaseTreatment.h"
+#include "nsCharTraits.h" // for NS_IS_HIGH_SURROGATE, etc.
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsCopySupport.h" // for nsCopySupport
+#include "nsDOMString.h" // for DOMStringIsNull
+#include "nsDebug.h" // for NS_WARNING, etc.
+#include "nsError.h" // for NS_OK, etc.
+#include "nsFocusManager.h" // for nsFocusManager
+#include "nsFrameSelection.h" // for nsFrameSelection
+#include "nsGenericHTMLElement.h" // for nsGenericHTMLElement
+#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::dir
+#include "nsIClipboard.h" // for nsIClipboard
+#include "nsIContent.h" // for nsIContent
+#include "nsIContentInlines.h" // for nsINode::IsInDesignMode()
+#include "nsIDocumentEncoder.h" // for nsIDocumentEncoder
+#include "nsIDocumentStateListener.h" // for nsIDocumentStateListener
+#include "nsIDocShell.h" // for nsIDocShell
+#include "nsIEditActionListener.h" // for nsIEditActionListener
+#include "nsIFrame.h" // for nsIFrame
+#include "nsIInlineSpellChecker.h" // for nsIInlineSpellChecker, etc.
+#include "nsNameSpaceManager.h" // for kNameSpaceID_None, etc.
+#include "nsINode.h" // for nsINode, etc.
+#include "nsISelectionController.h" // for nsISelectionController, etc.
+#include "nsISelectionDisplay.h" // for nsISelectionDisplay, etc.
+#include "nsISupports.h" // for nsISupports
+#include "nsISupportsUtils.h" // for NS_ADDREF, NS_IF_ADDREF
+#include "nsITransferable.h" // for nsITransferable
+#include "nsIWeakReference.h" // for nsISupportsWeakReference
+#include "nsIWidget.h" // for nsIWidget, IMEState, etc.
+#include "nsPIDOMWindow.h" // for nsPIDOMWindow
+#include "nsPresContext.h" // for nsPresContext
+#include "nsRange.h" // for nsRange
+#include "nsReadableUtils.h" // for EmptyString, ToNewCString
+#include "nsString.h" // for nsAutoString, nsString, etc.
+#include "nsStringFwd.h" // for nsString
+#include "nsStyleConsts.h" // for StyleDirection::Rtl, etc.
+#include "nsStyleStruct.h" // for nsStyleDisplay, nsStyleText, etc.
+#include "nsStyleStructFwd.h" // for nsIFrame::StyleUIReset, etc.
+#include "nsTextNode.h" // for nsTextNode
+#include "nsThreadUtils.h" // for nsRunnable
+#include "prtime.h" // for PR_Now
+
+class nsIOutputStream;
+class nsITransferable;
+
+namespace mozilla {
+
+using namespace dom;
+using namespace widget;
+
+using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
+using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
+
+/*****************************************************************************
+ * mozilla::EditorBase
+ *****************************************************************************/
+template EditorDOMPoint EditorBase::GetFirstIMESelectionStartPoint() const;
+template EditorRawDOMPoint EditorBase::GetFirstIMESelectionStartPoint() const;
+template EditorDOMPoint EditorBase::GetLastIMESelectionEndPoint() const;
+template EditorRawDOMPoint EditorBase::GetLastIMESelectionEndPoint() const;
+
+template Result<CreateContentResult, nsresult>
+EditorBase::InsertNodeWithTransaction(nsIContent& aContentToInsert,
+ const EditorDOMPoint& aPointToInsert);
+template Result<CreateElementResult, nsresult>
+EditorBase::InsertNodeWithTransaction(Element& aContentToInsert,
+ const EditorDOMPoint& aPointToInsert);
+template Result<CreateTextResult, nsresult>
+EditorBase::InsertNodeWithTransaction(Text& aContentToInsert,
+ const EditorDOMPoint& aPointToInsert);
+
+template EditorDOMPoint EditorBase::GetFirstSelectionStartPoint() const;
+template EditorRawDOMPoint EditorBase::GetFirstSelectionStartPoint() const;
+template EditorDOMPoint EditorBase::GetFirstSelectionEndPoint() const;
+template EditorRawDOMPoint EditorBase::GetFirstSelectionEndPoint() const;
+
+template EditorBase::AutoCaretBidiLevelManager::AutoCaretBidiLevelManager(
+ const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aPointAtCaret);
+template EditorBase::AutoCaretBidiLevelManager::AutoCaretBidiLevelManager(
+ const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount,
+ const EditorRawDOMPoint& aPointAtCaret);
+
+EditorBase::EditorBase(EditorType aEditorType)
+ : mEditActionData(nullptr),
+ mPlaceholderName(nullptr),
+ mModCount(0),
+ mFlags(0),
+ mUpdateCount(0),
+ mPlaceholderBatch(0),
+ mNewlineHandling(StaticPrefs::editor_singleLine_pasteNewlines()),
+ mCaretStyle(StaticPrefs::layout_selection_caret_style()),
+ mDocDirtyState(-1),
+ mSpellcheckCheckboxState(eTriUnset),
+ mInitSucceeded(false),
+ mAllowsTransactionsToChangeSelection(true),
+ mDidPreDestroy(false),
+ mDidPostCreate(false),
+ mDispatchInputEvent(true),
+ mIsInEditSubAction(false),
+ mHidingCaret(false),
+ mSpellCheckerDictionaryUpdated(true),
+ mIsHTMLEditorClass(aEditorType == EditorType::HTML) {
+#ifdef XP_WIN
+ if (!mCaretStyle && !IsTextEditor()) {
+ // Wordpad-like caret behavior.
+ mCaretStyle = 1;
+ }
+#endif // #ifdef XP_WIN
+ if (mNewlineHandling < nsIEditor::eNewlinesPasteIntact ||
+ mNewlineHandling > nsIEditor::eNewlinesStripSurroundingWhitespace) {
+ mNewlineHandling = nsIEditor::eNewlinesPasteToFirst;
+ }
+}
+
+EditorBase::~EditorBase() {
+ MOZ_ASSERT(!IsInitialized() || mDidPreDestroy,
+ "Why PreDestroy hasn't been called?");
+
+ if (mComposition) {
+ mComposition->OnEditorDestroyed();
+ mComposition = nullptr;
+ }
+ // If this editor is still hiding the caret, we need to restore it.
+ HideCaret(false);
+ mTransactionManager = nullptr;
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(EditorBase)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EditorBase)
+ // Remove event listeners first since EditorEventListener may need
+ // mDocument, mEventTarget, etc.
+ if (tmp->mEventListener) {
+ tmp->mEventListener->Disconnect();
+ tmp->mEventListener = nullptr;
+ }
+
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectionController)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mIMEContentObserver)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mInlineSpellChecker)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextServicesDocument)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextInputListener)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransactionManager)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionListeners)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocStateListeners)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventTarget)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlaceholderTransaction)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mCachedDocumentEncoder)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditorBase)
+ Document* currentDoc =
+ tmp->mRootElement ? tmp->mRootElement->GetUncomposedDoc() : nullptr;
+ if (currentDoc && nsCCUncollectableMarker::InGeneration(
+ cb, currentDoc->GetMarkedCCGeneration())) {
+ return NS_SUCCESS_INTERRUPTED_TRAVERSE;
+ }
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionController)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIMEContentObserver)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineSpellChecker)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextServicesDocument)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextInputListener)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransactionManager)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionListeners)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocStateListeners)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventTarget)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventListener)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlaceholderTransaction)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedDocumentEncoder)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditorBase)
+ NS_INTERFACE_MAP_ENTRY(nsISelectionListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIEditor)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditor)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorBase)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorBase)
+
+nsresult EditorBase::InitInternal(Document& aDocument, Element* aRootElement,
+ nsISelectionController& aSelectionController,
+ uint32_t aFlags) {
+ MOZ_ASSERT_IF(
+ !mEditActionData ||
+ !mEditActionData->HasEditorDestroyedDuringHandlingEditAction(),
+ GetTopLevelEditSubAction() == EditSubAction::eNone);
+
+ // First only set flags, but other stuff shouldn't be initialized now.
+ // Note that SetFlags() will be called by PostCreate().
+ mFlags = aFlags;
+
+ mDocument = &aDocument;
+ // nsISelectionController should be stored only when we're a `TextEditor`.
+ // Otherwise, in `HTMLEditor`, it's `PresShell`, and grabbing it causes
+ // a circular reference and memory leak.
+ // XXX Should we move `mSelectionController to `TextEditor`?
+ MOZ_ASSERT_IF(!IsTextEditor(), &aSelectionController == GetPresShell());
+ if (IsTextEditor()) {
+ MOZ_ASSERT(&aSelectionController != GetPresShell());
+ mSelectionController = &aSelectionController;
+ }
+
+ if (mEditActionData) {
+ // During edit action, selection is cached. But this selection is invalid
+ // now since selection controller is updated, so we have to update this
+ // cache.
+ Selection* selection = aSelectionController.GetSelection(
+ nsISelectionController::SELECTION_NORMAL);
+ NS_WARNING_ASSERTION(selection,
+ "SelectionController::GetSelection() failed");
+ if (selection) {
+ mEditActionData->UpdateSelectionCache(*selection);
+ }
+ }
+
+ // set up root element if we are passed one.
+ if (aRootElement) {
+ mRootElement = aRootElement;
+ }
+
+ // If this is an editor for <input> or <textarea>, the text node which
+ // has composition string is always recreated with same content. Therefore,
+ // we need to nodify mComposition of text node destruction and replacing
+ // composing string when this receives eCompositionChange event next time.
+ if (mComposition && mComposition->GetContainerTextNode() &&
+ !mComposition->GetContainerTextNode()->IsInComposedDoc()) {
+ mComposition->OnTextNodeRemoved();
+ }
+
+ // Show the caret.
+ DebugOnly<nsresult> rvIgnored = aSelectionController.SetCaretReadOnly(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISelectionController::SetCaretReadOnly(false) failed, but ignored");
+ // Show all the selection reflected to user.
+ rvIgnored =
+ aSelectionController.SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsISelectionController::SetSelectionFlags("
+ "nsISelectionDisplay::DISPLAY_ALL) failed, but ignored");
+
+ // Make sure that the editor will be destroyed properly
+ mDidPreDestroy = false;
+ // Make sure that the editor will be created properly
+ mDidPostCreate = false;
+
+ MOZ_ASSERT(IsBeingInitialized());
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SelectionRef().AddSelectionListener(this);
+
+ return NS_OK;
+}
+
+nsresult EditorBase::EnsureEmptyTextFirstChild() {
+ MOZ_ASSERT(IsTextEditor());
+ RefPtr<Element> root = GetRoot();
+ nsIContent* firstChild = root->GetFirstChild();
+
+ if (!firstChild || !firstChild->IsText()) {
+ RefPtr<nsTextNode> newTextNode = CreateTextNode(u""_ns);
+ if (!newTextNode) {
+ NS_WARNING("EditorBase::CreateTextNode() failed");
+ return NS_ERROR_UNEXPECTED;
+ }
+ IgnoredErrorResult ignoredError;
+ root->InsertChildBefore(newTextNode, root->GetFirstChild(), true,
+ ignoredError);
+ MOZ_ASSERT(!ignoredError.Failed());
+ }
+
+ return NS_OK;
+}
+
+nsresult EditorBase::PostCreateInternal() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Synchronize some stuff for the flags. SetFlags() will initialize
+ // something by the flag difference. This is first time of that, so, all
+ // initializations must be run. For such reason, we need to invert mFlags
+ // value first.
+ mFlags = ~mFlags;
+ nsresult rv = SetFlags(~mFlags);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::SetFlags() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // These operations only need to happen on the first PostCreate call
+ if (!mDidPostCreate) {
+ mDidPostCreate = true;
+
+ // Set up listeners
+ CreateEventListeners();
+ nsresult rv = InstallEventListeners();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InstallEventListeners() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // nuke the modification count, so the doc appears unmodified
+ // do this before we notify listeners
+ DebugOnly<nsresult> rvIgnored = ResetModificationCount();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::ResetModificationCount() failed, but ignored");
+
+ // update the UI with our state
+ rvIgnored = NotifyDocumentListeners(eDocumentCreated);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::NotifyDocumentListeners(eDocumentCreated)"
+ " failed, but ignored");
+ rvIgnored = NotifyDocumentListeners(eDocumentStateChanged);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::NotifyDocumentListeners("
+ "eDocumentStateChanged) failed, but ignored");
+ }
+
+ // update nsTextStateManager and caret if we have focus
+ if (RefPtr<Element> focusedElement = GetFocusedElement()) {
+ DebugOnly<nsresult> rvIgnored = InitializeSelection(*focusedElement);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::InitializeSelection() failed, but ignored");
+
+ // If the text control gets reframed during focus, Focus() would not be
+ // called, so take a chance here to see if we need to spell check the text
+ // control.
+ nsresult rv = FlushPendingSpellCheck();
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::FlushPendingSpellCheck() caused destroying the editor");
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::FlushPendingSpellCheck() failed, but ignored");
+
+ IMEState newState;
+ rv = GetPreferredIMEState(&newState);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::GetPreferredIMEState() failed");
+ return NS_OK;
+ }
+ IMEStateManager::UpdateIMEState(newState, focusedElement, *this);
+ }
+
+ // FYI: This call might cause destroying this editor.
+ IMEStateManager::OnEditorInitialized(*this);
+
+ return NS_OK;
+}
+
+void EditorBase::SetTextInputListener(TextInputListener* aTextInputListener) {
+ MOZ_ASSERT(!mTextInputListener || !aTextInputListener ||
+ mTextInputListener == aTextInputListener);
+ mTextInputListener = aTextInputListener;
+}
+
+void EditorBase::SetIMEContentObserver(
+ IMEContentObserver* aIMEContentObserver) {
+ MOZ_ASSERT(!mIMEContentObserver || !aIMEContentObserver ||
+ mIMEContentObserver == aIMEContentObserver);
+ mIMEContentObserver = aIMEContentObserver;
+}
+
+void EditorBase::CreateEventListeners() {
+ // Don't create the handler twice
+ if (!mEventListener) {
+ mEventListener = new EditorEventListener();
+ }
+}
+
+nsresult EditorBase::InstallEventListeners() {
+ // FIXME InstallEventListeners() should not be called if we failed to set
+ // document or create an event listener. So, these checks should be
+ // MOZ_DIAGNOSTIC_ASSERT instead.
+ MOZ_ASSERT(GetDocument());
+ if (MOZ_UNLIKELY(!GetDocument()) || NS_WARN_IF(!mEventListener)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Initialize the event target.
+ mEventTarget = GetExposedRoot();
+ if (NS_WARN_IF(!mEventTarget)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv = mEventListener->Connect(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::Connect() failed");
+ if (mComposition) {
+ // If mComposition has already been destroyed, we should forget it.
+ // This may happen if it ended while we don't listen to composition
+ // events.
+ if (mComposition->Destroyed()) {
+ // XXX We may need to fix existing composition transaction here.
+ // However, this may be called when it's not safe.
+ // Perhaps, we should stop handling composition with events.
+ mComposition = nullptr;
+ }
+ // Otherwise, Restart to handle composition with new editor contents.
+ else {
+ mComposition->StartHandlingComposition(this);
+ }
+ }
+ return rv;
+}
+
+void EditorBase::RemoveEventListeners() {
+ if (!mEventListener) {
+ return;
+ }
+ mEventListener->Disconnect();
+ if (mComposition) {
+ // Even if this is called, don't release mComposition because this is
+ // may be reused after reframing.
+ mComposition->EndHandlingComposition(this);
+ }
+ mEventTarget = nullptr;
+}
+
+bool EditorBase::IsListeningToEvents() const {
+ return mEventListener && !mEventListener->DetachedFromEditor();
+}
+
+bool EditorBase::GetDesiredSpellCheckState() {
+ // Check user override on this element
+ if (mSpellcheckCheckboxState != eTriUnset) {
+ return (mSpellcheckCheckboxState == eTriTrue);
+ }
+
+ // Check user preferences
+ int32_t spellcheckLevel = Preferences::GetInt("layout.spellcheckDefault", 1);
+
+ if (!spellcheckLevel) {
+ return false; // Spellchecking forced off globally
+ }
+
+ if (!CanEnableSpellCheck()) {
+ return false;
+ }
+
+ PresShell* presShell = GetPresShell();
+ if (presShell) {
+ nsPresContext* context = presShell->GetPresContext();
+ if (context && !context->IsDynamic()) {
+ return false;
+ }
+ }
+
+ // Check DOM state
+ nsCOMPtr<nsIContent> content = GetExposedRoot();
+ if (!content) {
+ return false;
+ }
+
+ auto element = nsGenericHTMLElement::FromNode(content);
+ if (!element) {
+ return false;
+ }
+
+ // XXX I'm not sure whether we don't use this path when we're a plaintext mail
+ // composer.
+ if (IsHTMLEditor() && !AsHTMLEditor()->IsPlaintextMailComposer()) {
+ // Some of the page content might be editable and some not, if spellcheck=
+ // is explicitly set anywhere, so if there's anything editable on the page,
+ // return true and let the spellchecker figure it out.
+ Document* doc = content->GetComposedDoc();
+ return doc && doc->IsEditingOn();
+ }
+
+ return element->Spellcheck();
+}
+
+void EditorBase::PreDestroyInternal() {
+ MOZ_ASSERT(!mDidPreDestroy);
+
+ mInitSucceeded = false;
+
+ Selection* selection = GetSelection();
+ if (selection) {
+ selection->RemoveSelectionListener(this);
+ }
+
+ IMEStateManager::OnEditorDestroying(*this);
+
+ // Let spellchecker clean up its observers etc. It is important not to
+ // actually free the spellchecker here, since the spellchecker could have
+ // caused flush notifications, which could have gotten here if a textbox
+ // is being removed. Setting the spellchecker to nullptr could free the
+ // object that is still in use! It will be freed when the editor is
+ // destroyed.
+ if (mInlineSpellChecker) {
+ DebugOnly<nsresult> rvIgnored =
+ mInlineSpellChecker->Cleanup(IsTextEditor());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "mozInlineSpellChecker::Cleanup() failed, but ignored");
+ }
+
+ // tell our listeners that the doc is going away
+ DebugOnly<nsresult> rvIgnored =
+ NotifyDocumentListeners(eDocumentToBeDestroyed);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::NotifyDocumentListeners("
+ "eDocumentToBeDestroyed) failed, but ignored");
+
+ // Unregister event listeners
+ RemoveEventListeners();
+ // If this editor is still hiding the caret, we need to restore it.
+ HideCaret(false);
+ mActionListeners.Clear();
+ mDocStateListeners.Clear();
+ mInlineSpellChecker = nullptr;
+ mTextServicesDocument = nullptr;
+ mTextInputListener = nullptr;
+ mSpellcheckCheckboxState = eTriUnset;
+ mRootElement = nullptr;
+
+ // Transaction may grab this instance. Therefore, they should be released
+ // here for stopping the circular reference with this instance.
+ if (mTransactionManager) {
+ DebugOnly<bool> disabledUndoRedo = DisableUndoRedo();
+ NS_WARNING_ASSERTION(disabledUndoRedo,
+ "EditorBase::DisableUndoRedo() failed, but ignored");
+ mTransactionManager = nullptr;
+ }
+
+ if (mEditActionData) {
+ mEditActionData->OnEditorDestroy();
+ }
+
+ mDidPreDestroy = true;
+}
+
+NS_IMETHODIMP EditorBase::GetFlags(uint32_t* aFlags) {
+ // NOTE: If you need to override this method, you need to make Flags()
+ // virtual.
+ *aFlags = Flags();
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::SetFlags(uint32_t aFlags) {
+ if (mFlags == aFlags) {
+ return NS_OK;
+ }
+
+ // If we're a `TextEditor` instance, it's always a plaintext editor.
+ // Therefore, `eEditorPlaintextMask` is not necessary and should not be set
+ // for the performance reason.
+ MOZ_ASSERT_IF(IsTextEditor(), !(aFlags & nsIEditor::eEditorPlaintextMask));
+ // If we're an `HTMLEditor` instance, we cannot treat it as a single line
+ // editor. So, eEditorSingleLineMask is available only when we're a
+ // `TextEditor` instance.
+ MOZ_ASSERT_IF(IsHTMLEditor(), !(aFlags & nsIEditor::eEditorSingleLineMask));
+ // If we're an `HTMLEditor` instance, we cannot treat it as a password editor.
+ // So, eEditorPasswordMask is available only when we're a `TextEditor`
+ // instance.
+ MOZ_ASSERT_IF(IsHTMLEditor(), !(aFlags & nsIEditor::eEditorPasswordMask));
+ // eEditorAllowInteraction changes the behavior of `HTMLEditor`. So, it's
+ // not available with `TextEditor` instance.
+ MOZ_ASSERT_IF(IsTextEditor(), !(aFlags & nsIEditor::eEditorAllowInteraction));
+
+ const bool isCalledByPostCreate = (mFlags == ~aFlags);
+ // We don't support dynamic password flag change.
+ MOZ_ASSERT_IF(!isCalledByPostCreate,
+ !((mFlags ^ aFlags) & nsIEditor::eEditorPasswordMask));
+ bool spellcheckerWasEnabled = !isCalledByPostCreate && CanEnableSpellCheck();
+ mFlags = aFlags;
+
+ if (!IsInitialized()) {
+ // If we're initializing, we shouldn't do anything now.
+ // SetFlags() will be called by PostCreate(),
+ // we should synchronize some stuff for the flags at that time.
+ return NS_OK;
+ }
+
+ // The flag change may cause the spellchecker state change
+ if (CanEnableSpellCheck() != spellcheckerWasEnabled) {
+ SyncRealTimeSpell();
+ }
+
+ // If this is called from PostCreate(), it will update the IME state if it's
+ // necessary.
+ if (!mDidPostCreate) {
+ return NS_OK;
+ }
+
+ // Might be changing editable state, so, we need to reset current IME state
+ // if we're focused and the flag change causes IME state change.
+ if (RefPtr<Element> focusedElement = GetFocusedElement()) {
+ IMEState newState;
+ nsresult rv = GetPreferredIMEState(&newState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::GetPreferredIMEState() failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ // NOTE: When the enabled state isn't going to be modified, this method
+ // is going to do nothing.
+ IMEStateManager::UpdateIMEState(newState, focusedElement, *this);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetIsSelectionEditable(bool* aIsSelectionEditable) {
+ if (NS_WARN_IF(!aIsSelectionEditable)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aIsSelectionEditable = IsSelectionEditable();
+ return NS_OK;
+}
+
+bool EditorBase::IsSelectionEditable() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return false;
+ }
+
+ if (IsTextEditor()) {
+ // XXX we just check that the anchor node is editable at the moment
+ // we should check that all nodes in the selection are editable
+ const nsINode* anchorNode = SelectionRef().GetAnchorNode();
+ return anchorNode && anchorNode->IsContent() && anchorNode->IsEditable();
+ }
+
+ const nsINode* anchorNode = SelectionRef().GetAnchorNode();
+ const nsINode* focusNode = SelectionRef().GetFocusNode();
+ if (!anchorNode || !focusNode) {
+ return false;
+ }
+
+ // if anchorNode or focusNode is in a native anonymous subtree, HTMLEditor
+ // shouldn't edit content in it.
+ // XXX This must be a bug of Selection API.
+ if (MOZ_UNLIKELY(anchorNode->IsInNativeAnonymousSubtree() ||
+ focusNode->IsInNativeAnonymousSubtree())) {
+ return false;
+ }
+
+ // Per the editing spec as of June 2012: we have to have a selection whose
+ // start and end nodes are editable, and which share an ancestor editing
+ // host. (Bug 766387.)
+ bool isSelectionEditable = SelectionRef().RangeCount() &&
+ anchorNode->IsEditable() &&
+ focusNode->IsEditable();
+ if (!isSelectionEditable) {
+ return false;
+ }
+
+ const nsINode* commonAncestor =
+ SelectionRef().GetAnchorFocusRange()->GetClosestCommonInclusiveAncestor();
+ while (commonAncestor && !commonAncestor->IsEditable()) {
+ commonAncestor = commonAncestor->GetParentNode();
+ }
+ // If there is no editable common ancestor, return false.
+ return !!commonAncestor;
+}
+
+NS_IMETHODIMP EditorBase::GetIsDocumentEditable(bool* aIsDocumentEditable) {
+ if (NS_WARN_IF(!aIsDocumentEditable)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ RefPtr<Document> document = GetDocument();
+ *aIsDocumentEditable = document && IsModifiable();
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetDocument(Document** aDocument) {
+ if (NS_WARN_IF(!aDocument)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aDocument = do_AddRef(mDocument).take();
+ return NS_WARN_IF(!*aDocument) ? NS_ERROR_NOT_INITIALIZED : NS_OK;
+}
+
+already_AddRefed<nsIWidget> EditorBase::GetWidget() const {
+ nsPresContext* presContext = GetPresContext();
+ if (NS_WARN_IF(!presContext)) {
+ return nullptr;
+ }
+ nsCOMPtr<nsIWidget> widget = presContext->GetRootWidget();
+ return NS_WARN_IF(!widget) ? nullptr : widget.forget();
+}
+
+NS_IMETHODIMP EditorBase::GetContentsMIMEType(nsAString& aContentsMIMEType) {
+ aContentsMIMEType = mContentMIMEType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::SetContentsMIMEType(
+ const nsAString& aContentsMIMEType) {
+ mContentMIMEType.Assign(aContentsMIMEType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetSelectionController(
+ nsISelectionController** aSelectionController) {
+ if (NS_WARN_IF(!aSelectionController)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aSelectionController = do_AddRef(GetSelectionController()).take();
+ return NS_WARN_IF(!*aSelectionController) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::DeleteSelection(EDirection aAction,
+ EStripWrappers aStripWrappers) {
+ nsresult rv = DeleteSelectionAsAction(aAction, aStripWrappers);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsAction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP EditorBase::GetSelection(Selection** aSelection) {
+ nsresult rv = GetSelection(SelectionType::eNormal, aSelection);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::GetSelection(SelectionType::eNormal) failed");
+ return rv;
+}
+
+nsresult EditorBase::GetSelection(SelectionType aSelectionType,
+ Selection** aSelection) const {
+ if (NS_WARN_IF(!aSelection)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (IsEditActionDataAvailable()) {
+ *aSelection = do_AddRef(&SelectionRef()).take();
+ return NS_OK;
+ }
+ nsISelectionController* selectionController = GetSelectionController();
+ if (NS_WARN_IF(!selectionController)) {
+ *aSelection = nullptr;
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ *aSelection = do_AddRef(selectionController->GetSelection(
+ ToRawSelectionType(aSelectionType)))
+ .take();
+ return NS_WARN_IF(!*aSelection) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+nsresult EditorBase::DoTransactionInternal(nsITransaction* aTransaction) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!ShouldAlreadyHaveHandledBeforeInputEventDispatching(),
+ "beforeinput event hasn't been dispatched yet");
+
+ if (mPlaceholderBatch && !mPlaceholderTransaction) {
+ MOZ_DIAGNOSTIC_ASSERT(mPlaceholderName);
+ mPlaceholderTransaction = PlaceholderTransaction::Create(
+ *this, *mPlaceholderName, std::move(mSelState));
+ MOZ_ASSERT(mSelState.isNothing());
+
+ // We will recurse, but will not hit this case in the nested call
+ RefPtr<PlaceholderTransaction> placeholderTransaction =
+ mPlaceholderTransaction;
+ DebugOnly<nsresult> rvIgnored =
+ DoTransactionInternal(placeholderTransaction);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::DoTransactionInternal() failed, but ignored");
+
+ if (mTransactionManager) {
+ if (nsCOMPtr<nsITransaction> topTransaction =
+ mTransactionManager->PeekUndoStack()) {
+ if (RefPtr<EditTransactionBase> topTransactionBase =
+ topTransaction->GetAsEditTransactionBase()) {
+ if (PlaceholderTransaction* topPlaceholderTransaction =
+ topTransactionBase->GetAsPlaceholderTransaction()) {
+ // there is a placeholder transaction on top of the undo stack. It
+ // is either the one we just created, or an earlier one that we are
+ // now merging into. From here on out remember this placeholder
+ // instead of the one we just created.
+ mPlaceholderTransaction = topPlaceholderTransaction;
+ }
+ }
+ }
+ }
+ }
+
+ if (aTransaction) {
+ // XXX: Why are we doing selection specific batching stuff here?
+ // XXX: Most entry points into the editor have auto variables that
+ // XXX: should trigger Begin/EndUpdateViewBatch() calls that will make
+ // XXX: these selection batch calls no-ops.
+ // XXX:
+ // XXX: I suspect that this was placed here to avoid multiple
+ // XXX: selection changed notifications from happening until after
+ // XXX: the transaction was done. I suppose that can still happen
+ // XXX: if an embedding application called DoTransaction() directly
+ // XXX: to pump its own transactions through the system, but in that
+ // XXX: case, wouldn't we want to use Begin/EndUpdateViewBatch() or
+ // XXX: its auto equivalent AutoUpdateViewBatch to ensure that
+ // XXX: selection listeners have access to accurate frame data?
+ // XXX:
+ // XXX: Note that if we did add Begin/EndUpdateViewBatch() calls
+ // XXX: we will need to make sure that they are disabled during
+ // XXX: the init of the editor for text widgets to avoid layout
+ // XXX: re-entry during initial reflow. - kin
+
+ // get the selection and start a batch change
+ SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__);
+
+ if (mTransactionManager) {
+ RefPtr<TransactionManager> transactionManager(mTransactionManager);
+ nsresult rv = transactionManager->DoTransaction(aTransaction);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TransactionManager::DoTransaction() failed");
+ return rv;
+ }
+ } else {
+ nsresult rv = aTransaction->DoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsITransaction::DoTransaction() failed");
+ return rv;
+ }
+ }
+
+ DoAfterDoTransaction(aTransaction);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::EnableUndo(bool aEnable) {
+ // XXX Should we return NS_ERROR_FAILURE if EdnableUndoRedo() or
+ // DisableUndoRedo() returns false?
+ if (aEnable) {
+ DebugOnly<bool> enabledUndoRedo = EnableUndoRedo();
+ NS_WARNING_ASSERTION(enabledUndoRedo,
+ "EditorBase::EnableUndoRedo() failed, but ignored");
+ return NS_OK;
+ }
+ DebugOnly<bool> disabledUndoRedo = DisableUndoRedo();
+ NS_WARNING_ASSERTION(disabledUndoRedo,
+ "EditorBase::DisableUndoRedo() failed, but ignored");
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::ClearUndoRedoXPCOM() {
+ if (MOZ_UNLIKELY(!ClearUndoRedo())) {
+ return NS_ERROR_FAILURE; // We're handling a transaction
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::Undo() {
+ nsresult rv = UndoAsAction(1u);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::UndoAsAction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP EditorBase::UndoAll() {
+ if (!mTransactionManager) {
+ return NS_OK;
+ }
+ size_t numberOfUndoItems = mTransactionManager->NumberOfUndoItems();
+ if (!numberOfUndoItems) {
+ return NS_OK; // no transactions
+ }
+ nsresult rv = UndoAsAction(numberOfUndoItems);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::UndoAsAction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP EditorBase::GetUndoRedoEnabled(bool* aIsEnabled) {
+ MOZ_ASSERT(aIsEnabled);
+ *aIsEnabled = IsUndoRedoEnabled();
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetCanUndo(bool* aCanUndo) {
+ MOZ_ASSERT(aCanUndo);
+ *aCanUndo = CanUndo();
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::Redo() {
+ nsresult rv = RedoAsAction(1u);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::RedoAsAction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP EditorBase::GetCanRedo(bool* aCanRedo) {
+ MOZ_ASSERT(aCanRedo);
+ *aCanRedo = CanRedo();
+ return NS_OK;
+}
+
+nsresult EditorBase::UndoAsAction(uint32_t aCount, nsIPrincipal* aPrincipal) {
+ if (aCount == 0 || IsReadonly()) {
+ return NS_OK;
+ }
+
+ // If we don't have transaction in the undo stack, we shouldn't notify
+ // anybody of trying to undo since it's not useful notification but we
+ // need to pay some runtime cost.
+ if (!CanUndo()) {
+ return NS_OK;
+ }
+
+ // If there is composition, we shouldn't allow to undo with committing
+ // composition since Chrome doesn't allow it and it doesn't make sense
+ // because committing composition causes one transaction and Undo(1)
+ // undoes the committing composition.
+ if (GetComposition()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eUndo, aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__);
+
+ NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+ if (NS_WARN_IF(!CanUndo()) || NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = NS_OK;
+ {
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eUndo, nsIEditor::eNone, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return EditorBase::ToGenericNSResult(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() "
+ "failed, but ignored");
+
+ RefPtr<TransactionManager> transactionManager(mTransactionManager);
+ for (uint32_t i = 0; i < aCount; ++i) {
+ if (NS_FAILED(transactionManager->Undo())) {
+ NS_WARNING("TransactionManager::Undo() failed");
+ break;
+ }
+ DoAfterUndoTransaction();
+ }
+
+ if (IsHTMLEditor()) {
+ rv = AsHTMLEditor()->ReflectPaddingBRElementForEmptyEditor();
+ }
+ }
+
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::RedoAsAction(uint32_t aCount, nsIPrincipal* aPrincipal) {
+ if (aCount == 0 || IsReadonly()) {
+ return NS_OK;
+ }
+
+ // If we don't have transaction in the redo stack, we shouldn't notify
+ // anybody of trying to redo since it's not useful notification but we
+ // need to pay some runtime cost.
+ if (!CanRedo()) {
+ return NS_OK;
+ }
+
+ // If there is composition, we shouldn't allow to redo with committing
+ // composition since Chrome doesn't allow it and it doesn't make sense
+ // because committing composition causes removing all transactions from
+ // the redo queue. So, it becomes impossible to redo anything.
+ if (GetComposition()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eRedo, aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__);
+
+ NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+ if (NS_WARN_IF(!CanRedo()) || NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = NS_OK;
+ {
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eRedo, nsIEditor::eNone, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() "
+ "failed, but ignored");
+
+ RefPtr<TransactionManager> transactionManager(mTransactionManager);
+ for (uint32_t i = 0; i < aCount; ++i) {
+ if (NS_FAILED(transactionManager->Redo())) {
+ NS_WARNING("TransactionManager::Redo() failed");
+ break;
+ }
+ DoAfterRedoTransaction();
+ }
+
+ if (IsHTMLEditor()) {
+ rv = AsHTMLEditor()->ReflectPaddingBRElementForEmptyEditor();
+ }
+ }
+
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP EditorBase::BeginTransaction() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eUnknown);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ BeginTransactionInternal(__FUNCTION__);
+ return NS_OK;
+}
+
+void EditorBase::BeginTransactionInternal(const char* aRequesterFuncName) {
+ BeginUpdateViewBatch(aRequesterFuncName);
+
+ if (NS_WARN_IF(!mTransactionManager)) {
+ return;
+ }
+
+ RefPtr<TransactionManager> transactionManager(mTransactionManager);
+ DebugOnly<nsresult> rvIgnored = transactionManager->BeginBatch(nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TransactionManager::BeginBatch() failed, but ignored");
+}
+
+NS_IMETHODIMP EditorBase::EndTransaction() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eUnknown);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EndTransactionInternal(__FUNCTION__);
+ return NS_OK;
+}
+
+void EditorBase::EndTransactionInternal(const char* aRequesterFuncName) {
+ if (mTransactionManager) {
+ RefPtr<TransactionManager> transactionManager(mTransactionManager);
+ DebugOnly<nsresult> rvIgnored = transactionManager->EndBatch(false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TransactionManager::EndBatch() failed, but ignored");
+ }
+
+ EndUpdateViewBatch(aRequesterFuncName);
+}
+
+void EditorBase::BeginPlaceholderTransaction(nsStaticAtom& aTransactionName,
+ const char* aRequesterFuncName) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(mPlaceholderBatch >= 0, "negative placeholder batch count!");
+
+ if (!mPlaceholderBatch) {
+ NotifyEditorObservers(eNotifyEditorObserversOfBefore);
+ // time to turn on the batch
+ BeginUpdateViewBatch(aRequesterFuncName);
+ mPlaceholderTransaction = nullptr;
+ mPlaceholderName = &aTransactionName;
+ mSelState.emplace();
+ mSelState->SaveSelection(SelectionRef());
+ // Composition transaction can modify multiple nodes and it merges text
+ // node for ime into single text node.
+ // So if current selection is into IME text node, it might be failed
+ // to restore selection by UndoTransaction.
+ // So we need update selection by range updater.
+ if (mPlaceholderName == nsGkAtoms::IMETxnName) {
+ RangeUpdaterRef().RegisterSelectionState(*mSelState);
+ }
+ }
+ mPlaceholderBatch++;
+}
+
+void EditorBase::EndPlaceholderTransaction(
+ ScrollSelectionIntoView aScrollSelectionIntoView,
+ const char* aRequesterFuncName) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(mPlaceholderBatch > 0,
+ "zero or negative placeholder batch count when ending batch!");
+
+ if (!(--mPlaceholderBatch)) {
+ // By making the assumption that no reflow happens during the calls
+ // to EndUpdateViewBatch and ScrollSelectionFocusIntoView, we are able to
+ // allow the selection to cache a frame offset which is used by the
+ // caret drawing code. We only enable this cache here; at other times,
+ // we have no way to know whether reflow invalidates it
+ // See bugs 35296 and 199412.
+ SelectionRef().SetCanCacheFrameOffset(true);
+
+ // time to turn off the batch
+ EndUpdateViewBatch(aRequesterFuncName);
+ // make sure selection is in view
+
+ // After ScrollSelectionFocusIntoView(), the pending notifications might be
+ // flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
+ // XXX Even if we're destroyed, we need to keep handling below because
+ // this method changes a lot of status. We should rewrite this safer.
+ if (aScrollSelectionIntoView == ScrollSelectionIntoView::Yes) {
+ DebugOnly<nsresult> rvIgnored = ScrollSelectionFocusIntoView();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::ScrollSelectionFocusIntoView() failed, but Ignored");
+ }
+
+ // cached for frame offset are Not available now
+ SelectionRef().SetCanCacheFrameOffset(false);
+
+ if (mSelState) {
+ // we saved the selection state, but never got to hand it to placeholder
+ // (else we ould have nulled out this pointer), so destroy it to prevent
+ // leaks.
+ if (mPlaceholderName == nsGkAtoms::IMETxnName) {
+ RangeUpdaterRef().DropSelectionState(*mSelState);
+ }
+ mSelState.reset();
+ }
+ // We might have never made a placeholder if no action took place.
+ if (mPlaceholderTransaction) {
+ // FYI: Disconnect placeholder transaction before dispatching "input"
+ // event because an input event listener may start other things.
+ // TODO: We should forget EditActionDataSetter too.
+ RefPtr<PlaceholderTransaction> placeholderTransaction =
+ std::move(mPlaceholderTransaction);
+ DebugOnly<nsresult> rvIgnored =
+ placeholderTransaction->EndPlaceHolderBatch();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "PlaceholderTransaction::EndPlaceHolderBatch() failed, but ignored");
+ // notify editor observers of action but if composing, it's done by
+ // compositionchange event handler.
+ if (!mComposition) {
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ }
+ } else {
+ NotifyEditorObservers(eNotifyEditorObserversOfCancel);
+ }
+ }
+}
+
+NS_IMETHODIMP EditorBase::GetDocumentIsEmpty(bool* aDocumentIsEmpty) {
+ MOZ_ASSERT(aDocumentIsEmpty);
+ *aDocumentIsEmpty = IsEmpty();
+ return NS_OK;
+}
+
+// XXX: The rule system should tell us which node to select all on (ie, the
+// root, or the body)
+NS_IMETHODIMP EditorBase::SelectAll() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = SelectAllInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SelectAllInternal() failed");
+ // This is low level API for XUL applcation. So, we should return raw
+ // error code here.
+ return rv;
+}
+
+nsresult EditorBase::SelectAllInternal() {
+ MOZ_ASSERT(IsInitialized());
+
+ DebugOnly<nsresult> rvIgnored = CommitComposition();
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CommitComposition() failed, but ignored");
+
+ // XXX Do we need to keep handling after committing composition causes moving
+ // focus to different element? Although TextEditor has independent
+ // selection, so, we may not see any odd behavior even in such case.
+
+ nsresult rv = SelectEntireDocument();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SelectEntireDocument() failed");
+ return rv;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP EditorBase::BeginningOfDocument() {
+ MOZ_ASSERT(IsTextEditor());
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // get the root element
+ RefPtr<Element> rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // find first editable thingy
+ nsCOMPtr<nsIContent> firstEditableLeaf;
+ // If we're `TextEditor`, the first editable leaf node is a text node or
+ // padding `<br>` element. In the first case, we need to collapse selection
+ // into it.
+ if (rootElement->GetFirstChild() && rootElement->GetFirstChild()->IsText()) {
+ firstEditableLeaf = rootElement->GetFirstChild();
+ }
+ if (!firstEditableLeaf) {
+ // just the root node, set selection to inside the root
+ nsresult rv = CollapseSelectionToStartOf(*rootElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ return rv;
+ }
+
+ if (firstEditableLeaf->IsText()) {
+ // If firstEditableLeaf is text, set selection to beginning of the text
+ // node.
+ nsresult rv = CollapseSelectionToStartOf(*firstEditableLeaf);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ return rv;
+ }
+
+ // Otherwise, it's a leaf node and we set the selection just in front of it.
+ nsCOMPtr<nsIContent> parent = firstEditableLeaf->GetParent();
+ if (NS_WARN_IF(!parent)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ MOZ_ASSERT(
+ parent->ComputeIndexOf(firstEditableLeaf).valueOr(UINT32_MAX) == 0,
+ "How come the first node isn't the left most child in its parent?");
+ nsresult rv = CollapseSelectionToStartOf(*parent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ return rv;
+}
+
+NS_IMETHODIMP EditorBase::EndOfDocument() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP EditorBase::GetDocumentModified(bool* aOutDocModified) {
+ if (NS_WARN_IF(!aOutDocModified)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ int32_t modCount = 0;
+ DebugOnly<nsresult> rvIgnored = GetModificationCount(&modCount);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::GetModificationCount() failed, but ignored");
+
+ *aOutDocModified = (modCount != 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetDocumentCharacterSet(nsACString& aCharacterSet) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult EditorBase::GetDocumentCharsetInternal(nsACString& aCharset) const {
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ document->GetDocumentCharacterSet()->Name(aCharset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::SetDocumentCharacterSet(
+ const nsACString& aCharacterSet) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP EditorBase::OutputToString(const nsAString& aFormatType,
+ uint32_t aDocumentEncoderFlags,
+ nsAString& aOutputString) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv =
+ ComputeValueInternal(aFormatType, aDocumentEncoderFlags, aOutputString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::ComputeValueInternal() failed");
+ // This is low level API for XUL application. So, we should return raw
+ // error code here.
+ return rv;
+}
+
+nsresult EditorBase::ComputeValueInternal(const nsAString& aFormatType,
+ uint32_t aDocumentEncoderFlags,
+ nsAString& aOutputString) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // First, let's try to get the value simply only from text node if the
+ // caller wants plaintext value.
+ if (aFormatType.LowerCaseEqualsLiteral("text/plain") &&
+ !(aDocumentEncoderFlags & (nsIDocumentEncoder::OutputSelectionOnly |
+ nsIDocumentEncoder::OutputWrap))) {
+ // Shortcut for empty editor case.
+ if (IsEmpty()) {
+ aOutputString.Truncate();
+ return NS_OK;
+ }
+ // NOTE: If it's neither <input type="text"> nor <textarea>, e.g., an HTML
+ // editor which is in plaintext mode (e.g., plaintext email composer on
+ // Thunderbird), it should be handled by the expensive path.
+ if (IsTextEditor()) {
+ // If it's necessary to check selection range or the editor wraps hard,
+ // we need some complicated handling. In such case, we need to use the
+ // expensive path.
+ // XXX Anything else what we cannot return the text node data simply?
+ Result<EditActionResult, nsresult> result =
+ AsTextEditor()->ComputeValueFromTextNodeAndBRElement(aOutputString);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("TextEditor::ComputeValueFromTextNodeAndBRElement() failed");
+ return result.unwrapErr();
+ }
+ if (!result.inspect().Ignored()) {
+ return NS_OK;
+ }
+ }
+ }
+
+ nsAutoCString charset;
+ nsresult rv = GetDocumentCharsetInternal(charset);
+ if (NS_FAILED(rv) || charset.IsEmpty()) {
+ charset.AssignLiteral("windows-1252"); // XXX Why don't we use "UTF-8"?
+ }
+
+ nsCOMPtr<nsIDocumentEncoder> encoder =
+ GetAndInitDocEncoder(aFormatType, aDocumentEncoderFlags, charset);
+ if (!encoder) {
+ NS_WARNING("EditorBase::GetAndInitDocEncoder() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = encoder->EncodeToString(aOutputString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIDocumentEncoder::EncodeToString() failed");
+ return rv;
+}
+
+already_AddRefed<nsIDocumentEncoder> EditorBase::GetAndInitDocEncoder(
+ const nsAString& aFormatType, uint32_t aDocumentEncoderFlags,
+ const nsACString& aCharset) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsCOMPtr<nsIDocumentEncoder> docEncoder;
+ if (!mCachedDocumentEncoder ||
+ !mCachedDocumentEncoderType.Equals(aFormatType)) {
+ nsAutoCString formatType;
+ LossyAppendUTF16toASCII(aFormatType, formatType);
+ docEncoder = do_createDocumentEncoder(PromiseFlatCString(formatType).get());
+ if (NS_WARN_IF(!docEncoder)) {
+ return nullptr;
+ }
+ mCachedDocumentEncoder = docEncoder;
+ mCachedDocumentEncoderType = aFormatType;
+ } else {
+ docEncoder = mCachedDocumentEncoder;
+ }
+
+ RefPtr<Document> doc = GetDocument();
+ NS_ASSERTION(doc, "Need a document");
+
+ nsresult rv = docEncoder->NativeInit(
+ doc, aFormatType,
+ aDocumentEncoderFlags | nsIDocumentEncoder::RequiresReinitAfterOutput);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIDocumentEncoder::NativeInit() failed");
+ return nullptr;
+ }
+
+ if (!aCharset.IsEmpty() && !aCharset.EqualsLiteral("null")) {
+ DebugOnly<nsresult> rvIgnored = docEncoder->SetCharset(aCharset);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIDocumentEncoder::SetCharset() failed, but ignored");
+ }
+
+ const int32_t wrapWidth = std::max(WrapWidth(), 0);
+ DebugOnly<nsresult> rvIgnored = docEncoder->SetWrapColumn(wrapWidth);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIDocumentEncoder::SetWrapColumn() failed, but ignored");
+
+ // Set the selection, if appropriate.
+ // We do this either if the OutputSelectionOnly flag is set,
+ // in which case we use our existing selection ...
+ if (aDocumentEncoderFlags & nsIDocumentEncoder::OutputSelectionOnly) {
+ if (NS_FAILED(docEncoder->SetSelection(&SelectionRef()))) {
+ NS_WARNING("nsIDocumentEncoder::SetSelection() failed");
+ return nullptr;
+ }
+ }
+ // ... or if the root element is not a body,
+ // in which case we set the selection to encompass the root.
+ else {
+ Element* rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return nullptr;
+ }
+ if (!rootElement->IsHTMLElement(nsGkAtoms::body)) {
+ if (NS_FAILED(docEncoder->SetContainerNode(rootElement))) {
+ NS_WARNING("nsIDocumentEncoder::SetContainerNode() failed");
+ return nullptr;
+ }
+ }
+ }
+
+ return docEncoder.forget();
+}
+
+bool EditorBase::AreClipboardCommandsUnconditionallyEnabled() const {
+ Document* document = GetDocument();
+ return document && document->AreClipboardCommandsUnconditionallyEnabled();
+}
+
+bool EditorBase::CheckForClipboardCommandListener(
+ nsAtom* aCommand, EventMessage aEventMessage) const {
+ RefPtr<Document> document = GetDocument();
+ if (!document) {
+ return false;
+ }
+
+ // We exclude XUL and chrome docs here to maintain current behavior where
+ // in these cases the editor element alone is expected to handle clipboard
+ // command availability.
+ if (!document->AreClipboardCommandsUnconditionallyEnabled()) {
+ return false;
+ }
+
+ // So in web content documents, "unconditionally" enabled Cut/Copy are not
+ // really unconditional; they're enabled if there is a listener that wants
+ // to handle them. What they're not conditional on here is whether there is
+ // currently a selection in the editor.
+ RefPtr<PresShell> presShell = document->GetObservingPresShell();
+ if (!presShell) {
+ return false;
+ }
+ RefPtr<nsPresContext> presContext = presShell->GetPresContext();
+ if (!presContext) {
+ return false;
+ }
+
+ RefPtr<EventTarget> et = IsHTMLEditor()
+ ? AsHTMLEditor()->ComputeEditingHost(
+ HTMLEditor::LimitInBodyElement::No)
+ : GetDOMEventTarget();
+
+ while (et) {
+ EventListenerManager* elm = et->GetExistingListenerManager();
+ if (elm && elm->HasListenersFor(aCommand)) {
+ return true;
+ }
+ InternalClipboardEvent event(true, aEventMessage);
+ EventChainPreVisitor visitor(presContext, &event, nullptr,
+ nsEventStatus_eIgnore, false, et);
+ et->GetEventTargetParent(visitor);
+ et = visitor.GetParentTarget();
+ }
+
+ return false;
+}
+
+Result<EditorBase::ClipboardEventResult, nsresult>
+EditorBase::DispatchClipboardEventAndUpdateClipboard(EventMessage aEventMessage,
+ int32_t aClipboardType) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ const bool isPasting =
+ aEventMessage == ePaste || aEventMessage == ePasteNoFormatting;
+ if (isPasting) {
+ CommitComposition();
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+
+ const RefPtr<Selection> sel = [&]() {
+ if (IsHTMLEditor() && aEventMessage == eCopy &&
+ SelectionRef().IsCollapsed()) {
+ // If we don't have a usable selection for copy and we're an HTML
+ // editor (which is global for the document) try to use the last
+ // focused selection instead.
+ return nsCopySupport::GetSelectionForCopy(GetDocument());
+ }
+ return do_AddRef(&SelectionRef());
+ }();
+
+ bool actionTaken = false;
+ const bool doDefault = nsCopySupport::FireClipboardEvent(
+ aEventMessage, aClipboardType, presShell, sel, &actionTaken);
+ NotifyOfDispatchingClipboardEvent();
+
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ if (doDefault) {
+ MOZ_ASSERT(actionTaken);
+ return ClipboardEventResult::DoDefault;
+ }
+ // If we handle a "paste" and nsCopySupport::FireClipboardEvent sets
+ // actionTaken to "false" means that it's an error. Otherwise, the "paste"
+ // event is just canceled.
+ if (isPasting) {
+ return actionTaken ? ClipboardEventResult::DefaultPreventedOfPaste
+ : ClipboardEventResult::IgnoredOrError;
+ }
+ // If we handle a "copy", actionTaken is set to true only when
+ // nsCopySupport::FireClipboardEvent does not meet an error.
+ // If we handle a "cut", actionTaken is set to true only when
+ // nsCopySupport::FireClipboardEvent does not meet an error and
+ // - the selection is collapsed in editable elements when the event is not
+ // canceled.
+ // - the event is canceled but update the clipboard with the dataTransfer
+ // of the event.
+ return actionTaken ? ClipboardEventResult::CopyOrCutHandled
+ : ClipboardEventResult::IgnoredOrError;
+}
+
+NS_IMETHODIMP EditorBase::Cut() {
+ nsresult rv = CutAsAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CutAsAction() failed");
+ return rv;
+}
+
+nsresult EditorBase::CutAsAction(nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eCut, aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ {
+ RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();
+
+ Result<ClipboardEventResult, nsresult> ret =
+ DispatchClipboardEventAndUpdateClipboard(
+ eCut, nsIClipboard::kGlobalClipboard);
+ if (MOZ_UNLIKELY(ret.isErr())) {
+ NS_WARNING(
+ "EditorBase::DispatchClipboardEventAndUpdateClipboard(eCut, "
+ "nsIClipboard::kGlobalClipboard) failed");
+ return EditorBase::ToGenericNSResult(ret.unwrapErr());
+ }
+ switch (ret.unwrap()) {
+ case ClipboardEventResult::DoDefault:
+ break;
+ case ClipboardEventResult::CopyOrCutHandled:
+ return NS_OK;
+ case ClipboardEventResult::IgnoredOrError:
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ case ClipboardEventResult::DefaultPreventedOfPaste:
+ MOZ_ASSERT_UNREACHABLE("Invalid result for eCut");
+ }
+
+ // If focus is changed by a "cut" event listener, we should stop handling
+ // the cut.
+ const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
+ if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
+ if (focusManager->GetFocusedWindow() != GetWindow()) {
+ return NS_OK;
+ }
+ RefPtr<EditorBase> editorBase =
+ nsContentUtils::GetActiveEditor(GetPresContext());
+ if (!editorBase || (editorBase->IsHTMLEditor() &&
+ !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
+ return NS_OK;
+ }
+ if (editorBase != this) {
+ return NS_OK;
+ }
+ }
+ }
+
+ // Dispatch "beforeinput" event after dispatching "cut" event.
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ // XXX This transaction name is referred by PlaceholderTransaction::Merge()
+ // so that we need to keep using it here.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::DeleteTxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+ rv = DeleteSelectionAsSubAction(
+ eNone, IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsSubAction(eNone) failed, but ignored");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP EditorBase::CanCut(bool* aCanCut) {
+ if (NS_WARN_IF(!aCanCut)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aCanCut = IsCutCommandEnabled();
+ return NS_OK;
+}
+
+bool EditorBase::IsCutCommandEnabled() const {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return false;
+ }
+
+ if (IsModifiable() && IsCopyToClipboardAllowedInternal()) {
+ return true;
+ }
+
+ // If there's an event listener for "cut", we always enable the command
+ // as we don't really know what the listener may want to do in response.
+ // We look up the event target chain for a possible listener on a parent
+ // in addition to checking the immediate target.
+ return CheckForClipboardCommandListener(nsGkAtoms::oncut, eCut);
+}
+
+NS_IMETHODIMP EditorBase::Copy() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eCopy);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ Result<ClipboardEventResult, nsresult> ret =
+ DispatchClipboardEventAndUpdateClipboard(eCopy,
+ nsIClipboard::kGlobalClipboard);
+ if (MOZ_UNLIKELY(ret.isErr())) {
+ NS_WARNING(
+ "EditorBase::DispatchClipboardEventAndUpdateClipboard(eCopy, "
+ "nsIClipboard::kGlobalClipboard) failed");
+ return EditorBase::ToGenericNSResult(ret.unwrapErr());
+ }
+ switch (ret.unwrap()) {
+ case ClipboardEventResult::DoDefault:
+ case ClipboardEventResult::CopyOrCutHandled:
+ return NS_OK;
+ case ClipboardEventResult::IgnoredOrError:
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ case ClipboardEventResult::DefaultPreventedOfPaste:
+ MOZ_ASSERT_UNREACHABLE("Invalid result for eCopy");
+ }
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP EditorBase::CanCopy(bool* aCanCopy) {
+ if (NS_WARN_IF(!aCanCopy)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aCanCopy = IsCopyCommandEnabled();
+ return NS_OK;
+}
+
+bool EditorBase::IsCopyCommandEnabled() const {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return false;
+ }
+
+ if (IsCopyToClipboardAllowedInternal()) {
+ return true;
+ }
+
+ // Like "cut", always enable "copy" if there's a listener.
+ return CheckForClipboardCommandListener(nsGkAtoms::oncopy, eCopy);
+}
+
+NS_IMETHODIMP EditorBase::Paste(int32_t aClipboardType) {
+ const nsresult rv = PasteAsAction(aClipboardType, DispatchPasteEvent::Yes);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::PasteAsAction(DispatchPasteEvent::Yes) failed");
+ return rv;
+}
+
+nsresult EditorBase::PasteAsAction(int32_t aClipboardType,
+ DispatchPasteEvent aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal /* = nullptr */) {
+ if (IsHTMLEditor() && IsReadonly()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::ePaste,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (aDispatchPasteEvent == DispatchPasteEvent::Yes) {
+ RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();
+
+ Result<ClipboardEventResult, nsresult> ret =
+ DispatchClipboardEventAndUpdateClipboard(ePaste, aClipboardType);
+ if (MOZ_UNLIKELY(ret.isErr())) {
+ NS_WARNING(
+ "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste) "
+ "failed");
+ return EditorBase::ToGenericNSResult(ret.unwrapErr());
+ }
+ switch (ret.inspect()) {
+ case ClipboardEventResult::DoDefault:
+ break;
+ case ClipboardEventResult::DefaultPreventedOfPaste:
+ case ClipboardEventResult::IgnoredOrError:
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ case ClipboardEventResult::CopyOrCutHandled:
+ MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
+ }
+
+ // If focus is changed by a "paste" event listener, we should keep handling
+ // the "pasting" in new focused editor because Chrome works as so.
+ const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
+ if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
+ // For the privacy reason, let's top handling it if new focused element is
+ // in different document.
+ if (focusManager->GetFocusedWindow() != GetWindow()) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ RefPtr<EditorBase> editorBase =
+ nsContentUtils::GetActiveEditor(GetPresContext());
+ if (!editorBase || (editorBase->IsHTMLEditor() &&
+ !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ if (editorBase != this) {
+ nsresult rv = editorBase->PasteAsAction(
+ aClipboardType, DispatchPasteEvent::No, aPrincipal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::PasteAsAction(DispatchPasteEvent::No) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+ } else {
+ // The caller must already have dispatched a "paste" event.
+ editActionData.NotifyOfDispatchingClipboardEvent();
+ }
+
+ nsresult rv = HandlePaste(editActionData, aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::HandlePaste() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::PasteAsQuotationAsAction(
+ int32_t aClipboardType, DispatchPasteEvent aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal /* = nullptr */) {
+ MOZ_ASSERT(aClipboardType == nsIClipboard::kGlobalClipboard ||
+ aClipboardType == nsIClipboard::kSelectionClipboard);
+
+ if (IsHTMLEditor() && IsReadonly()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::ePasteAsQuotation,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (aDispatchPasteEvent == DispatchPasteEvent::Yes) {
+ RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();
+
+ Result<ClipboardEventResult, nsresult> ret =
+ DispatchClipboardEventAndUpdateClipboard(ePaste, aClipboardType);
+ if (MOZ_UNLIKELY(ret.isErr())) {
+ NS_WARNING(
+ "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste) "
+ "failed");
+ return EditorBase::ToGenericNSResult(ret.unwrapErr());
+ }
+ switch (ret.inspect()) {
+ case ClipboardEventResult::DoDefault:
+ break;
+ case ClipboardEventResult::DefaultPreventedOfPaste:
+ case ClipboardEventResult::IgnoredOrError:
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ case ClipboardEventResult::CopyOrCutHandled:
+ MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
+ }
+
+ // If focus is changed by a "paste" event listener, we should keep handling
+ // the "pasting" in new focused editor because Chrome works as so.
+ const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
+ if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
+ // For the privacy reason, let's top handling it if new focused element is
+ // in different document.
+ if (focusManager->GetFocusedWindow() != GetWindow()) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ RefPtr<EditorBase> editorBase =
+ nsContentUtils::GetActiveEditor(GetPresContext());
+ if (!editorBase || (editorBase->IsHTMLEditor() &&
+ !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ if (editorBase != this) {
+ nsresult rv = editorBase->PasteAsQuotationAsAction(
+ aClipboardType, DispatchPasteEvent::No, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::PasteAsQuotationAsAction("
+ "DispatchPasteEvent::No) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+ } else {
+ // The caller must already have dispatched a "paste" event.
+ editActionData.NotifyOfDispatchingClipboardEvent();
+ }
+
+ nsresult rv = HandlePasteAsQuotation(editActionData, aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::HandlePasteAsQuotation() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::PasteTransferableAsAction(
+ nsITransferable* aTransferable, DispatchPasteEvent aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal /* = nullptr */) {
+ // FIXME: This may be called as a call of nsIEditor::PasteTransferable.
+ // In this case, we should keep handling the paste even in the readonly mode.
+ if (IsHTMLEditor() && IsReadonly()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::ePaste,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (aDispatchPasteEvent == DispatchPasteEvent::Yes) {
+ RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();
+
+ // Use an invalid value for the clipboard type as data comes from
+ // aTransferable and we don't currently implement a way to put that in the
+ // data transfer in TextEditor yet.
+ Result<ClipboardEventResult, nsresult> ret =
+ DispatchClipboardEventAndUpdateClipboard(
+ ePaste, IsTextEditor() ? -1 : nsIClipboard::kGlobalClipboard);
+ if (MOZ_UNLIKELY(ret.isErr())) {
+ NS_WARNING(
+ "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste) "
+ "failed");
+ return EditorBase::ToGenericNSResult(ret.unwrapErr());
+ }
+ switch (ret.inspect()) {
+ case ClipboardEventResult::DoDefault:
+ break;
+ case ClipboardEventResult::DefaultPreventedOfPaste:
+ case ClipboardEventResult::IgnoredOrError:
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ case ClipboardEventResult::CopyOrCutHandled:
+ MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
+ }
+
+ // If focus is changed by a "paste" event listener, we should keep handling
+ // the "pasting" in new focused editor because Chrome works as so.
+ const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
+ if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
+ // For the privacy reason, let's top handling it if new focused element is
+ // in different document.
+ if (focusManager->GetFocusedWindow() != GetWindow()) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ RefPtr<EditorBase> editorBase =
+ nsContentUtils::GetActiveEditor(GetPresContext());
+ if (!editorBase || (editorBase->IsHTMLEditor() &&
+ !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ if (editorBase != this) {
+ nsresult rv = editorBase->PasteTransferableAsAction(
+ aTransferable, DispatchPasteEvent::No, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::PasteTransferableAsAction("
+ "DispatchPasteEvent::No) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+ } else {
+ // The caller must already have dispatched a "paste" event.
+ editActionData.NotifyOfDispatchingClipboardEvent();
+ }
+
+ if (NS_WARN_IF(!aTransferable)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv = HandlePasteTransferable(editActionData, *aTransferable);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::HandlePasteTransferable() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::PrepareToInsertContent(
+ const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent) {
+ // TODO: Move this method to `EditorBase`.
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ MOZ_ASSERT(aPointToInsert.IsSet());
+
+ EditorDOMPoint pointToInsert(aPointToInsert);
+ if (aDeleteSelectedContent == DeleteSelectedContent::Yes) {
+ AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert);
+ nsresult rv = DeleteSelectionAsSubAction(
+ nsIEditor::eNone,
+ IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteSelectionAsSubAction(eNone) failed");
+ return rv;
+ }
+ }
+
+ nsresult rv = CollapseSelectionTo(pointToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+nsresult EditorBase::InsertTextAt(
+ const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsert.IsSet());
+
+ nsresult rv = PrepareToInsertContent(aPointToInsert, aDeleteSelectedContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::PrepareToInsertContent() failed");
+ return rv;
+ }
+
+ rv = InsertTextAsSubAction(aStringToInsert, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+}
+
+EditorBase::SafeToInsertData EditorBase::IsSafeToInsertData(
+ nsIPrincipal* aSourcePrincipal) const {
+ // Try to determine whether we should use a sanitizing fragment sink
+ RefPtr<Document> destdoc = GetDocument();
+ NS_ASSERTION(destdoc, "Where is our destination doc?");
+
+ nsIDocShell* docShell = nullptr;
+ if (RefPtr<BrowsingContext> bc = destdoc->GetBrowsingContext()) {
+ RefPtr<BrowsingContext> root = bc->Top();
+ MOZ_ASSERT(root, "root should not be null");
+
+ docShell = root->GetDocShell();
+ }
+
+ bool isSafe =
+ docShell && docShell->GetAppType() == nsIDocShell::APP_TYPE_EDITOR;
+
+ if (!isSafe && aSourcePrincipal) {
+ nsIPrincipal* destPrincipal = destdoc->NodePrincipal();
+ NS_ASSERTION(destPrincipal, "How come we don't have a principal?");
+ DebugOnly<nsresult> rvIgnored =
+ aSourcePrincipal->Subsumes(destPrincipal, &isSafe);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsIPrincipal::Subsumes() failed, but ignored");
+ }
+
+ return isSafe ? SafeToInsertData::Yes : SafeToInsertData::No;
+}
+
+NS_IMETHODIMP EditorBase::PasteTransferable(nsITransferable* aTransferable) {
+ nsresult rv =
+ PasteTransferableAsAction(aTransferable, DispatchPasteEvent::Yes);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::PasteTransferableAsAction(DispatchPasteEvent::Yes) failed");
+ return rv;
+}
+
+NS_IMETHODIMP EditorBase::CanPaste(int32_t aClipboardType, bool* aCanPaste) {
+ if (NS_WARN_IF(!aCanPaste)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aCanPaste = CanPaste(aClipboardType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::SetAttribute(Element* aElement,
+ const nsAString& aAttribute,
+ const nsAString& aValue) {
+ if (NS_WARN_IF(aAttribute.IsEmpty()) || NS_WARN_IF(!aElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetAttribute);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<nsAtom> attribute = NS_Atomize(aAttribute);
+ rv = SetAttributeWithTransaction(*aElement, *attribute, aValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::SetAttributeWithTransaction(Element& aElement,
+ nsAtom& aAttribute,
+ const nsAString& aValue) {
+ RefPtr<ChangeAttributeTransaction> transaction =
+ ChangeAttributeTransaction::Create(aElement, aAttribute, aValue);
+ nsresult rv = DoTransactionInternal(transaction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+ return rv;
+}
+
+NS_IMETHODIMP EditorBase::RemoveAttribute(Element* aElement,
+ const nsAString& aAttribute) {
+ if (NS_WARN_IF(aAttribute.IsEmpty()) || NS_WARN_IF(!aElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveAttribute);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<nsAtom> attribute = NS_Atomize(aAttribute);
+ rv = RemoveAttributeWithTransaction(*aElement, *attribute);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::RemoveAttributeWithTransaction(Element& aElement,
+ nsAtom& aAttribute) {
+ if (!aElement.HasAttr(&aAttribute)) {
+ return NS_OK;
+ }
+ RefPtr<ChangeAttributeTransaction> transaction =
+ ChangeAttributeTransaction::CreateToRemove(aElement, aAttribute);
+ nsresult rv = DoTransactionInternal(transaction);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+ return rv;
+}
+
+nsresult EditorBase::MarkElementDirty(Element& aElement) const {
+ // Mark the node dirty, but not for webpages (bug 599983)
+ if (!OutputsMozDirty()) {
+ return NS_OK;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::mozdirty, u""_ns, false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::mozdirty) failed, but ignored");
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetInlineSpellChecker(
+ bool aAutoCreate, nsIInlineSpellChecker** aInlineSpellChecker) {
+ if (NS_WARN_IF(!aInlineSpellChecker)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mDidPreDestroy) {
+ // Don't allow people to get or create the spell checker once the editor
+ // is going away.
+ *aInlineSpellChecker = nullptr;
+ return aAutoCreate ? NS_ERROR_NOT_AVAILABLE : NS_OK;
+ }
+
+ // We don't want to show the spell checking UI if there are no spell check
+ // dictionaries available.
+ if (!mozInlineSpellChecker::CanEnableInlineSpellChecking()) {
+ *aInlineSpellChecker = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mInlineSpellChecker && aAutoCreate) {
+ mInlineSpellChecker = new mozInlineSpellChecker();
+ }
+
+ if (mInlineSpellChecker) {
+ nsresult rv = mInlineSpellChecker->Init(this);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("mozInlineSpellChecker::Init() failed");
+ mInlineSpellChecker = nullptr;
+ return rv;
+ }
+ }
+
+ *aInlineSpellChecker = do_AddRef(mInlineSpellChecker).take();
+ return NS_OK;
+}
+
+void EditorBase::SyncRealTimeSpell() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ bool enable = GetDesiredSpellCheckState();
+
+ // Initializes mInlineSpellChecker
+ nsCOMPtr<nsIInlineSpellChecker> spellChecker;
+ DebugOnly<nsresult> rvIgnored =
+ GetInlineSpellChecker(enable, getter_AddRefs(spellChecker));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::GetInlineSpellChecker() failed, but ignored");
+
+ if (mInlineSpellChecker) {
+ if (!mSpellCheckerDictionaryUpdated && enable) {
+ DebugOnly<nsresult> rvIgnored =
+ mInlineSpellChecker->UpdateCurrentDictionary();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "mozInlineSpellChecker::UpdateCurrentDictionary() "
+ "failed, but ignored");
+ mSpellCheckerDictionaryUpdated = true;
+ }
+
+ // We might have a mInlineSpellChecker even if there are no dictionaries
+ // available since we don't destroy the mInlineSpellChecker when the last
+ // dictionariy is removed, but in that case spellChecker is null
+ DebugOnly<nsresult> rvIgnored =
+ mInlineSpellChecker->SetEnableRealTimeSpell(enable && spellChecker);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "mozInlineSpellChecker::SetEnableRealTimeSpell() failed, but ignored");
+ }
+}
+
+NS_IMETHODIMP EditorBase::SetSpellcheckUserOverride(bool enable) {
+ mSpellcheckCheckboxState = enable ? eTriTrue : eTriFalse;
+ SyncRealTimeSpell();
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::InsertNode(nsINode* aNodeToInsert,
+ nsINode* aContainer, uint32_t aOffset,
+ bool aPreserveSelection,
+ uint8_t aOptionalArgCount) {
+ MOZ_DIAGNOSTIC_ASSERT(IsHTMLEditor());
+
+ nsCOMPtr<nsIContent> contentToInsert =
+ nsIContent::FromNodeOrNull(aNodeToInsert);
+ if (NS_WARN_IF(!contentToInsert) || NS_WARN_IF(!aContainer)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertNode);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Make dispatch `input` event after stopping preserving selection.
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this,
+ ScrollSelectionIntoView::No, // not a user interaction
+ __FUNCTION__);
+
+ Maybe<AutoTransactionsConserveSelection> preseveSelection;
+ if (aOptionalArgCount && aPreserveSelection) {
+ preseveSelection.emplace(*this);
+ }
+
+ const uint32_t offset = std::min(aOffset, aContainer->Length());
+ Result<CreateContentResult, nsresult> insertContentResult =
+ InsertNodeWithTransaction(*contentToInsert,
+ EditorDOMPoint(aContainer, offset));
+ if (MOZ_UNLIKELY(insertContentResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(insertContentResult.unwrapErr());
+ }
+ rv = insertContentResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateContentResult::SuggestCaretPointTo() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateContentResult::SuggestCaretPointTo() failed, but ignored");
+ return NS_OK;
+}
+
+template <typename ContentNodeType>
+Result<CreateNodeResultBase<ContentNodeType>, nsresult>
+EditorBase::InsertNodeWithTransaction(ContentNodeType& aContentToInsert,
+ const EditorDOMPoint& aPointToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT_IF(IsTextEditor(), !aContentToInsert.IsText());
+
+ if (NS_WARN_IF(!aPointToInsert.IsSet())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ RefPtr<InsertNodeTransaction> transaction =
+ InsertNodeTransaction::Create(*this, aContentToInsert, aPointToInsert);
+ nsresult rv = DoTransactionInternal(transaction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+
+ DebugOnly<nsresult> rvIgnored =
+ RangeUpdaterRef().SelAdjInsertNode(aPointToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "RangeUpdater::SelAdjInsertNode() failed, but ignored");
+
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_WARN_IF(aContentToInsert.GetParentNode() !=
+ aPointToInsert.GetContainer())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+
+ if (IsHTMLEditor()) {
+ TopLevelEditSubActionDataRef().DidInsertContent(*this, aContentToInsert);
+ }
+
+ return CreateNodeResultBase<ContentNodeType>(
+ &aContentToInsert, transaction->SuggestPointToPutCaret<EditorDOMPoint>());
+}
+
+Result<CreateElementResult, nsresult>
+EditorBase::InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ const EditorDOMPoint& aPointToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsHTMLEditor() || !aPointToInsert.IsInTextNode());
+
+ if (MOZ_UNLIKELY(!aPointToInsert.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ EditorDOMPoint pointToInsert;
+ if (IsTextEditor()) {
+ pointToInsert = aPointToInsert;
+ } else {
+ Result<EditorDOMPoint, nsresult> maybePointToInsert =
+ MOZ_KnownLive(AsHTMLEditor())->PrepareToInsertBRElement(aPointToInsert);
+ if (maybePointToInsert.isErr()) {
+ return maybePointToInsert.propagateErr();
+ }
+ MOZ_ASSERT(maybePointToInsert.inspect().IsSetAndValid());
+ pointToInsert = maybePointToInsert.unwrap();
+ }
+
+ RefPtr<Element> newBRElement = CreateHTMLContent(nsGkAtoms::br);
+ if (NS_WARN_IF(!newBRElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ newBRElement->SetFlags(NS_PADDING_FOR_EMPTY_LAST_LINE);
+
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertNodeWithTransaction<Element>(*newBRElement, pointToInsert);
+ NS_WARNING_ASSERTION(insertBRElementResult.isOk(),
+ "EditorBase::InsertNodeWithTransaction() failed");
+ return insertBRElementResult;
+}
+
+NS_IMETHODIMP EditorBase::DeleteNode(nsINode* aNode, bool aPreserveSelection,
+ uint8_t aOptionalArgCount) {
+ MOZ_ASSERT_UNREACHABLE("Do not use this API with TextEditor");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult EditorBase::DeleteNodeWithTransaction(nsIContent& aContent) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT_IF(IsTextEditor(), !aContent.IsText());
+
+ // Do nothing if the node is read-only.
+ if (IsHTMLEditor() && NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(aContent))) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteNode, nsIEditor::ePrevious, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ if (IsHTMLEditor()) {
+ TopLevelEditSubActionDataRef().WillDeleteContent(*this, aContent);
+ }
+
+ // FYI: DeleteNodeTransaction grabs aContent while it's alive. So, it's safe
+ // to refer aContent even after calling DoTransaction().
+ RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
+ DeleteNodeTransaction::MaybeCreate(*this, aContent);
+ NS_WARNING_ASSERTION(deleteNodeTransaction,
+ "DeleteNodeTransaction::MaybeCreate() failed");
+ nsresult rv;
+ if (deleteNodeTransaction) {
+ rv = DoTransactionInternal(deleteNodeTransaction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+
+ if (mTextServicesDocument && NS_SUCCEEDED(rv)) {
+ RefPtr<TextServicesDocument> textServicesDocument = mTextServicesDocument;
+ textServicesDocument->DidDeleteContent(aContent);
+ }
+ } else {
+ rv = NS_ERROR_FAILURE;
+ }
+
+ if (!mActionListeners.IsEmpty()) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored = listener->DidDeleteNode(&aContent, rv);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::DidDeleteNode() failed, but ignored");
+ }
+ }
+
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : rv;
+}
+
+NS_IMETHODIMP EditorBase::NotifySelectionChanged(Document* aDocument,
+ Selection* aSelection,
+ int16_t aReason,
+ int32_t aAmount) {
+ if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mTextInputListener) {
+ RefPtr<TextInputListener> textInputListener = mTextInputListener;
+ textInputListener->OnSelectionChange(*aSelection, aReason);
+ }
+
+ if (mIMEContentObserver) {
+ RefPtr<IMEContentObserver> observer = mIMEContentObserver;
+ observer->OnSelectionChange(*aSelection);
+ }
+
+ return NS_OK;
+}
+
+void EditorBase::NotifyEditorObservers(
+ NotificationForEditorObservers aNotification) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ switch (aNotification) {
+ case eNotifyEditorObserversOfEnd:
+ mIsInEditSubAction = false;
+
+ if (mEditActionData) {
+ mEditActionData->MarkAsHandled();
+ }
+
+ if (mTextInputListener) {
+ // TODO: TextInputListener::OnEditActionHandled() may return
+ // NS_ERROR_OUT_OF_MEMORY. If so and if
+ // TextControlState::SetValue() setting value with us, we should
+ // return the result to EditorBase::ReplaceTextAsAction(),
+ // EditorBase::DeleteSelectionAsAction() and
+ // TextEditor::InsertTextAsAction(). However, it requires a lot
+ // of changes in editor classes, but it's not so important since
+ // editor does not use fallible allocation. Therefore, normally,
+ // the process must be crashed anyway.
+ RefPtr<TextInputListener> listener = mTextInputListener;
+ nsresult rv =
+ listener->OnEditActionHandled(MOZ_KnownLive(*AsTextEditor()));
+ MOZ_RELEASE_ASSERT(rv != NS_ERROR_OUT_OF_MEMORY,
+ "Setting value failed due to out of memory");
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "TextInputListener::OnEditActionHandled() failed, but ignored");
+ }
+
+ if (mIMEContentObserver) {
+ RefPtr<IMEContentObserver> observer = mIMEContentObserver;
+ observer->OnEditActionHandled();
+ }
+
+ if (!mDispatchInputEvent || IsEditActionAborted() ||
+ IsEditActionCanceled()) {
+ break;
+ }
+
+ DispatchInputEvent();
+ break;
+ case eNotifyEditorObserversOfBefore:
+ if (NS_WARN_IF(mIsInEditSubAction)) {
+ return;
+ }
+
+ mIsInEditSubAction = true;
+
+ if (mIMEContentObserver) {
+ RefPtr<IMEContentObserver> observer = mIMEContentObserver;
+ observer->BeforeEditAction();
+ }
+ return;
+ case eNotifyEditorObserversOfCancel:
+ mIsInEditSubAction = false;
+
+ if (mEditActionData) {
+ mEditActionData->MarkAsHandled();
+ }
+
+ if (mIMEContentObserver) {
+ RefPtr<IMEContentObserver> observer = mIMEContentObserver;
+ observer->CancelEditAction();
+ }
+ break;
+ default:
+ MOZ_CRASH("Handle all notifications here");
+ break;
+ }
+
+ if (IsHTMLEditor() && !Destroyed()) {
+ // We may need to show resizing handles or update existing ones after
+ // all transactions are done. This way of doing is preferred to DOM
+ // mutation events listeners because all the changes the user can apply
+ // to a document may result in multiple events, some of them quite hard
+ // to listen too (in particular when an ancestor of the selection is
+ // changed but the selection itself is not changed).
+ DebugOnly<nsresult> rvIgnored =
+ MOZ_KnownLive(AsHTMLEditor())->RefreshEditingUI();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::RefreshEditingUI() failed, but ignored");
+ }
+}
+
+void EditorBase::DispatchInputEvent() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsEditActionCanceled(),
+ "If preceding beforeinput event is canceled, we shouldn't "
+ "dispatch input event");
+ MOZ_ASSERT(
+ !ShouldAlreadyHaveHandledBeforeInputEventDispatching(),
+ "We've not handled beforeinput event but trying to dispatch input event");
+
+ // We don't need to dispatch multiple input events if there is a pending
+ // input event. However, it may have different event target. If we resolved
+ // this issue, we need to manage the pending events in an array. But it's
+ // overwork. We don't need to do it for the very rare case.
+ // TODO: However, we start to set InputEvent.inputType. So, each "input"
+ // event now notifies web app each change. So, perhaps, we should
+ // not omit input events.
+
+ RefPtr<Element> targetElement = GetInputEventTargetElement();
+ if (NS_WARN_IF(!targetElement)) {
+ return;
+ }
+ RefPtr<DataTransfer> dataTransfer = GetInputEventDataTransfer();
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
+ targetElement, eEditorInput, ToInputType(GetEditAction()), this,
+ dataTransfer ? InputEventOptions(dataTransfer,
+ InputEventOptions::NeverCancelable::No)
+ : InputEventOptions(GetInputEventData(),
+ InputEventOptions::NeverCancelable::No));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsContentUtils::DispatchInputEvent() failed, but ignored");
+}
+
+NS_IMETHODIMP EditorBase::AddEditActionListener(
+ nsIEditActionListener* aListener) {
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // If given edit action listener is text services document for the inline
+ // spell checker, store it as reference of concrete class for performance
+ // reason.
+ if (mInlineSpellChecker) {
+ EditorSpellCheck* editorSpellCheck =
+ mInlineSpellChecker->GetEditorSpellCheck();
+ if (editorSpellCheck) {
+ mozSpellChecker* spellChecker = editorSpellCheck->GetSpellChecker();
+ if (spellChecker) {
+ TextServicesDocument* textServicesDocument =
+ spellChecker->GetTextServicesDocument();
+ if (static_cast<nsIEditActionListener*>(textServicesDocument) ==
+ aListener) {
+ mTextServicesDocument = textServicesDocument;
+ return NS_OK;
+ }
+ }
+ }
+ }
+
+ // Make sure the listener isn't already on the list
+ if (!mActionListeners.Contains(aListener)) {
+ mActionListeners.AppendElement(*aListener);
+ NS_WARNING_ASSERTION(
+ mActionListeners.Length() != 1,
+ "nsIEditActionListener installed, this editor becomes slower");
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::RemoveEditActionListener(
+ nsIEditActionListener* aListener) {
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (static_cast<nsIEditActionListener*>(mTextServicesDocument) == aListener) {
+ mTextServicesDocument = nullptr;
+ return NS_OK;
+ }
+
+ NS_WARNING_ASSERTION(mActionListeners.Length() != 1,
+ "All nsIEditActionListeners have been removed, this "
+ "editor becomes faster");
+ mActionListeners.RemoveElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::AddDocumentStateListener(
+ nsIDocumentStateListener* aListener) {
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!mDocStateListeners.Contains(aListener)) {
+ mDocStateListeners.AppendElement(*aListener);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::RemoveDocumentStateListener(
+ nsIDocumentStateListener* aListener) {
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mDocStateListeners.RemoveElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::ForceCompositionEnd() {
+ nsresult rv = CommitComposition();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CommitComposition() failed");
+ return rv;
+}
+
+nsresult EditorBase::CommitComposition() {
+ nsPresContext* presContext = GetPresContext();
+ if (NS_WARN_IF(!presContext)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (!mComposition) {
+ return NS_OK;
+ }
+ nsresult rv =
+ IMEStateManager::NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, presContext);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "IMEStateManager::NotifyIME() failed");
+ return rv;
+}
+
+nsresult EditorBase::GetPreferredIMEState(IMEState* aState) {
+ if (NS_WARN_IF(!aState)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aState->mEnabled = IMEEnabled::Enabled;
+ aState->mOpen = IMEState::DONT_CHANGE_OPEN_STATE;
+
+ if (IsReadonly()) {
+ aState->mEnabled = IMEEnabled::Disabled;
+ return NS_OK;
+ }
+
+ Element* rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIFrame* frameForRootElement = rootElement->GetPrimaryFrame();
+ if (NS_WARN_IF(!frameForRootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ switch (frameForRootElement->StyleUIReset()->mIMEMode) {
+ case StyleImeMode::Auto:
+ if (IsPasswordEditor()) {
+ aState->mEnabled = IMEEnabled::Password;
+ }
+ break;
+ case StyleImeMode::Disabled:
+ // we should use password state for |ime-mode: disabled;|.
+ aState->mEnabled = IMEEnabled::Password;
+ break;
+ case StyleImeMode::Active:
+ aState->mOpen = IMEState::OPEN;
+ break;
+ case StyleImeMode::Inactive:
+ aState->mOpen = IMEState::CLOSED;
+ break;
+ case StyleImeMode::Normal:
+ break;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetComposing(bool* aResult) {
+ if (NS_WARN_IF(!aResult)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aResult = IsIMEComposing();
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetRootElement(Element** aRootElement) {
+ if (NS_WARN_IF(!aRootElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aRootElement = do_AddRef(mRootElement).take();
+ return NS_WARN_IF(!*aRootElement) ? NS_ERROR_NOT_AVAILABLE : NS_OK;
+}
+
+void EditorBase::OnStartToHandleTopLevelEditSubAction(
+ EditSubAction aTopLevelEditSubAction,
+ nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRv.Failed());
+ mEditActionData->SetTopLevelEditSubAction(aTopLevelEditSubAction,
+ aDirectionOfTopLevelEditSubAction);
+}
+
+nsresult EditorBase::OnEndHandlingTopLevelEditSubAction() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ mEditActionData->SetTopLevelEditSubAction(EditSubAction::eNone, eNone);
+ return NS_OK;
+}
+
+void EditorBase::DoInsertText(Text& aText, uint32_t aOffset,
+ const nsAString& aStringToInsert,
+ ErrorResult& aRv) {
+ aText.InsertData(aOffset, aStringToInsert, aRv);
+ if (NS_WARN_IF(Destroyed())) {
+ aRv = NS_ERROR_EDITOR_DESTROYED;
+ return;
+ }
+ if (aRv.Failed()) {
+ NS_WARNING("Text::InsertData() failed");
+ return;
+ }
+ if (IsTextEditor() && !aStringToInsert.IsEmpty()) {
+ aRv = MOZ_KnownLive(AsTextEditor())
+ ->DidInsertText(aText.TextLength(), aOffset,
+ aStringToInsert.Length());
+ NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed");
+ }
+}
+
+void EditorBase::DoDeleteText(Text& aText, uint32_t aOffset, uint32_t aCount,
+ ErrorResult& aRv) {
+ if (IsTextEditor() && aCount > 0) {
+ AsTextEditor()->WillDeleteText(aText.TextLength(), aOffset, aCount);
+ }
+ aText.DeleteData(aOffset, aCount, aRv);
+ if (NS_WARN_IF(Destroyed())) {
+ aRv = NS_ERROR_EDITOR_DESTROYED;
+ return;
+ }
+ NS_WARNING_ASSERTION(!aRv.Failed(), "Text::DeleteData() failed");
+}
+
+void EditorBase::DoReplaceText(Text& aText, uint32_t aOffset, uint32_t aCount,
+ const nsAString& aStringToInsert,
+ ErrorResult& aRv) {
+ if (IsTextEditor() && aCount > 0) {
+ AsTextEditor()->WillDeleteText(aText.TextLength(), aOffset, aCount);
+ }
+ aText.ReplaceData(aOffset, aCount, aStringToInsert, aRv);
+ if (NS_WARN_IF(Destroyed())) {
+ aRv = NS_ERROR_EDITOR_DESTROYED;
+ return;
+ }
+ if (aRv.Failed()) {
+ NS_WARNING("Text::ReplaceData() failed");
+ return;
+ }
+ if (IsTextEditor() && !aStringToInsert.IsEmpty()) {
+ aRv = MOZ_KnownLive(AsTextEditor())
+ ->DidInsertText(aText.TextLength(), aOffset,
+ aStringToInsert.Length());
+ NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed");
+ }
+}
+
+void EditorBase::DoSetText(Text& aText, const nsAString& aStringToSet,
+ ErrorResult& aRv) {
+ if (IsTextEditor()) {
+ uint32_t length = aText.TextLength();
+ if (length > 0) {
+ AsTextEditor()->WillDeleteText(length, 0, length);
+ }
+ }
+ aText.SetData(aStringToSet, aRv);
+ if (NS_WARN_IF(Destroyed())) {
+ aRv = NS_ERROR_EDITOR_DESTROYED;
+ return;
+ }
+ if (aRv.Failed()) {
+ NS_WARNING("Text::SetData() failed");
+ return;
+ }
+ if (IsTextEditor() && !aStringToSet.IsEmpty()) {
+ aRv = MOZ_KnownLive(AsTextEditor())
+ ->DidInsertText(aText.Length(), 0, aStringToSet.Length());
+ NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed");
+ }
+}
+
+nsresult EditorBase::CloneAttributeWithTransaction(nsAtom& aAttribute,
+ Element& aDestElement,
+ Element& aSourceElement) {
+ nsAutoString attrValue;
+ if (aSourceElement.GetAttr(&aAttribute, attrValue)) {
+ nsresult rv =
+ SetAttributeWithTransaction(aDestElement, aAttribute, attrValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction() failed");
+ return rv;
+ }
+ nsresult rv = RemoveAttributeWithTransaction(aDestElement, aAttribute);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP EditorBase::CloneAttributes(Element* aDestElement,
+ Element* aSourceElement) {
+ if (NS_WARN_IF(!aDestElement) || NS_WARN_IF(!aSourceElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetAttribute);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ CloneAttributesWithTransaction(*aDestElement, *aSourceElement);
+
+ return NS_OK;
+}
+
+void EditorBase::CloneAttributesWithTransaction(Element& aDestElement,
+ Element& aSourceElement) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ // Use transaction system for undo only if destination is already in the
+ // document
+ Element* rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return;
+ }
+
+ OwningNonNull<Element> destElement(aDestElement);
+ OwningNonNull<Element> sourceElement(aSourceElement);
+ bool isDestElementInBody = rootElement->Contains(destElement);
+
+ // Clear existing attributes
+ RefPtr<nsDOMAttributeMap> destAttributes = destElement->Attributes();
+ while (RefPtr<Attr> attr = destAttributes->Item(0)) {
+ if (isDestElementInBody) {
+ DebugOnly<nsresult> rvIgnored = RemoveAttributeWithTransaction(
+ destElement, MOZ_KnownLive(*attr->NodeInfo()->NameAtom()));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::RemoveAttributeWithTransaction() failed, but ignored");
+ } else {
+ DebugOnly<nsresult> rvIgnored = destElement->UnsetAttr(
+ kNameSpaceID_None, attr->NodeInfo()->NameAtom(), true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Element::UnsetAttr() failed, but ignored");
+ }
+ }
+
+ // Set just the attributes that the source element has
+ RefPtr<nsDOMAttributeMap> sourceAttributes = sourceElement->Attributes();
+ uint32_t sourceCount = sourceAttributes->Length();
+ for (uint32_t i = 0; i < sourceCount; i++) {
+ RefPtr<Attr> attr = sourceAttributes->Item(i);
+ nsAutoString value;
+ attr->GetValue(value);
+ if (isDestElementInBody) {
+ DebugOnly<nsresult> rvIgnored = SetAttributeOrEquivalent(
+ destElement, MOZ_KnownLive(attr->NodeInfo()->NameAtom()), value,
+ false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::SetAttributeOrEquivalent() failed, but ignored");
+ } else {
+ // The element is not inserted in the document yet, we don't want to put
+ // a transaction on the UndoStack
+ DebugOnly<nsresult> rvIgnored = SetAttributeOrEquivalent(
+ destElement, MOZ_KnownLive(attr->NodeInfo()->NameAtom()), value,
+ true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::SetAttributeOrEquivalent() failed, but ignored");
+ }
+ }
+}
+
+nsresult EditorBase::ScrollSelectionFocusIntoView() const {
+ nsISelectionController* selectionController = GetSelectionController();
+ if (!selectionController) {
+ return NS_OK;
+ }
+
+ DebugOnly<nsresult> rvIgnored = selectionController->ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_FOCUS_REGION,
+ nsISelectionController::SCROLL_OVERFLOW_HIDDEN);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISelectionController::ScrollSelectionIntoView() failed, but ignored");
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+Result<InsertTextResult, nsresult> EditorBase::InsertTextWithTransaction(
+ Document& aDocument, const nsAString& aStringToInsert,
+ const EditorDOMPoint& aPointToInsert) {
+ if (NS_WARN_IF(!aPointToInsert.IsSet())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) {
+ return InsertTextResult();
+ }
+
+ // In some cases, the node may be the anonymous div element or a padding
+ // <br> element for empty last line. Let's try to look for better insertion
+ // point in the nearest text node if there is.
+ EditorDOMPoint pointToInsert = [&]() {
+ if (IsTextEditor()) {
+ return AsTextEditor()->FindBetterInsertionPoint(aPointToInsert);
+ }
+ return aPointToInsert
+ .GetPointInTextNodeIfPointingAroundTextNode<EditorDOMPoint>();
+ }();
+
+ if (ShouldHandleIMEComposition()) {
+ if (!pointToInsert.IsInTextNode()) {
+ // create a text node
+ RefPtr<nsTextNode> newTextNode = CreateTextNode(u""_ns);
+ if (NS_WARN_IF(!newTextNode)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // then we insert it into the dom tree
+ Result<CreateTextResult, nsresult> insertTextNodeResult =
+ InsertNodeWithTransaction<Text>(*newTextNode, pointToInsert);
+ if (MOZ_UNLIKELY(insertTextNodeResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertTextNodeResult.propagateErr();
+ }
+ insertTextNodeResult.unwrap().IgnoreCaretPointSuggestion();
+ pointToInsert.Set(newTextNode, 0u);
+ }
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextIntoTextNodeWithTransaction(aStringToInsert,
+ pointToInsert.AsInText());
+ NS_WARNING_ASSERTION(
+ insertTextResult.isOk(),
+ "EditorBase::InsertTextIntoTextNodeWithTransaction() failed");
+ return insertTextResult;
+ }
+
+ if (pointToInsert.IsInTextNode()) {
+ // we are inserting text into an existing text node.
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextIntoTextNodeWithTransaction(aStringToInsert,
+ pointToInsert.AsInText());
+ NS_WARNING_ASSERTION(
+ insertTextResult.isOk(),
+ "EditorBase::InsertTextIntoTextNodeWithTransaction() failed");
+ return insertTextResult;
+ }
+
+ // we are inserting text into a non-text node. first we have to create a
+ // textnode (this also populates it with the text)
+ RefPtr<nsTextNode> newTextNode = CreateTextNode(aStringToInsert);
+ if (NS_WARN_IF(!newTextNode)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // then we insert it into the dom tree
+ Result<CreateTextResult, nsresult> insertTextNodeResult =
+ InsertNodeWithTransaction<Text>(*newTextNode, pointToInsert);
+ if (MOZ_UNLIKELY(insertTextNodeResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return Err(insertTextNodeResult.unwrapErr());
+ }
+ insertTextNodeResult.unwrap().IgnoreCaretPointSuggestion();
+ if (NS_WARN_IF(!newTextNode->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ return InsertTextResult(EditorDOMPointInText::AtEndOf(*newTextNode),
+ EditorDOMPoint::AtEndOf(*newTextNode));
+}
+
+static bool TextFragmentBeginsWithStringAtOffset(
+ const nsTextFragment& aTextFragment, const uint32_t aOffset,
+ const nsAString& aString) {
+ const uint32_t stringLength = aString.Length();
+
+ if (aOffset + stringLength > aTextFragment.GetLength()) {
+ return false;
+ }
+
+ if (aTextFragment.Is2b()) {
+ return aString.Equals(aTextFragment.Get2b() + aOffset);
+ }
+
+ return aString.EqualsLatin1(aTextFragment.Get1b() + aOffset, stringLength);
+}
+
+static std::tuple<EditorDOMPointInText, EditorDOMPointInText>
+AdjustTextInsertionRange(const EditorDOMPointInText& aInsertedPoint,
+ const nsAString& aInsertedString) {
+ if (TextFragmentBeginsWithStringAtOffset(
+ aInsertedPoint.ContainerAs<Text>()->TextFragment(),
+ aInsertedPoint.Offset(), aInsertedString)) {
+ return {aInsertedPoint,
+ EditorDOMPointInText(
+ aInsertedPoint.ContainerAs<Text>(),
+ aInsertedPoint.Offset() + aInsertedString.Length())};
+ }
+
+ return {EditorDOMPointInText(aInsertedPoint.ContainerAs<Text>(), 0),
+ EditorDOMPointInText::AtEndOf(*aInsertedPoint.ContainerAs<Text>())};
+}
+
+std::tuple<EditorDOMPointInText, EditorDOMPointInText>
+EditorBase::ComputeInsertedRange(const EditorDOMPointInText& aInsertedPoint,
+ const nsAString& aInsertedString) const {
+ MOZ_ASSERT(aInsertedPoint.IsSet());
+
+ // The DOM was potentially modified during the transaction. This is possible
+ // through mutation event listeners. That is, the node could've been removed
+ // from the doc or otherwise modified.
+ if (!MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED)) {
+ EditorDOMPointInText endOfInsertion(
+ aInsertedPoint.ContainerAs<Text>(),
+ aInsertedPoint.Offset() + aInsertedString.Length());
+ return {aInsertedPoint, endOfInsertion};
+ }
+ if (aInsertedPoint.ContainerAs<Text>()->IsInComposedDoc()) {
+ EditorDOMPointInText begin, end;
+ return AdjustTextInsertionRange(aInsertedPoint, aInsertedString);
+ }
+ return {EditorDOMPointInText(), EditorDOMPointInText()};
+}
+
+Result<InsertTextResult, nsresult>
+EditorBase::InsertTextIntoTextNodeWithTransaction(
+ const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ RefPtr<EditTransactionBase> transaction;
+ bool isIMETransaction = false;
+ if (ShouldHandleIMEComposition()) {
+ transaction =
+ CompositionTransaction::Create(*this, aStringToInsert, aPointToInsert);
+ isIMETransaction = true;
+ } else {
+ transaction =
+ InsertTextTransaction::Create(*this, aStringToInsert, aPointToInsert);
+ }
+
+ // XXX We may not need these view batches anymore. This is handled at a
+ // higher level now I believe.
+ BeginUpdateViewBatch(__FUNCTION__);
+ nsresult rv = DoTransactionInternal(transaction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+ EndUpdateViewBatch(__FUNCTION__);
+
+ // Don't check whether we've been destroyed here because we need to notify
+ // listeners and observers below even if we've already destroyed.
+
+ auto pointToInsert = [&]() -> EditorDOMPointInText {
+ if (!isIMETransaction) {
+ return aPointToInsert;
+ }
+ if (NS_WARN_IF(!mComposition->GetContainerTextNode())) {
+ return aPointToInsert;
+ }
+ return EditorDOMPointInText(
+ mComposition->GetContainerTextNode(),
+ std::min(mComposition->XPOffsetInTextNode(),
+ mComposition->GetContainerTextNode()->TextDataLength()));
+ }();
+
+ EditorDOMPointInText endOfInsertedText(
+ pointToInsert.ContainerAs<Text>(),
+ pointToInsert.Offset() + aStringToInsert.Length());
+
+ if (IsHTMLEditor()) {
+ auto [begin, end] = ComputeInsertedRange(pointToInsert, aStringToInsert);
+ if (begin.IsSet() && end.IsSet()) {
+ TopLevelEditSubActionDataRef().DidInsertText(
+ *this, begin.To<EditorRawDOMPoint>(), end.To<EditorRawDOMPoint>());
+ }
+ if (isIMETransaction) {
+ // Let's mark the text node as "modified frequently" if it interact with
+ // IME since non-ASCII character may be inserted into it in most cases.
+ pointToInsert.ContainerAs<Text>()->MarkAsMaybeModifiedFrequently();
+ }
+ // XXX Should we update endOfInsertedText here?
+ }
+
+ // let listeners know what happened
+ if (!mActionListeners.IsEmpty()) {
+ for (auto& listener : mActionListeners.Clone()) {
+ // TODO: might need adaptation because of mutation event listeners called
+ // during `DoTransactionInternal`.
+ DebugOnly<nsresult> rvIgnored = listener->DidInsertText(
+ pointToInsert.ContainerAs<Text>(),
+ static_cast<int32_t>(pointToInsert.Offset()), aStringToInsert, rv);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::DidInsertText() failed, but ignored");
+ }
+ }
+
+ // Added some cruft here for bug 43366. Layout was crashing because we left
+ // an empty text node lying around in the document. So I delete empty text
+ // nodes caused by IME. I have to mark the IME transaction as "fixed", which
+ // means that furure IME txns won't merge with it. This is because we don't
+ // want future IME txns trying to put their text into a node that is no
+ // longer in the document. This does not break undo/redo, because all these
+ // txns are wrapped in a parent PlaceHolder txn, and placeholder txns are
+ // already savvy to having multiple ime txns inside them.
+
+ // Delete empty IME text node if there is one
+ if (IsHTMLEditor() && isIMETransaction && mComposition) {
+ RefPtr<Text> textNode = mComposition->GetContainerTextNode();
+ if (textNode && !textNode->Length()) {
+ rv = DeleteNodeWithTransaction(*textNode);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeTransaction() failed");
+ if (MOZ_LIKELY(!textNode->IsInComposedDoc())) {
+ mComposition->OnTextNodeRemoved();
+ }
+ static_cast<CompositionTransaction*>(transaction.get())->MarkFixed();
+ }
+ }
+
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ InsertTextTransaction* const insertTextTransaction =
+ transaction->GetAsInsertTextTransaction();
+ return insertTextTransaction
+ ? InsertTextResult(std::move(endOfInsertedText),
+ insertTextTransaction
+ ->SuggestPointToPutCaret<EditorDOMPoint>())
+ : InsertTextResult(std::move(endOfInsertedText));
+}
+
+nsresult EditorBase::NotifyDocumentListeners(
+ TDocumentListenerNotification aNotificationType) {
+ switch (aNotificationType) {
+ case eDocumentCreated:
+ if (IsTextEditor()) {
+ return NS_OK;
+ }
+ if (RefPtr<ComposerCommandsUpdater> composerCommandsUpdate =
+ AsHTMLEditor()->mComposerCommandsUpdater) {
+ composerCommandsUpdate->OnHTMLEditorCreated();
+ }
+ return NS_OK;
+
+ case eDocumentToBeDestroyed: {
+ RefPtr<ComposerCommandsUpdater> composerCommandsUpdate =
+ IsHTMLEditor() ? AsHTMLEditor()->mComposerCommandsUpdater : nullptr;
+ if (!mDocStateListeners.Length() && !composerCommandsUpdate) {
+ return NS_OK;
+ }
+ // Needs to store all listeners before notifying ComposerCommandsUpdate
+ // since notifying it might change mDocStateListeners.
+ const AutoDocumentStateListenerArray listeners(
+ mDocStateListeners.Clone());
+ if (composerCommandsUpdate) {
+ composerCommandsUpdate->OnBeforeHTMLEditorDestroyed();
+ }
+ for (auto& listener : listeners) {
+ // MOZ_KnownLive because 'listeners' is guaranteed to
+ // keep it alive.
+ //
+ // This can go away once
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 is fixed.
+ nsresult rv = MOZ_KnownLive(listener)->NotifyDocumentWillBeDestroyed();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "nsIDocumentStateListener::NotifyDocumentWillBeDestroyed() "
+ "failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+ }
+ case eDocumentStateChanged: {
+ bool docIsDirty;
+ nsresult rv = GetDocumentModified(&docIsDirty);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::GetDocumentModified() failed");
+ return rv;
+ }
+
+ if (static_cast<int8_t>(docIsDirty) == mDocDirtyState) {
+ return NS_OK;
+ }
+
+ mDocDirtyState = docIsDirty;
+
+ RefPtr<ComposerCommandsUpdater> composerCommandsUpdate =
+ IsHTMLEditor() ? AsHTMLEditor()->mComposerCommandsUpdater : nullptr;
+ if (!mDocStateListeners.Length() && !composerCommandsUpdate) {
+ return NS_OK;
+ }
+ // Needs to store all listeners before notifying ComposerCommandsUpdate
+ // since notifying it might change mDocStateListeners.
+ const AutoDocumentStateListenerArray listeners(
+ mDocStateListeners.Clone());
+ if (composerCommandsUpdate) {
+ composerCommandsUpdate->OnHTMLEditorDirtyStateChanged(mDocDirtyState);
+ }
+ for (auto& listener : listeners) {
+ // MOZ_KnownLive because 'listeners' is guaranteed to
+ // keep it alive.
+ //
+ // This can go away once
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 is fixed.
+ nsresult rv =
+ MOZ_KnownLive(listener)->NotifyDocumentStateChanged(mDocDirtyState);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "nsIDocumentStateListener::NotifyDocumentStateChanged() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown notification");
+ return NS_ERROR_FAILURE;
+ }
+}
+
+nsresult EditorBase::SetTextNodeWithoutTransaction(const nsAString& aString,
+ Text& aTextNode) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTextEditor());
+ MOZ_ASSERT(!IsUndoRedoEnabled());
+
+ const uint32_t length = aTextNode.Length();
+
+ // Let listeners know what's up
+ if (!mActionListeners.IsEmpty() && length) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored =
+ listener->WillDeleteText(MOZ_KnownLive(&aTextNode), 0, length);
+ if (NS_WARN_IF(Destroyed())) {
+ NS_WARNING(
+ "nsIEditActionListener::WillDeleteText() failed, but ignored");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ }
+ }
+
+ // We don't support undo here, so we don't really need all of the transaction
+ // machinery, therefore we can run our transaction directly, breaking all of
+ // the rules!
+ IgnoredErrorResult error;
+ DoSetText(aTextNode, aString, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("EditorBase::DoSetText() failed");
+ return error.StealNSResult();
+ }
+
+ CollapseSelectionTo(EditorRawDOMPoint(&aTextNode, aString.Length()), error);
+ if (MOZ_UNLIKELY(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ NS_WARNING("EditorBase::CollapseSelection() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_ASSERTION(!error.Failed(),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+
+ RangeUpdaterRef().SelAdjReplaceText(aTextNode, 0, length, aString.Length());
+
+ // Let listeners know what happened
+ if (!mActionListeners.IsEmpty() && !aString.IsEmpty()) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored =
+ listener->DidInsertText(&aTextNode, 0, aString, NS_OK);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::DidInsertText() failed, but ignored");
+ }
+ }
+
+ return NS_OK;
+}
+
+Result<CaretPoint, nsresult> EditorBase::DeleteTextWithTransaction(
+ Text& aTextNode, uint32_t aOffset, uint32_t aLength) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<DeleteTextTransaction> transaction =
+ DeleteTextTransaction::MaybeCreate(*this, aTextNode, aOffset, aLength);
+ if (MOZ_UNLIKELY(!transaction)) {
+ NS_WARNING("DeleteTextTransaction::MaybeCreate() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteText, nsIEditor::ePrevious, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // Let listeners know what's up
+ if (!mActionListeners.IsEmpty()) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored =
+ listener->WillDeleteText(&aTextNode, aOffset, aLength);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::WillDeleteText() failed, but ignored");
+ }
+ }
+
+ nsresult rv = DoTransactionInternal(transaction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+
+ if (IsHTMLEditor()) {
+ TopLevelEditSubActionDataRef().DidDeleteText(
+ *this, EditorRawDOMPoint(&aTextNode, aOffset));
+ }
+
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+
+ return CaretPoint(transaction->SuggestPointToPutCaret());
+}
+
+bool EditorBase::IsRoot(const nsINode* inNode) const {
+ if (NS_WARN_IF(!inNode)) {
+ return false;
+ }
+ nsINode* rootNode = GetRoot();
+ return inNode == rootNode;
+}
+
+bool EditorBase::IsDescendantOfRoot(const nsINode* inNode) const {
+ if (NS_WARN_IF(!inNode)) {
+ return false;
+ }
+ nsIContent* root = GetRoot();
+ if (NS_WARN_IF(!root)) {
+ return false;
+ }
+
+ return inNode->IsInclusiveDescendantOf(root);
+}
+
+NS_IMETHODIMP EditorBase::IncrementModificationCount(int32_t inNumMods) {
+ uint32_t oldModCount = mModCount;
+
+ mModCount += inNumMods;
+
+ if ((!oldModCount && mModCount) || (oldModCount && !mModCount)) {
+ DebugOnly<nsresult> rvIgnored =
+ NotifyDocumentListeners(eDocumentStateChanged);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::NotifyDocumentListeners() failed, but ignored");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetModificationCount(int32_t* aOutModCount) {
+ if (NS_WARN_IF(!aOutModCount)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aOutModCount = mModCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::ResetModificationCount() {
+ bool doNotify = (mModCount != 0);
+
+ mModCount = 0;
+
+ if (!doNotify) {
+ return NS_OK;
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ NotifyDocumentListeners(eDocumentStateChanged);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::NotifyDocumentListeners() failed, but ignored");
+ return NS_OK;
+}
+
+template <typename EditorDOMPointType>
+EditorDOMPointType EditorBase::GetFirstSelectionStartPoint() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ if (MOZ_UNLIKELY(!SelectionRef().RangeCount())) {
+ return EditorDOMPointType();
+ }
+
+ const nsRange* range = SelectionRef().GetRangeAt(0);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned()))) {
+ return EditorDOMPointType();
+ }
+
+ return EditorDOMPointType(range->StartRef());
+}
+
+template <typename EditorDOMPointType>
+EditorDOMPointType EditorBase::GetFirstSelectionEndPoint() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ if (MOZ_UNLIKELY(!SelectionRef().RangeCount())) {
+ return EditorDOMPointType();
+ }
+
+ const nsRange* range = SelectionRef().GetRangeAt(0);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned()))) {
+ return EditorDOMPointType();
+ }
+
+ return EditorDOMPointType(range->EndRef());
+}
+
+// static
+nsresult EditorBase::GetEndChildNode(const Selection& aSelection,
+ nsIContent** aEndNode) {
+ MOZ_ASSERT(aEndNode);
+
+ *aEndNode = nullptr;
+
+ if (NS_WARN_IF(!aSelection.RangeCount())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const nsRange* range = aSelection.GetRangeAt(0);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!range->IsPositioned())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_IF_ADDREF(*aEndNode = range->GetChildAtEndOffset());
+ return NS_OK;
+}
+
+nsresult EditorBase::EnsurePaddingBRElementInMultilineEditor() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTextEditor() || AsHTMLEditor()->IsPlaintextMailComposer());
+ MOZ_ASSERT(!IsSingleLineEditor());
+
+ Element* anonymousDivOrBodyElement = GetRoot();
+ if (NS_WARN_IF(!anonymousDivOrBodyElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Assuming EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() has been
+ // called first.
+ // XXX This assumption is wrong. This method may be called alone. Actually,
+ // we see this warning in mochitest log. So, we should fix this bug
+ // later.
+ if (NS_WARN_IF(!anonymousDivOrBodyElement->GetLastChild())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<HTMLBRElement> brElement =
+ HTMLBRElement::FromNode(anonymousDivOrBodyElement->GetLastChild());
+ if (!brElement) {
+ // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary
+ // in normal cases. However, it may be required for nested edit
+ // actions which may be caused by legacy mutation event listeners or
+ // chrome script.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+ EditorDOMPoint endOfAnonymousDiv(
+ EditorDOMPoint::AtEndOf(*anonymousDivOrBodyElement));
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ endOfAnonymousDiv);
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "EditorBase::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
+ "failed");
+ return insertPaddingBRElementResult.unwrapErr();
+ }
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+ }
+
+ // Check to see if the trailing BR is a former padding <br> element for empty
+ // editor - this will have stuck around if we previously morphed a trailing
+ // node into a padding <br> element.
+ if (!brElement->IsPaddingForEmptyEditor()) {
+ return NS_OK;
+ }
+
+ // Morph it back to a padding <br> element for empty last line.
+ brElement->UnsetFlags(NS_PADDING_FOR_EMPTY_EDITOR);
+ brElement->SetFlags(NS_PADDING_FOR_EMPTY_LAST_LINE);
+
+ return NS_OK;
+}
+
+void EditorBase::BeginUpdateViewBatch(const char* aRequesterFuncName) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(mUpdateCount >= 0, "bad state");
+
+ if (!mUpdateCount) {
+ // Turn off selection updates and notifications.
+ SelectionRef().StartBatchChanges(aRequesterFuncName);
+ }
+
+ mUpdateCount++;
+}
+
+void EditorBase::EndUpdateViewBatch(const char* aRequesterFuncName) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(mUpdateCount > 0, "bad state");
+
+ if (NS_WARN_IF(mUpdateCount <= 0)) {
+ mUpdateCount = 0;
+ return;
+ }
+
+ if (--mUpdateCount) {
+ return;
+ }
+
+ // Turn selection updating and notifications back on.
+ SelectionRef().EndBatchChanges(aRequesterFuncName);
+}
+
+TextComposition* EditorBase::GetComposition() const { return mComposition; }
+
+template <typename EditorDOMPointType>
+EditorDOMPointType EditorBase::GetFirstIMESelectionStartPoint() const {
+ return mComposition
+ ? EditorDOMPointType(mComposition->FirstIMESelectionStartRef())
+ : EditorDOMPointType();
+}
+
+template <typename EditorDOMPointType>
+EditorDOMPointType EditorBase::GetLastIMESelectionEndPoint() const {
+ return mComposition
+ ? EditorDOMPointType(mComposition->LastIMESelectionEndRef())
+ : EditorDOMPointType();
+}
+
+bool EditorBase::IsIMEComposing() const {
+ return mComposition && mComposition->IsComposing();
+}
+
+bool EditorBase::ShouldHandleIMEComposition() const {
+ // When the editor is being reframed, the old value may be restored with
+ // InsertText(). In this time, the text should be inserted as not a part
+ // of the composition.
+ return mComposition && mDidPostCreate;
+}
+
+bool EditorBase::EnsureComposition(WidgetCompositionEvent& aCompositionEvent) {
+ if (mComposition) {
+ return true;
+ }
+ // The compositionstart event must cause creating new TextComposition
+ // instance at being dispatched by IMEStateManager.
+ mComposition = IMEStateManager::GetTextCompositionFor(&aCompositionEvent);
+ if (!mComposition) {
+ // However, TextComposition may be committed before the composition
+ // event comes here.
+ return false;
+ }
+ mComposition->StartHandlingComposition(this);
+ return true;
+}
+
+nsresult EditorBase::OnCompositionStart(
+ WidgetCompositionEvent& aCompositionStartEvent) {
+ if (mComposition) {
+ NS_WARNING("There was a composition at receiving compositionstart event");
+ return NS_OK;
+ }
+
+ // "beforeinput" event shouldn't be fired before "compositionstart".
+ AutoEditActionDataSetter editActionData(*this, EditAction::eStartComposition);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ EnsureComposition(aCompositionStartEvent);
+ NS_WARNING_ASSERTION(mComposition, "Failed to get TextComposition instance?");
+ return NS_OK;
+}
+
+nsresult EditorBase::OnCompositionChange(
+ WidgetCompositionEvent& aCompositionChangeEvent) {
+ MOZ_ASSERT(aCompositionChangeEvent.mMessage == eCompositionChange,
+ "The event should be eCompositionChange");
+
+ if (!mComposition) {
+ NS_WARNING(
+ "There is no composition, but receiving compositionchange event");
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eUpdateComposition);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // If:
+ // - new composition string is not empty,
+ // - there is no composition string in the DOM tree,
+ // - and there is non-collapsed Selection,
+ // the selected content will be removed by this composition.
+ if (aCompositionChangeEvent.mData.IsEmpty() &&
+ mComposition->String().IsEmpty() && !SelectionRef().IsCollapsed()) {
+ editActionData.UpdateEditAction(EditAction::eDeleteByComposition);
+ }
+
+ // If Input Events Level 2 is enabled, EditAction::eDeleteByComposition is
+ // mapped to EditorInputType::eDeleteByComposition and it requires null
+ // for InputEvent.data. Therefore, only otherwise, we should set data.
+ if (ToInputType(editActionData.GetEditAction()) !=
+ EditorInputType::eDeleteByComposition) {
+ MOZ_ASSERT(ToInputType(editActionData.GetEditAction()) ==
+ EditorInputType::eInsertCompositionText);
+ MOZ_ASSERT(!aCompositionChangeEvent.mData.IsVoid());
+ editActionData.SetData(aCompositionChangeEvent.mData);
+ }
+
+ // If we're an `HTMLEditor` and this is second or later composition change,
+ // we should set target range to the range of composition string.
+ // Otherwise, set target ranges to selection ranges (will be done by
+ // editActionData itself before dispatching `beforeinput` event).
+ if (IsHTMLEditor() && mComposition->GetContainerTextNode()) {
+ RefPtr<StaticRange> targetRange = StaticRange::Create(
+ mComposition->GetContainerTextNode(),
+ mComposition->XPOffsetInTextNode(),
+ mComposition->GetContainerTextNode(),
+ mComposition->XPEndOffsetInTextNode(), IgnoreErrors());
+ NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(),
+ "StaticRange::Create() failed");
+ if (targetRange && targetRange->IsPositioned()) {
+ editActionData.AppendTargetRange(*targetRange);
+ }
+ }
+
+ // TODO: We need to use different EditAction value for beforeinput event
+ // if the event is followed by "compositionend" because corresponding
+ // "input" event will be fired from OnCompositionEnd() later with
+ // different EditAction value.
+ // TODO: If Input Events Level 2 is enabled, "beforeinput" event may be
+ // actually canceled if edit action is eDeleteByComposition. In such
+ // case, we might need to keep selected text, but insert composition
+ // string before or after the selection. However, the spec is still
+ // unstable. We should keep handling the composition since other
+ // parts including widget may not be ready for such complicated
+ // behavior.
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (rv != NS_ERROR_EDITOR_ACTION_CANCELED && NS_FAILED(rv)) {
+ NS_WARNING("MaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (!EnsureComposition(aCompositionChangeEvent)) {
+ NS_WARNING("EditorBase::EnsureComposition() failed");
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!GetPresShell())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // NOTE: TextComposition should receive selection change notification before
+ // CompositionChangeEventHandlingMarker notifies TextComposition of the
+ // end of handling compositionchange event because TextComposition may
+ // need to ignore selection changes caused by composition. Therefore,
+ // CompositionChangeEventHandlingMarker must be destroyed after a call
+ // of NotifiyEditorObservers(eNotifyEditorObserversOfEnd) or
+ // NotifiyEditorObservers(eNotifyEditorObserversOfCancel) which notifies
+ // TextComposition of a selection change.
+ MOZ_ASSERT(
+ !mPlaceholderBatch,
+ "UpdateIMEComposition() must be called without place holder batch");
+ nsString data(aCompositionChangeEvent.mData);
+ if (IsHTMLEditor()) {
+ nsContentUtils::PlatformToDOMLineBreaks(data);
+ }
+
+ {
+ // This needs to be destroyed before dispatching "input" event from
+ // the following call of `NotifyEditorObservers`. Therefore, we need to
+ // put this in this block rather than outside of this.
+ const bool wasComposing = mComposition->IsComposing();
+ TextComposition::CompositionChangeEventHandlingMarker
+ compositionChangeEventHandlingMarker(mComposition,
+ &aCompositionChangeEvent);
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::IMETxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+
+ // XXX Why don't we get caret after the DOM mutation?
+ RefPtr<nsCaret> caret = GetCaret();
+
+ MOZ_ASSERT(
+ mIsInEditSubAction,
+ "AutoPlaceholderBatch should've notified the observes of before-edit");
+ // If we're updating composition, we need to ignore normal selection
+ // which may be updated by the web content.
+ rv = InsertTextAsSubAction(data, wasComposing ? SelectionHandling::Ignore
+ : SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+
+ if (caret) {
+ caret->SetSelection(&SelectionRef());
+ }
+ }
+
+ // If still composing, we should fire input event via observer.
+ // Note that if the composition will be committed by the following
+ // compositionend event, we don't need to notify editor observes of this
+ // change.
+ // NOTE: We must notify after the auto batch will be gone.
+ if (!aCompositionChangeEvent.IsFollowedByCompositionEnd()) {
+ // If we're a TextEditor, we'll be initialized with a new anonymous subtree,
+ // which can be caused by reframing from a "input" event listener. At that
+ // time, we'll move composition from current text node to the new text node
+ // with using mComposition's data. Therefore, it's important that
+ // mComposition already has the latest information here.
+ MOZ_ASSERT_IF(mComposition, mComposition->String() == data);
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ }
+
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+void EditorBase::OnCompositionEnd(
+ WidgetCompositionEvent& aCompositionEndEvent) {
+ if (!mComposition) {
+ NS_WARNING("There is no composition, but receiving compositionend event");
+ return;
+ }
+
+ EditAction editAction = aCompositionEndEvent.mData.IsEmpty()
+ ? EditAction::eCancelComposition
+ : EditAction::eCommitComposition;
+ AutoEditActionDataSetter editActionData(*this, editAction);
+ // If Input Events Level 2 is enabled, EditAction::eCancelComposition is
+ // mapped to EditorInputType::eDeleteCompositionText and it requires null
+ // for InputEvent.data. Therefore, only otherwise, we should set data.
+ if (ToInputType(editAction) != EditorInputType::eDeleteCompositionText) {
+ MOZ_ASSERT(
+ ToInputType(editAction) == EditorInputType::eInsertCompositionText ||
+ ToInputType(editAction) == EditorInputType::eInsertFromComposition);
+ MOZ_ASSERT(!aCompositionEndEvent.mData.IsVoid());
+ editActionData.SetData(aCompositionEndEvent.mData);
+ }
+
+ // commit the IME transaction..we can get at it via the transaction mgr.
+ // Note that this means IME won't work without an undo stack!
+ if (mTransactionManager) {
+ if (nsCOMPtr<nsITransaction> transaction =
+ mTransactionManager->PeekUndoStack()) {
+ if (RefPtr<EditTransactionBase> transactionBase =
+ transaction->GetAsEditTransactionBase()) {
+ if (PlaceholderTransaction* placeholderTransaction =
+ transactionBase->GetAsPlaceholderTransaction()) {
+ placeholderTransaction->Commit();
+ }
+ }
+ }
+ }
+
+ // Note that this just marks as that we've already handled "beforeinput" for
+ // preventing assertions in FireInputEvent(). Note that corresponding
+ // "beforeinput" event for the following "input" event should've already
+ // been dispatched from `OnCompositionChange()`.
+ DebugOnly<nsresult> rvIgnored =
+ editActionData.MaybeDispatchBeforeInputEvent();
+ MOZ_ASSERT(rvIgnored != NS_ERROR_EDITOR_ACTION_CANCELED,
+ "Why beforeinput event was canceled in this case?");
+ MOZ_ASSERT(NS_SUCCEEDED(rvIgnored),
+ "MaybeDispatchBeforeInputEvent() should just mark the instance as "
+ "handled it");
+
+ // Composition string may have hidden the caret. Therefore, we need to
+ // cancel it here.
+ HideCaret(false);
+
+ // FYI: mComposition still keeps storing container text node of committed
+ // string, its offset and length. However, they will be invalidated
+ // soon since its Destroy() will be called by IMEStateManager.
+ mComposition->EndHandlingComposition(this);
+ mComposition = nullptr;
+
+ // notify editor observers of action
+ // FYI: With current draft, "input" event should be fired from
+ // OnCompositionChange(), however, it requires a lot of our UI code
+ // change and does not make sense. See spec issue:
+ // https://github.com/w3c/uievents/issues/202
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+}
+
+void EditorBase::DoAfterDoTransaction(nsITransaction* aTransaction) {
+ bool isTransientTransaction;
+ MOZ_ALWAYS_SUCCEEDS(aTransaction->GetIsTransient(&isTransientTransaction));
+
+ if (!isTransientTransaction) {
+ // we need to deal here with the case where the user saved after some
+ // edits, then undid one or more times. Then, the undo count is -ve,
+ // but we can't let a do take it back to zero. So we flip it up to
+ // a +ve number.
+ int32_t modCount;
+ DebugOnly<nsresult> rvIgnored = GetModificationCount(&modCount);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::GetModificationCount() failed, but ignored");
+ if (modCount < 0) {
+ modCount = -modCount;
+ }
+
+ // don't count transient transactions
+ MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1));
+ }
+}
+
+void EditorBase::DoAfterUndoTransaction() {
+ // all undoable transactions are non-transient
+ MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(-1));
+}
+
+void EditorBase::DoAfterRedoTransaction() {
+ // all redoable transactions are non-transient
+ MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1));
+}
+
+already_AddRefed<DeleteMultipleRangesTransaction>
+EditorBase::CreateTransactionForDeleteSelection(
+ HowToHandleCollapsedRange aHowToHandleCollapsedRange,
+ const AutoRangeArray& aRangesToDelete) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
+
+ // Check whether the selection is collapsed and we should do nothing:
+ if (NS_WARN_IF(aRangesToDelete.IsCollapsed() &&
+ aHowToHandleCollapsedRange ==
+ HowToHandleCollapsedRange::Ignore)) {
+ return nullptr;
+ }
+
+ // allocate the out-param transaction
+ RefPtr<DeleteMultipleRangesTransaction> transaction =
+ DeleteMultipleRangesTransaction::Create();
+ for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
+ // Same with range as with selection; if it is collapsed and action
+ // is eNone, do nothing.
+ if (!range->Collapsed()) {
+ RefPtr<DeleteRangeTransaction> deleteRangeTransaction =
+ DeleteRangeTransaction::Create(*this, range);
+ // XXX Oh, not checking if deleteRangeTransaction can modify the range...
+ transaction->AppendChild(*deleteRangeTransaction);
+ continue;
+ }
+
+ if (aHowToHandleCollapsedRange == HowToHandleCollapsedRange::Ignore) {
+ continue;
+ }
+
+ // Let's extend the collapsed range to delete content around it.
+ RefPtr<DeleteContentTransactionBase> deleteNodeOrTextTransaction =
+ CreateTransactionForCollapsedRange(range, aHowToHandleCollapsedRange);
+ // XXX When there are two or more ranges and at least one of them is
+ // not editable, deleteNodeOrTextTransaction may be nullptr.
+ // In such case, should we stop removing other ranges too?
+ if (!deleteNodeOrTextTransaction) {
+ NS_WARNING("EditorBase::CreateTransactionForCollapsedRange() failed");
+ return nullptr;
+ }
+ transaction->AppendChild(*deleteNodeOrTextTransaction);
+ }
+
+ return transaction.forget();
+}
+
+// XXX: currently, this doesn't handle edge conditions because GetNext/GetPrior
+// are not implemented
+already_AddRefed<DeleteContentTransactionBase>
+EditorBase::CreateTransactionForCollapsedRange(
+ const nsRange& aCollapsedRange,
+ HowToHandleCollapsedRange aHowToHandleCollapsedRange) {
+ MOZ_ASSERT(aCollapsedRange.Collapsed());
+ MOZ_ASSERT(
+ aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendBackward ||
+ aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendForward);
+
+ EditorRawDOMPoint point(aCollapsedRange.StartRef());
+ if (NS_WARN_IF(!point.IsSet())) {
+ return nullptr;
+ }
+ if (IsTextEditor()) {
+ // There should be only one text node in the anonymous `<div>` (but may
+ // be followed by a padding `<br>`). We should adjust the point into
+ // the text node (or return nullptr if there is no text to delete) for
+ // avoiding finding the text node with complicated API.
+ if (!point.IsInTextNode()) {
+ const Element* anonymousDiv = GetRoot();
+ if (NS_WARN_IF(!anonymousDiv)) {
+ return nullptr;
+ }
+ if (!anonymousDiv->GetFirstChild() ||
+ !anonymousDiv->GetFirstChild()->IsText()) {
+ return nullptr; // The value is empty.
+ }
+ if (point.GetContainer() == anonymousDiv) {
+ if (point.IsStartOfContainer()) {
+ point.Set(anonymousDiv->GetFirstChild(), 0);
+ } else {
+ point.SetToEndOf(anonymousDiv->GetFirstChild());
+ }
+ } else {
+ // Must be referring a padding `<br>` element or after the text node.
+ point.SetToEndOf(anonymousDiv->GetFirstChild());
+ }
+ }
+ MOZ_ASSERT(!point.ContainerAs<Text>()->GetPreviousSibling());
+ MOZ_ASSERT(!point.ContainerAs<Text>()->GetNextSibling() ||
+ !point.ContainerAs<Text>()->GetNextSibling()->IsText());
+ if (aHowToHandleCollapsedRange ==
+ HowToHandleCollapsedRange::ExtendBackward &&
+ point.IsStartOfContainer()) {
+ return nullptr;
+ }
+ if (aHowToHandleCollapsedRange ==
+ HowToHandleCollapsedRange::ExtendForward &&
+ point.IsEndOfContainer()) {
+ return nullptr;
+ }
+ }
+
+ // XXX: if the container of point is empty, then we'll need to delete the node
+ // as well as the 1 child
+
+ // build a transaction for deleting the appropriate data
+ // XXX: this has to come from rule section
+ const Element* const anonymousDivOrEditingHost =
+ IsTextEditor() ? GetRoot() : AsHTMLEditor()->ComputeEditingHost();
+ if (aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendBackward &&
+ point.IsStartOfContainer()) {
+ MOZ_ASSERT(IsHTMLEditor());
+ // We're backspacing from the beginning of a node. Delete the last thing
+ // of previous editable content.
+ nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent(
+ *point.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode},
+ IsTextEditor() ? BlockInlineCheck::UseHTMLDefaultStyle
+ : BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ anonymousDivOrEditingHost);
+ if (!previousEditableContent) {
+ NS_WARNING("There was no editable content before the collapsed range");
+ return nullptr;
+ }
+
+ // There is an editable content, so delete its last child (if a text node,
+ // delete the last char). If it has no children, delete it.
+ if (previousEditableContent->IsText()) {
+ uint32_t length = previousEditableContent->Length();
+ // Bail out for empty text node.
+ // XXX Do we want to do something else?
+ // XXX If other browsers delete empty text node, we should follow it.
+ if (NS_WARN_IF(!length)) {
+ NS_WARNING("Previous editable content was an empty text node");
+ return nullptr;
+ }
+ RefPtr<DeleteTextTransaction> deleteTextTransaction =
+ DeleteTextTransaction::MaybeCreateForPreviousCharacter(
+ *this, *previousEditableContent->AsText(), length);
+ if (!deleteTextTransaction) {
+ NS_WARNING(
+ "DeleteTextTransaction::MaybeCreateForPreviousCharacter() failed");
+ return nullptr;
+ }
+ return deleteTextTransaction.forget();
+ }
+
+ if (IsHTMLEditor() &&
+ NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*previousEditableContent))) {
+ return nullptr;
+ }
+ RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
+ DeleteNodeTransaction::MaybeCreate(*this, *previousEditableContent);
+ if (!deleteNodeTransaction) {
+ NS_WARNING("DeleteNodeTransaction::MaybeCreate() failed");
+ return nullptr;
+ }
+ return deleteNodeTransaction.forget();
+ }
+
+ if (aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendForward &&
+ point.IsEndOfContainer()) {
+ MOZ_ASSERT(IsHTMLEditor());
+ // We're deleting from the end of a node. Delete the first thing of
+ // next editable content.
+ nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent(
+ *point.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode},
+ IsTextEditor() ? BlockInlineCheck::UseHTMLDefaultStyle
+ : BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ anonymousDivOrEditingHost);
+ if (!nextEditableContent) {
+ NS_WARNING("There was no editable content after the collapsed range");
+ return nullptr;
+ }
+
+ // There is an editable content, so delete its first child (if a text node,
+ // delete the first char). If it has no children, delete it.
+ if (nextEditableContent->IsText()) {
+ uint32_t length = nextEditableContent->Length();
+ // Bail out for empty text node.
+ // XXX Do we want to do something else?
+ // XXX If other browsers delete empty text node, we should follow it.
+ if (!length) {
+ NS_WARNING("Next editable content was an empty text node");
+ return nullptr;
+ }
+ RefPtr<DeleteTextTransaction> deleteTextTransaction =
+ DeleteTextTransaction::MaybeCreateForNextCharacter(
+ *this, *nextEditableContent->AsText(), 0);
+ if (!deleteTextTransaction) {
+ NS_WARNING(
+ "DeleteTextTransaction::MaybeCreateForNextCharacter() failed");
+ return nullptr;
+ }
+ return deleteTextTransaction.forget();
+ }
+
+ if (IsHTMLEditor() &&
+ NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*nextEditableContent))) {
+ return nullptr;
+ }
+ RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
+ DeleteNodeTransaction::MaybeCreate(*this, *nextEditableContent);
+ if (!deleteNodeTransaction) {
+ NS_WARNING("DeleteNodeTransaction::MaybeCreate() failed");
+ return nullptr;
+ }
+ return deleteNodeTransaction.forget();
+ }
+
+ if (point.IsInTextNode()) {
+ if (aHowToHandleCollapsedRange ==
+ HowToHandleCollapsedRange::ExtendBackward) {
+ RefPtr<DeleteTextTransaction> deleteTextTransaction =
+ DeleteTextTransaction::MaybeCreateForPreviousCharacter(
+ *this, *point.ContainerAs<Text>(), point.Offset());
+ NS_WARNING_ASSERTION(
+ deleteTextTransaction,
+ "DeleteTextTransaction::MaybeCreateForPreviousCharacter() failed");
+ return deleteTextTransaction.forget();
+ }
+ RefPtr<DeleteTextTransaction> deleteTextTransaction =
+ DeleteTextTransaction::MaybeCreateForNextCharacter(
+ *this, *point.ContainerAs<Text>(), point.Offset());
+ NS_WARNING_ASSERTION(
+ deleteTextTransaction,
+ "DeleteTextTransaction::MaybeCreateForNextCharacter() failed");
+ return deleteTextTransaction.forget();
+ }
+
+ nsIContent* editableContent = nullptr;
+ if (IsHTMLEditor()) {
+ editableContent =
+ aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendBackward
+ ? HTMLEditUtils::GetPreviousContent(
+ point, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ anonymousDivOrEditingHost)
+ : HTMLEditUtils::GetNextContent(
+ point, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ anonymousDivOrEditingHost);
+ if (!editableContent) {
+ NS_WARNING("There was no editable content around the collapsed range");
+ return nullptr;
+ }
+ while (editableContent && editableContent->IsCharacterData() &&
+ !editableContent->Length()) {
+ // Can't delete an empty text node (bug 762183)
+ editableContent =
+ aHowToHandleCollapsedRange ==
+ HowToHandleCollapsedRange::ExtendBackward
+ ? HTMLEditUtils::GetPreviousContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ anonymousDivOrEditingHost)
+ : HTMLEditUtils::GetNextContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ anonymousDivOrEditingHost);
+ }
+ if (!editableContent) {
+ NS_WARNING(
+ "There was no editable content which is not empty around the "
+ "collapsed range");
+ return nullptr;
+ }
+ } else {
+ MOZ_ASSERT(point.IsInTextNode());
+ editableContent = point.GetContainerAs<nsIContent>();
+ if (!editableContent) {
+ NS_WARNING("If there was no text node, should've been handled first");
+ return nullptr;
+ }
+ }
+
+ if (editableContent->IsText()) {
+ if (aHowToHandleCollapsedRange ==
+ HowToHandleCollapsedRange::ExtendBackward) {
+ RefPtr<DeleteTextTransaction> deleteTextTransaction =
+ DeleteTextTransaction::MaybeCreateForPreviousCharacter(
+ *this, *editableContent->AsText(), editableContent->Length());
+ NS_WARNING_ASSERTION(
+ deleteTextTransaction,
+ "DeleteTextTransaction::MaybeCreateForPreviousCharacter() failed");
+ return deleteTextTransaction.forget();
+ }
+
+ RefPtr<DeleteTextTransaction> deleteTextTransaction =
+ DeleteTextTransaction::MaybeCreateForNextCharacter(
+ *this, *editableContent->AsText(), 0);
+ NS_WARNING_ASSERTION(
+ deleteTextTransaction,
+ "DeleteTextTransaction::MaybeCreateForNextCharacter() failed");
+ return deleteTextTransaction.forget();
+ }
+
+ MOZ_ASSERT(IsHTMLEditor());
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*editableContent))) {
+ return nullptr;
+ }
+ RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
+ DeleteNodeTransaction::MaybeCreate(*this, *editableContent);
+ NS_WARNING_ASSERTION(deleteNodeTransaction,
+ "DeleteNodeTransaction::MaybeCreate() failed");
+ return deleteNodeTransaction.forget();
+}
+
+bool EditorBase::FlushPendingNotificationsIfToHandleDeletionWithFrameSelection(
+ nsIEditor::EDirection aDirectionAndAmount) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(Destroyed())) {
+ return false;
+ }
+ if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection(
+ aDirectionAndAmount, SelectionRef())) {
+ return true;
+ }
+ // Although AutoRangeArray::ExtendAnchorFocusRangeFor() will use
+ // nsFrameSelection, if it still has dirty frame, nsFrameSelection doesn't
+ // extend selection since we block script.
+ if (RefPtr<PresShell> presShell = GetPresShell()) {
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ if (NS_WARN_IF(Destroyed())) {
+ return false;
+ }
+ }
+ return true;
+}
+
+nsresult EditorBase::DeleteSelectionAsAction(
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers, nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
+ // Showing this assertion is fine if this method is called by outside via
+ // mutation event listener or something. Otherwise, this is called by
+ // wrong method.
+ NS_ASSERTION(
+ !mPlaceholderBatch,
+ "Should be called only when this is the only edit action of the "
+ "operation unless mutation event listener nests some operations");
+
+ // If we're a TextEditor instance, we don't need to treat parent elements
+ // so that we can ignore aStripWrappers for skipping unnecessary cost.
+ if (IsTextEditor()) {
+ aStripWrappers = nsIEditor::eNoStrip;
+ }
+
+ EditAction editAction = EditAction::eDeleteSelection;
+ switch (aDirectionAndAmount) {
+ case nsIEditor::ePrevious:
+ editAction = EditAction::eDeleteBackward;
+ break;
+ case nsIEditor::eNext:
+ editAction = EditAction::eDeleteForward;
+ break;
+ case nsIEditor::ePreviousWord:
+ editAction = EditAction::eDeleteWordBackward;
+ break;
+ case nsIEditor::eNextWord:
+ editAction = EditAction::eDeleteWordForward;
+ break;
+ case nsIEditor::eToBeginningOfLine:
+ editAction = EditAction::eDeleteToBeginningOfSoftLine;
+ break;
+ case nsIEditor::eToEndOfLine:
+ editAction = EditAction::eDeleteToEndOfSoftLine;
+ break;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, editAction, aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // If there is an existing selection when an extended delete is requested,
+ // platforms that use "caret-style" caret positioning collapse the
+ // selection to the start and then create a new selection.
+ // Platforms that use "selection-style" caret positioning just delete the
+ // existing selection without extending it.
+ if (!SelectionRef().IsCollapsed()) {
+ switch (aDirectionAndAmount) {
+ case eNextWord:
+ case ePreviousWord:
+ case eToBeginningOfLine:
+ case eToEndOfLine: {
+ if (mCaretStyle != 1) {
+ aDirectionAndAmount = eNone;
+ break;
+ }
+ ErrorResult error;
+ SelectionRef().CollapseToStart(error);
+ if (NS_WARN_IF(Destroyed())) {
+ error.SuppressException();
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (error.Failed()) {
+ NS_WARNING("Selection::CollapseToStart() failed");
+ editActionData.Abort();
+ return EditorBase::ToGenericNSResult(error.StealNSResult());
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ // If Selection is still NOT collapsed, it does not important removing
+ // range of the operation since we'll remove the selected content. However,
+ // information of direction (backward or forward) may be important for
+ // web apps. E.g., web apps may want to mark selected range as "deleted"
+ // and move caret before or after the range. Therefore, we should forget
+ // only the range information but keep range information. See discussion
+ // of the spec issue for the detail:
+ // https://github.com/w3c/input-events/issues/82
+ if (!SelectionRef().IsCollapsed()) {
+ switch (editAction) {
+ case EditAction::eDeleteWordBackward:
+ case EditAction::eDeleteToBeginningOfSoftLine:
+ editActionData.UpdateEditAction(EditAction::eDeleteBackward);
+ break;
+ case EditAction::eDeleteWordForward:
+ case EditAction::eDeleteToEndOfSoftLine:
+ editActionData.UpdateEditAction(EditAction::eDeleteForward);
+ break;
+ default:
+ break;
+ }
+ }
+
+ editActionData.SetSelectionCreatedByDoubleclick(
+ SelectionRef().GetFrameSelection() &&
+ SelectionRef().GetFrameSelection()->IsDoubleClickSelection());
+
+ if (!FlushPendingNotificationsIfToHandleDeletionWithFrameSelection(
+ aDirectionAndAmount)) {
+ NS_WARNING("Flusing pending notifications caused destroying the editor");
+ editActionData.Abort();
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ nsresult rv =
+ editActionData.MaybeDispatchBeforeInputEvent(aDirectionAndAmount);
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // delete placeholder txns merge.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::DeleteTxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+ rv = DeleteSelectionAsSubAction(aDirectionAndAmount, aStripWrappers);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::DeleteSelectionAsSubAction(
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ // If handling edit action is for table editing, this may be called with
+ // selecting an any table element by the caller, but it's not usual work of
+ // this so that `MayEditActionDeleteSelection()` returns false.
+ MOZ_ASSERT(MayEditActionDeleteSelection(GetEditAction()) ||
+ IsEditActionTableEditing(GetEditAction()));
+ MOZ_ASSERT(mPlaceholderBatch);
+ MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
+ NS_ASSERTION(IsHTMLEditor() || aStripWrappers == nsIEditor::eNoStrip,
+ "TextEditor does not support strip wrappers");
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteSelectedContent, aDirectionAndAmount,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result =
+ HandleDeleteSelection(aDirectionAndAmount, aStripWrappers);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("TextEditor::HandleDeleteSelection() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ // XXX This is odd. We just tries to remove empty text node here but we
+ // refer `Selection`. It may be modified by mutation event listeners
+ // so that we should remove the empty text node when we make it empty.
+ const auto atNewStartOfSelection =
+ GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!atNewStartOfSelection.IsSet())) {
+ // XXX And also it seems that we don't need to return error here.
+ // Why don't we just ignore? `Selection::RemoveAllRanges()` may
+ // have been called by mutation event listeners.
+ return NS_ERROR_FAILURE;
+ }
+ if (IsHTMLEditor() && atNewStartOfSelection.IsInTextNode() &&
+ !atNewStartOfSelection.GetContainer()->Length()) {
+ nsresult rv = DeleteNodeWithTransaction(
+ MOZ_KnownLive(*atNewStartOfSelection.ContainerAs<Text>()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+
+ // XXX I don't think that this is necessary in anonymous `<div>` element of
+ // TextEditor since there should be at most one text node and at most
+ // one padding `<br>` element so that `<br>` element won't be before
+ // caret.
+ if (!TopLevelEditSubActionDataRef().mDidExplicitlySetInterLine) {
+ // We prevent the caret from sticking on the left of previous `<br>`
+ // element (i.e. the end of previous line) after this deletion. Bug 92124.
+ if (MOZ_UNLIKELY(NS_FAILED(SelectionRef().SetInterlinePosition(
+ InterlinePosition::StartOfNextLine)))) {
+ NS_WARNING(
+ "Selection::SetInterlinePosition(InterlinePosition::StartOfNextLine) "
+ "failed");
+ return NS_ERROR_FAILURE; // Don't need to return NS_ERROR_NOT_INITIALIZED
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult EditorBase::HandleDropEvent(DragEvent* aDropEvent) {
+ if (NS_WARN_IF(!aDropEvent)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ DebugOnly<nsresult> rvIgnored = CommitComposition();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CommitComposition() failed, but ignored");
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eDrop);
+ // We need to initialize data or dataTransfer later. Therefore, we cannot
+ // dispatch "beforeinput" event until then.
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ RefPtr<DataTransfer> dataTransfer = aDropEvent->GetDataTransfer();
+ if (NS_WARN_IF(!dataTransfer)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession();
+ if (NS_WARN_IF(!dragSession)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsINode> sourceNode = dataTransfer->GetMozSourceNode();
+
+ // If there is no source document, then the drag was from another application
+ // or another process (such as an out of process subframe). The latter case is
+ // not currently handled below when checking for a move/copy and deleting the
+ // existing text.
+ RefPtr<Document> srcdoc;
+ if (sourceNode) {
+ srcdoc = sourceNode->OwnerDoc();
+ }
+
+ nsCOMPtr<nsIPrincipal> sourcePrincipal;
+ dragSession->GetTriggeringPrincipal(getter_AddRefs(sourcePrincipal));
+
+ if (nsContentUtils::CheckForSubFrameDrop(
+ dragSession, aDropEvent->WidgetEventPtr()->AsDragEvent())) {
+ // Don't allow drags from subframe documents with different origins than
+ // the drop destination.
+ if (IsSafeToInsertData(sourcePrincipal) == SafeToInsertData::No) {
+ return NS_OK;
+ }
+ }
+
+ // Current doc is destination
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ const uint32_t numItems = dataTransfer->MozItemCount();
+ if (NS_WARN_IF(!numItems)) {
+ return NS_ERROR_FAILURE; // Nothing to drop?
+ }
+
+ // We have to figure out whether to delete and relocate caret only once
+ // Parent and offset are under the mouse cursor.
+ int32_t dropOffset = -1;
+ nsCOMPtr<nsIContent> dropParentContent =
+ aDropEvent->GetRangeParentContentAndOffset(&dropOffset);
+ if (dropOffset < 0) {
+ NS_WARNING(
+ "DropEvent::GetRangeParentContentAndOffset() returned negative offset");
+ return NS_ERROR_FAILURE;
+ }
+ EditorDOMPoint droppedAt(dropParentContent,
+ AssertedCast<uint32_t>(dropOffset));
+ if (NS_WARN_IF(!droppedAt.IsInContentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check if dropping into a selected range. If so and the source comes from
+ // same document, jump through some hoops to determine if mouse is over
+ // selection (bail) and whether user wants to copy selection or delete it.
+ if (sourceNode && sourceNode->IsEditable() && srcdoc == document) {
+ bool isPointInSelection = nsContentUtils::IsPointInSelection(
+ SelectionRef(), *droppedAt.GetContainer(), droppedAt.Offset());
+ if (isPointInSelection) {
+ // If source document and destination document is same and dropping
+ // into one of selected ranges, we don't need to do nothing.
+ // XXX If the source comes from outside of this editor, this check
+ // means that we don't allow to drop the item in the selected
+ // range. However, the selection is hidden until the <input> or
+ // <textarea> gets focus, therefore, this looks odd.
+ return NS_OK;
+ }
+ }
+
+ // Delete if user doesn't want to copy when user moves selected content
+ // to different place in same editor.
+ // XXX Do we need the check whether it's in same document or not?
+ RefPtr<EditorBase> editorToDeleteSelection;
+ if (sourceNode && sourceNode->IsEditable() && srcdoc == document) {
+ if ((dataTransfer->DropEffectInt() &
+ nsIDragService::DRAGDROP_ACTION_MOVE) &&
+ !(dataTransfer->DropEffectInt() &
+ nsIDragService::DRAGDROP_ACTION_COPY)) {
+ // If the source node is in native anonymous tree, it must be in
+ // <input> or <textarea> element. If so, its TextEditor can remove it.
+ if (sourceNode->IsInNativeAnonymousSubtree()) {
+ if (RefPtr textControlElement = TextControlElement::FromNodeOrNull(
+ sourceNode
+ ->GetClosestNativeAnonymousSubtreeRootParentOrHost())) {
+ editorToDeleteSelection = textControlElement->GetTextEditor();
+ }
+ }
+ // Otherwise, must be the content is in HTMLEditor.
+ else if (IsHTMLEditor()) {
+ editorToDeleteSelection = this;
+ } else {
+ editorToDeleteSelection =
+ nsContentUtils::GetHTMLEditor(srcdoc->GetPresContext());
+ }
+ }
+ // If the found editor isn't modifiable, we should not try to delete
+ // selection.
+ if (editorToDeleteSelection && !editorToDeleteSelection->IsModifiable()) {
+ editorToDeleteSelection = nullptr;
+ }
+ // If the found editor has collapsed selection, we need to delete nothing
+ // in the editor.
+ if (editorToDeleteSelection) {
+ if (Selection* selection = editorToDeleteSelection->GetSelection()) {
+ if (selection->IsCollapsed()) {
+ editorToDeleteSelection = nullptr;
+ }
+ }
+ }
+ }
+
+ // Combine any deletion and drop insertion into one transaction.
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ // Don't dispatch "selectionchange" event until inserting all contents.
+ SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__);
+
+ // Track dropped point with nsRange because we shouldn't insert the
+ // dropped content into different position even if some event listeners
+ // modify selection. Note that Chrome's behavior is really odd. So,
+ // we don't need to worry about web-compat about this.
+ IgnoredErrorResult ignoredError;
+ RefPtr<nsRange> rangeAtDropPoint =
+ nsRange::Create(droppedAt.ToRawRangeBoundary(),
+ droppedAt.ToRawRangeBoundary(), ignoredError);
+ if (NS_WARN_IF(ignoredError.Failed()) ||
+ NS_WARN_IF(!rangeAtDropPoint->IsPositioned())) {
+ editActionData.Abort();
+ return NS_ERROR_FAILURE;
+ }
+
+ // Remove selected contents first here because we need to fire a pair of
+ // "beforeinput" and "input" for deletion and web apps can cancel only
+ // this deletion. Note that callee may handle insertion asynchronously.
+ // Therefore, it is the best to remove selected content here.
+ if (editorToDeleteSelection) {
+ nsresult rv = editorToDeleteSelection->DeleteSelectionByDragAsAction(
+ mDispatchInputEvent);
+ if (NS_WARN_IF(Destroyed())) {
+ editActionData.Abort();
+ return NS_OK;
+ }
+ // Ignore the editor instance specific error if it's another editor.
+ if (this != editorToDeleteSelection &&
+ (rv == NS_ERROR_NOT_INITIALIZED || rv == NS_ERROR_EDITOR_DESTROYED)) {
+ rv = NS_OK;
+ }
+ // Don't cancel "insertFromDrop" even if "deleteByDrag" is canceled.
+ if (rv != NS_ERROR_EDITOR_ACTION_CANCELED && NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteSelectionByDragAsAction() failed");
+ editActionData.Abort();
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (NS_WARN_IF(!rangeAtDropPoint->IsPositioned()) ||
+ NS_WARN_IF(!rangeAtDropPoint->GetStartContainer()->IsContent())) {
+ editActionData.Abort();
+ return NS_ERROR_FAILURE;
+ }
+ droppedAt = rangeAtDropPoint->StartRef();
+ MOZ_ASSERT(droppedAt.IsSetAndValid());
+ MOZ_ASSERT(droppedAt.IsInContentNode());
+ }
+
+ // Before inserting dropping content, we need to move focus for compatibility
+ // with Chrome and firing "beforeinput" event on new editing host.
+ RefPtr<Element> focusedElement, newFocusedElement;
+ if (IsTextEditor()) {
+ newFocusedElement = GetExposedRoot();
+ focusedElement = IsActiveInDOMWindow() ? newFocusedElement : nullptr;
+ }
+ // TODO: We need to add automated tests when dropping something into an
+ // editing host for contenteditable which is in a shadow DOM tree
+ // and its host which is in design mode.
+ else if (!AsHTMLEditor()->IsInDesignMode()) {
+ focusedElement = AsHTMLEditor()->ComputeEditingHost();
+ if (focusedElement &&
+ droppedAt.ContainerAs<nsIContent>()->IsInclusiveDescendantOf(
+ focusedElement)) {
+ newFocusedElement = focusedElement;
+ } else {
+ newFocusedElement = droppedAt.ContainerAs<nsIContent>()->GetEditingHost();
+ }
+ }
+ // Move selection right now. Note that this does not move focus because
+ // `Selection` moves focus with selection change only when the API caller is
+ // JS. And also this does not notify selection listeners (nor
+ // "selectionchange") since we created SelectionBatcher above.
+ ErrorResult error;
+ SelectionRef().SetStartAndEnd(droppedAt.ToRawRangeBoundary(),
+ droppedAt.ToRawRangeBoundary(), error);
+ if (error.Failed()) {
+ NS_WARNING("Selection::SetStartAndEnd() failed");
+ editActionData.Abort();
+ return error.StealNSResult();
+ }
+ if (NS_WARN_IF(Destroyed())) {
+ editActionData.Abort();
+ return NS_OK;
+ }
+ // Then, move focus if necessary. This must cause dispatching "blur" event
+ // and "focus" event.
+ if (newFocusedElement && focusedElement != newFocusedElement) {
+ RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager();
+ DebugOnly<nsresult> rvIgnored = fm->SetFocus(newFocusedElement, 0);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsFocusManager::SetFocus() failed to set focus "
+ "to the element, but ignored");
+ if (NS_WARN_IF(Destroyed())) {
+ editActionData.Abort();
+ return NS_OK;
+ }
+ // "blur" or "focus" event listener may have changed the value.
+ // Let's keep using the original point.
+ if (NS_WARN_IF(!rangeAtDropPoint->IsPositioned()) ||
+ NS_WARN_IF(!rangeAtDropPoint->GetStartContainer()->IsContent())) {
+ return NS_ERROR_FAILURE;
+ }
+ droppedAt = rangeAtDropPoint->StartRef();
+ MOZ_ASSERT(droppedAt.IsSetAndValid());
+
+ // If focus is changed to different element and we're handling drop in
+ // contenteditable, we cannot handle it without focus. So, we should give
+ // it up.
+ if (IsHTMLEditor() && !AsHTMLEditor()->IsInDesignMode() &&
+ NS_WARN_IF(newFocusedElement != AsHTMLEditor()->ComputeEditingHost())) {
+ editActionData.Abort();
+ return NS_OK;
+ }
+ }
+
+ nsresult rv = InsertDroppedDataTransferAsAction(editActionData, *dataTransfer,
+ droppedAt, sourcePrincipal);
+ if (rv == NS_ERROR_EDITOR_DESTROYED ||
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED) {
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::InsertDroppedDataTransferAsAction() failed, but ignored");
+
+ rv = ScrollSelectionFocusIntoView();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::ScrollSelectionFocusIntoView() failed");
+ return rv;
+}
+
+nsresult EditorBase::DeleteSelectionByDragAsAction(bool aDispatchInputEvent) {
+ // TODO: Move this method to `EditorBase`.
+ AutoRestore<bool> saveDispatchInputEvent(mDispatchInputEvent);
+ mDispatchInputEvent = aDispatchInputEvent;
+ // Even if we're handling "deleteByDrag" in same editor as "insertFromDrop",
+ // we need to recreate edit action data here because
+ // `AutoEditActionDataSetter` needs to manage event state separately.
+ bool requestedByAnotherEditor = GetEditAction() != EditAction::eDrop;
+ AutoEditActionDataSetter editActionData(*this, EditAction::eDeleteByDrag);
+ MOZ_ASSERT(!SelectionRef().IsCollapsed());
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return rv;
+ }
+ // But keep using placeholder transaction for "insertFromDrop" if there is.
+ Maybe<AutoPlaceholderBatch> treatAsOneTransaction;
+ if (requestedByAnotherEditor) {
+ treatAsOneTransaction.emplace(*this, ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+ }
+
+ // We may need to update the source node to dispatch "dragend" below.
+ // Chrome restricts the new target under the <body> here. Therefore, we
+ // should follow it here.
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/editing_utilities.cc;l=254;drc=da35f4ed6398ae287d5adc828b9546eec95f668a
+ const RefPtr<Element> editingHost =
+ IsHTMLEditor() ? AsHTMLEditor()->ComputeEditingHost(
+ HTMLEditor::LimitInBodyElement::Yes)
+ : nullptr;
+
+ rv = DeleteSelectionAsSubAction(nsIEditor::eNone, IsTextEditor()
+ ? nsIEditor::eNoStrip
+ : nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteSelectionAsSubAction(eNone) failed");
+ return rv;
+ }
+
+ if (!mDispatchInputEvent) {
+ return NS_OK;
+ }
+
+ if (treatAsOneTransaction.isNothing()) {
+ DispatchInputEvent();
+ }
+
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+
+ // If we success everything here, we may need to retarget "dragend" event
+ // target for compatibility with the other browsers. They do this only when
+ // their builtin editor delete the source node from the document. Then,
+ // they retarget the source node to the editing host.
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/page/drag_controller.cc;l=724;drc=d9ba13b8cd8ac0faed7afc3d1f7e4b67ebac2a0b
+ if (editingHost) {
+ if (nsCOMPtr<nsIDragService> dragService =
+ do_GetService("@mozilla.org/widget/dragservice;1")) {
+ dragService->MaybeEditorDeletedSourceNode(editingHost);
+ }
+ }
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+nsresult EditorBase::DeleteSelectionWithTransaction(
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+
+ AutoRangeArray rangesToDelete(SelectionRef());
+ if (NS_WARN_IF(rangesToDelete.Ranges().IsEmpty())) {
+ NS_ASSERTION(
+ false,
+ "For avoiding to throw incompatible exception for `execCommand`, fix "
+ "the caller");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (IsTextEditor()) {
+ if (const Text* theTextNode = AsTextEditor()->GetTextNode()) {
+ rangesToDelete.EnsureRangesInTextNode(*theTextNode);
+ }
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError = DeleteRangesWithTransaction(
+ aDirectionAndAmount, aStripWrappers, rangesToDelete);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("EditorBase::DeleteRangesWithTransaction() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ }
+ NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ return NS_OK;
+}
+
+Result<CaretPoint, nsresult> EditorBase::DeleteRangesWithTransaction(
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ const AutoRangeArray& aRangesToDelete) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!Destroyed());
+ MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
+ MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
+
+ HowToHandleCollapsedRange howToHandleCollapsedRange =
+ EditorBase::HowToHandleCollapsedRangeFor(aDirectionAndAmount);
+ if (NS_WARN_IF(aRangesToDelete.IsCollapsed() &&
+ howToHandleCollapsedRange ==
+ HowToHandleCollapsedRange::Ignore)) {
+ NS_ASSERTION(
+ false,
+ "For avoiding to throw incompatible exception for `execCommand`, fix "
+ "the caller");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ RefPtr<DeleteMultipleRangesTransaction> deleteSelectionTransaction =
+ CreateTransactionForDeleteSelection(howToHandleCollapsedRange,
+ aRangesToDelete);
+ if (MOZ_UNLIKELY(!deleteSelectionTransaction)) {
+ NS_WARNING("EditorBase::CreateTransactionForDeleteSelection() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // XXX This is odd, this assumes that there are no multiple collapsed
+ // ranges in `Selection`, but it's possible scenario.
+ // XXX This loop looks slow, but it's rarely so because of multiple
+ // selection is not used so many times.
+ nsCOMPtr<nsIContent> deleteContent;
+ uint32_t deleteCharOffset = 0;
+ for (const OwningNonNull<EditTransactionBase>& transactionBase :
+ Reversed(deleteSelectionTransaction->ChildTransactions())) {
+ if (DeleteTextTransaction* deleteTextTransaction =
+ transactionBase->GetAsDeleteTextTransaction()) {
+ deleteContent = deleteTextTransaction->GetText();
+ deleteCharOffset = deleteTextTransaction->Offset();
+ break;
+ }
+ if (DeleteNodeTransaction* deleteNodeTransaction =
+ transactionBase->GetAsDeleteNodeTransaction()) {
+ deleteContent = deleteNodeTransaction->GetContent();
+ break;
+ }
+ }
+
+ RefPtr<CharacterData> deleteCharData =
+ CharacterData::FromNodeOrNull(deleteContent);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteSelectedContent, aDirectionAndAmount,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ if (IsHTMLEditor()) {
+ if (!deleteContent) {
+ // XXX We may remove multiple ranges in the following. Therefore,
+ // this must have a bug since we only add the first range into
+ // the changed range.
+ TopLevelEditSubActionDataRef().WillDeleteRange(
+ *this, aRangesToDelete.GetFirstRangeStartPoint<EditorRawDOMPoint>(),
+ aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>());
+ } else if (!deleteCharData) {
+ TopLevelEditSubActionDataRef().WillDeleteContent(*this, *deleteContent);
+ }
+ }
+
+ // Notify nsIEditActionListener::WillDelete[Selection|Text]
+ if (!mActionListeners.IsEmpty()) {
+ if (!deleteContent) {
+ MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
+ AutoTArray<RefPtr<nsRange>, 8> rangesToDelete(
+ aRangesToDelete.CloneRanges<RefPtr>());
+ AutoActionListenerArray listeners(mActionListeners.Clone());
+ for (auto& listener : listeners) {
+ DebugOnly<nsresult> rvIgnored =
+ listener->WillDeleteRanges(rangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::WillDeleteRanges() failed, but ignored");
+ MOZ_DIAGNOSTIC_ASSERT(!Destroyed(),
+ "nsIEditActionListener::WillDeleteRanges() "
+ "must not destroy the editor");
+ }
+ } else if (deleteCharData) {
+ AutoActionListenerArray listeners(mActionListeners.Clone());
+ for (auto& listener : listeners) {
+ // XXX Why don't we notify listeners of actual length?
+ DebugOnly<nsresult> rvIgnored =
+ listener->WillDeleteText(deleteCharData, deleteCharOffset, 1);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::WillDeleteText() failed, but ignored");
+ MOZ_DIAGNOSTIC_ASSERT(!Destroyed(),
+ "nsIEditActionListener::WillDeleteText() must "
+ "not destroy the editor");
+ }
+ }
+ }
+
+ // Delete the specified amount
+ nsresult rv = DoTransactionInternal(deleteSelectionTransaction);
+ // I'm not sure whether we should keep notifying edit action listeners or
+ // stop doing it. For now, just keep traditional behavior.
+ bool destroyedByTransaction = Destroyed();
+ NS_WARNING_ASSERTION(destroyedByTransaction || NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+
+ if (IsHTMLEditor() && deleteCharData) {
+ MOZ_ASSERT(deleteContent);
+ TopLevelEditSubActionDataRef().DidDeleteText(
+ *this, EditorRawDOMPoint(deleteContent));
+ }
+
+ if (mTextServicesDocument && NS_SUCCEEDED(rv) && deleteContent &&
+ !deleteCharData) {
+ RefPtr<TextServicesDocument> textServicesDocument = mTextServicesDocument;
+ textServicesDocument->DidDeleteContent(*deleteContent);
+ MOZ_ASSERT(
+ destroyedByTransaction || !Destroyed(),
+ "TextServicesDocument::DidDeleteContent() must not destroy the editor");
+ }
+
+ if (!mActionListeners.IsEmpty() && deleteContent && !deleteCharData) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored =
+ listener->DidDeleteNode(deleteContent, rv);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::DidDeleteNode() failed, but ignored");
+ MOZ_DIAGNOSTIC_ASSERT(
+ destroyedByTransaction || !Destroyed(),
+ "nsIEditActionListener::DidDeleteNode() must not destroy the editor");
+ }
+ }
+
+ if (NS_WARN_IF(destroyedByTransaction)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+
+ EditorDOMPoint pointToPutCaret =
+ deleteSelectionTransaction->SuggestPointToPutCaret();
+ if (IsHTMLEditor() && aStripWrappers == nsIEditor::eStrip) {
+ const nsCOMPtr<nsIContent> anchorContent =
+ pointToPutCaret.GetContainerAs<nsIContent>();
+ if (MOZ_LIKELY(anchorContent) &&
+ MOZ_LIKELY(HTMLEditUtils::IsSimplyEditableNode(*anchorContent)) &&
+ // FIXME: Perhaps, this should use `HTMLEditor::IsEmptyNode` instead.
+ !anchorContent->Length()) {
+ AutoTrackDOMPoint trackPoint(RangeUpdaterRef(), &pointToPutCaret);
+ nsresult rv =
+ MOZ_KnownLive(AsHTMLEditor())
+ ->RemoveEmptyInclusiveAncestorInlineElements(*anchorContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::RemoveEmptyInclusiveAncestorInlineElements() "
+ "failed");
+ return Err(rv);
+ }
+ }
+ }
+
+ return CaretPoint(std::move(pointToPutCaret));
+}
+
+already_AddRefed<Element> EditorBase::CreateHTMLContent(
+ const nsAtom* aTag) const {
+ MOZ_ASSERT(aTag);
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return nullptr;
+ }
+
+ // XXX Wallpaper over editor bug (editor tries to create elements with an
+ // empty nodename).
+ if (aTag == nsGkAtoms::_empty) {
+ NS_ERROR(
+ "Don't pass an empty tag to EditorBase::CreateHTMLContent, "
+ "check caller.");
+ return nullptr;
+ }
+
+ return document->CreateElem(nsDependentAtomString(aTag), nullptr,
+ kNameSpaceID_XHTML);
+}
+
+already_AddRefed<nsTextNode> EditorBase::CreateTextNode(
+ const nsAString& aData) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return nullptr;
+ }
+ RefPtr<nsTextNode> text = document->CreateEmptyTextNode();
+ text->MarkAsMaybeModifiedFrequently();
+ if (IsPasswordEditor()) {
+ text->MarkAsMaybeMasked();
+ }
+ // Don't notify; this node is still being created.
+ DebugOnly<nsresult> rvIgnored = text->SetText(aData, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Text::SetText() failed, but ignored");
+ return text.forget();
+}
+
+NS_IMETHODIMP EditorBase::SetAttributeOrEquivalent(Element* aElement,
+ const nsAString& aAttribute,
+ const nsAString& aValue,
+ bool aSuppressTransaction) {
+ if (NS_WARN_IF(!aElement)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetAttribute);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<nsAtom> attribute = NS_Atomize(aAttribute);
+ rv = SetAttributeOrEquivalent(aElement, attribute, aValue,
+ aSuppressTransaction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeOrEquivalent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP EditorBase::RemoveAttributeOrEquivalent(
+ Element* aElement, const nsAString& aAttribute, bool aSuppressTransaction) {
+ if (NS_WARN_IF(!aElement)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveAttribute);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<nsAtom> attribute = NS_Atomize(aAttribute);
+ rv = RemoveAttributeOrEquivalent(aElement, attribute, aSuppressTransaction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeOrEquivalent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+void EditorBase::HandleKeyPressEventInReadOnlyMode(
+ WidgetKeyboardEvent& aKeyboardEvent) const {
+ MOZ_ASSERT(IsReadonly());
+ MOZ_ASSERT(aKeyboardEvent.mMessage == eKeyPress);
+
+ switch (aKeyboardEvent.mKeyCode) {
+ case NS_VK_BACK:
+ // If it's a `Backspace` key, let's consume it because it may be mapped
+ // to "Back" of the history navigation. So, it's possible that user
+ // tries to delete a character with `Backspace` even in the read-only
+ // editor.
+ aKeyboardEvent.PreventDefault();
+ break;
+ }
+ // XXX How about space key (page up and page down in browser navigation)?
+}
+
+nsresult EditorBase::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) {
+ MOZ_ASSERT(!IsReadonly());
+ MOZ_ASSERT(aKeyboardEvent);
+ MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress);
+
+ // NOTE: When you change this method, you should also change:
+ // * editor/libeditor/tests/test_texteditor_keyevent_handling.html
+ // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
+ //
+ // And also when you add new key handling, you need to change the subclass's
+ // HandleKeyPressEvent()'s switch statement.
+
+ switch (aKeyboardEvent->mKeyCode) {
+ case NS_VK_META:
+ case NS_VK_WIN:
+ case NS_VK_SHIFT:
+ case NS_VK_CONTROL:
+ case NS_VK_ALT:
+ MOZ_ASSERT_UNREACHABLE(
+ "eKeyPress event shouldn't be fired for modifier keys");
+ return NS_ERROR_UNEXPECTED;
+
+ case NS_VK_BACK: {
+ if (aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() ||
+ aKeyboardEvent->IsMeta()) {
+ return NS_OK;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ DeleteSelectionAsAction(nsIEditor::ePrevious, nsIEditor::eStrip);
+ aKeyboardEvent->PreventDefault();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::DeleteSelectionAsAction() failed, but ignored");
+ return NS_OK;
+ }
+ case NS_VK_DELETE: {
+ // on certain platforms (such as windows) the shift key
+ // modifies what delete does (cmd_cut in this case).
+ // bailing here to allow the keybindings to do the cut.
+ if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() ||
+ aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta()) {
+ return NS_OK;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ DeleteSelectionAsAction(nsIEditor::eNext, nsIEditor::eStrip);
+ aKeyboardEvent->PreventDefault();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::DeleteSelectionAsAction() failed, but ignored");
+ return NS_OK;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult EditorBase::OnInputText(const nsAString& aStringToInsert) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText);
+ MOZ_ASSERT(!aStringToInsert.IsVoid());
+ editActionData.SetData(aStringToInsert);
+ // FYI: For conforming to current UI Events spec, we should dispatch
+ // "beforeinput" event before "keypress" event, but here is in a
+ // "keypress" event listener. However, the other browsers dispatch
+ // "beforeinput" event after "keypress" event. Therefore, it makes
+ // sense to follow the other browsers. Spec issue:
+ // https://github.com/w3c/uievents/issues/220
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+ rv = InsertTextAsSubAction(aStringToInsert, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::ReplaceTextAsAction(
+ const nsAString& aString, nsRange* aReplaceRange,
+ AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound);
+ MOZ_ASSERT_IF(!aReplaceRange, IsTextEditor());
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eReplaceText,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ if (aAllowBeforeInputEventCancelable == AllowBeforeInputEventCancelable::No) {
+ editActionData.MakeBeforeInputEventNonCancelable();
+ }
+
+ if (IsTextEditor()) {
+ editActionData.SetData(aString);
+ } else {
+ editActionData.InitializeDataTransfer(aString);
+ RefPtr<StaticRange> targetRange;
+ if (aReplaceRange) {
+ // Compute offset of the range before dispatching `beforeinput` event
+ // because it may be referred after the DOM tree is changed and the
+ // range may have not computed the offset yet.
+ targetRange = StaticRange::Create(
+ aReplaceRange->GetStartContainer(), aReplaceRange->StartOffset(),
+ aReplaceRange->GetEndContainer(), aReplaceRange->EndOffset(),
+ IgnoreErrors());
+ NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(),
+ "StaticRange::Create() failed");
+ } else {
+ Element* editingHost = AsHTMLEditor()->ComputeEditingHost();
+ NS_WARNING_ASSERTION(editingHost,
+ "No active editing host, no target ranges");
+ if (editingHost) {
+ targetRange = StaticRange::Create(
+ editingHost, 0, editingHost, editingHost->Length(), IgnoreErrors());
+ NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(),
+ "StaticRange::Create() failed");
+ }
+ }
+ if (targetRange && targetRange->IsPositioned()) {
+ editActionData.AppendTargetRange(*targetRange);
+ }
+ }
+
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ // This should emulates inserting text for better undo/redo behavior.
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return EditorBase::ToGenericNSResult(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ if (!aReplaceRange) {
+ // Use fast path if we're `TextEditor` because it may be in a hot path.
+ if (IsTextEditor()) {
+ nsresult rv = MOZ_KnownLive(AsTextEditor())->SetTextAsSubAction(aString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetTextAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Setting value of `HTMLEditor` isn't supported");
+ return EditorBase::ToGenericNSResult(NS_ERROR_FAILURE);
+ }
+
+ if (aString.IsEmpty() && aReplaceRange->Collapsed()) {
+ NS_WARNING("Setting value was empty and replaced range was empty");
+ return NS_OK;
+ }
+
+ // Note that do not notify selectionchange caused by selecting all text
+ // because it's preparation of our delete implementation so web apps
+ // shouldn't receive such selectionchange before the first mutation.
+ AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__);
+
+ // Select the range but as far as possible, we should not create new range
+ // even if it's part of special Selection.
+ ErrorResult error;
+ SelectionRef().RemoveAllRanges(error);
+ if (error.Failed()) {
+ NS_WARNING("Selection::RemoveAllRanges() failed");
+ return error.StealNSResult();
+ }
+ SelectionRef().AddRangeAndSelectFramesAndNotifyListeners(*aReplaceRange,
+ error);
+ if (error.Failed()) {
+ NS_WARNING("Selection::AddRangeAndSelectFramesAndNotifyListeners() failed");
+ return error.StealNSResult();
+ }
+
+ rv = ReplaceSelectionAsSubAction(aString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::ReplaceSelectionAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::ReplaceSelectionAsSubAction(const nsAString& aString) {
+ if (aString.IsEmpty()) {
+ nsresult rv = DeleteSelectionAsSubAction(
+ nsIEditor::eNone,
+ IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsSubAction(eNone) failed");
+ return rv;
+ }
+
+ nsresult rv = InsertTextAsSubAction(aString, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+}
+
+nsresult EditorBase::HandleInlineSpellCheck(
+ const EditorDOMPoint& aPreviouslySelectedStart,
+ const AbstractRange* aRange) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!mInlineSpellChecker) {
+ return NS_OK;
+ }
+ nsresult rv = mInlineSpellChecker->SpellCheckAfterEditorChange(
+ GetTopLevelEditSubAction(), SelectionRef(),
+ aPreviouslySelectedStart.GetContainer(),
+ aPreviouslySelectedStart.Offset(),
+ aRange ? aRange->GetStartContainer() : nullptr,
+ aRange ? aRange->StartOffset() : 0,
+ aRange ? aRange->GetEndContainer() : nullptr,
+ aRange ? aRange->EndOffset() : 0);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "mozInlineSpellChecker::SpellCheckAfterEditorChange() failed");
+ return rv;
+}
+
+Element* EditorBase::FindSelectionRoot(const nsINode& aNode) const {
+ return GetRoot();
+}
+
+void EditorBase::InitializeSelectionAncestorLimit(
+ nsIContent& aAncestorLimit) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ SelectionRef().SetAncestorLimiter(&aAncestorLimit);
+}
+
+nsresult EditorBase::InitializeSelection(
+ const nsINode& aOriginalEventTargetNode) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsCOMPtr<nsIContent> selectionRootContent =
+ FindSelectionRoot(aOriginalEventTargetNode);
+ if (!selectionRootContent) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISelectionController> selectionController =
+ GetSelectionController();
+ if (NS_WARN_IF(!selectionController)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Init the caret
+ RefPtr<nsCaret> caret = GetCaret();
+ if (NS_WARN_IF(!caret)) {
+ return NS_ERROR_FAILURE;
+ }
+ caret->SetSelection(&SelectionRef());
+ DebugOnly<nsresult> rvIgnored =
+ selectionController->SetCaretReadOnly(IsReadonly());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISelectionController::SetCaretReadOnly() failed, but ignored");
+ rvIgnored = selectionController->SetCaretEnabled(true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISelectionController::SetCaretEnabled() failed, but ignored");
+ // NOTE(emilio): It's important for this call to be after
+ // SetCaretEnabled(true), since that would override mIgnoreUserModify to true.
+ //
+ // Also, make sure to always ignore it for designMode, since that effectively
+ // overrides everything and we allow to edit stuff with
+ // contenteditable="false" subtrees in such a document.
+ caret->SetIgnoreUserModify(aOriginalEventTargetNode.IsInDesignMode());
+
+ // Init selection
+ rvIgnored =
+ selectionController->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISelectionController::SetSelectionFlags() failed, but ignored");
+
+ selectionController->SelectionWillTakeFocus();
+
+ // If the computed selection root isn't root content, we should set it
+ // as selection ancestor limit. However, if that is root element, it means
+ // there is not limitation of the selection, then, we must set nullptr.
+ // NOTE: If we set a root element to the ancestor limit, some selection
+ // methods don't work fine.
+ if (selectionRootContent->GetParent()) {
+ InitializeSelectionAncestorLimit(*selectionRootContent);
+ } else {
+ SelectionRef().SetAncestorLimiter(nullptr);
+ }
+
+ // If there is composition when this is called, we may need to restore IME
+ // selection because if the editor is reframed, this already forgot IME
+ // selection and the transaction.
+ if (mComposition && mComposition->IsMovingToNewTextNode()) {
+ MOZ_DIAGNOSTIC_ASSERT(IsTextEditor());
+ if (NS_WARN_IF(!IsTextEditor())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ // We need to look for the new text node from current selection.
+ // XXX If selection is changed during reframe, this doesn't work well!
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+ EditorRawDOMPoint atStartOfFirstRange(firstRange->StartRef());
+ EditorRawDOMPoint betterInsertionPoint =
+ AsTextEditor()->FindBetterInsertionPoint(atStartOfFirstRange);
+ RefPtr<Text> textNode = betterInsertionPoint.GetContainerAs<Text>();
+ MOZ_ASSERT(textNode,
+ "There must be text node if composition string is not empty");
+ if (textNode) {
+ MOZ_ASSERT(textNode->Length() >= mComposition->XPEndOffsetInTextNode(),
+ "The text node must be different from the old text node");
+ RefPtr<TextRangeArray> ranges = mComposition->GetRanges();
+ DebugOnly<nsresult> rvIgnored = CompositionTransaction::SetIMESelection(
+ *this, textNode, mComposition->XPOffsetInTextNode(),
+ mComposition->XPLengthInTextNode(), ranges);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "CompositionTransaction::SetIMESelection() failed, but ignored");
+ mComposition->OnUpdateCompositionInEditor(
+ mComposition->String(), *textNode,
+ mComposition->XPOffsetInTextNode());
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult EditorBase::FinalizeSelection() {
+ nsCOMPtr<nsISelectionController> selectionController =
+ GetSelectionController();
+ if (NS_WARN_IF(!selectionController)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ SelectionRef().SetAncestorLimiter(nullptr);
+
+ if (NS_WARN_IF(!GetPresShell())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (RefPtr<nsCaret> caret = GetCaret()) {
+ caret->SetIgnoreUserModify(true);
+ DebugOnly<nsresult> rvIgnored = selectionController->SetCaretEnabled(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISelectionController::SetCaretEnabled(false) failed, but ignored");
+ }
+
+ RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ // TODO: Running script from here makes harder to handle blur events. We
+ // should do this asynchronously.
+ focusManager->UpdateCaretForCaretBrowsingMode();
+ if (Element* rootElement = GetExposedRoot()) {
+ if (rootElement->OwnerDoc()->GetUnretargetedFocusedContent() !=
+ rootElement) {
+ selectionController->SelectionWillLoseFocus();
+ } else {
+ // We leave this selection as the focused one. When the focus returns, it
+ // either returns to us (nothing to do), or it returns to something else,
+ // and nsDocumentViewerFocusListener::HandleEvent fixes it up.
+ }
+ }
+ return NS_OK;
+}
+
+Element* EditorBase::GetExposedRoot() const {
+ Element* rootElement = GetRoot();
+ if (!rootElement || !rootElement->IsInNativeAnonymousSubtree()) {
+ return rootElement;
+ }
+ return Element::FromNodeOrNull(
+ rootElement->GetClosestNativeAnonymousSubtreeRootParentOrHost());
+}
+
+nsresult EditorBase::DetermineCurrentDirection() {
+ // Get the current root direction from its frame
+ Element* rootElement = GetExposedRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If we don't have an explicit direction, determine our direction
+ // from the content's direction
+ if (!IsRightToLeft() && !IsLeftToRight()) {
+ nsIFrame* frameForRootElement = rootElement->GetPrimaryFrame();
+ if (NS_WARN_IF(!frameForRootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Set the flag here, to enable us to use the same code path below.
+ // It will be flipped before returning from the function.
+ if (frameForRootElement->StyleVisibility()->mDirection ==
+ StyleDirection::Rtl) {
+ mFlags |= nsIEditor::eEditorRightToLeft;
+ } else {
+ mFlags |= nsIEditor::eEditorLeftToRight;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult EditorBase::ToggleTextDirectionAsAction(nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetTextDirection,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = DetermineCurrentDirection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DetermineCurrentDirection() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ MOZ_ASSERT(IsRightToLeft() || IsLeftToRight());
+ // Note that we need to consider new direction before dispatching
+ // "beforeinput" event since "beforeinput" event listener may change it
+ // but not canceled.
+ TextDirection newDirection =
+ IsRightToLeft() ? TextDirection::eLTR : TextDirection::eRTL;
+ editActionData.SetData(IsRightToLeft() ? u"ltr"_ns : u"rtl"_ns);
+
+ // FYI: Oddly, Chrome does not dispatch beforeinput event in this case but
+ // dispatches input event.
+ rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = SetTextDirectionTo(newDirection);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::SetTextDirectionTo() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ editActionData.MarkAsHandled();
+
+ // XXX When we don't change the text direction, do we really need to
+ // dispatch input event?
+ DispatchInputEvent();
+
+ return NS_OK;
+}
+
+void EditorBase::SwitchTextDirectionTo(TextDirection aTextDirection) {
+ MOZ_ASSERT(aTextDirection == TextDirection::eLTR ||
+ aTextDirection == TextDirection::eRTL);
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetTextDirection);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ nsresult rv = DetermineCurrentDirection();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ editActionData.SetData(aTextDirection == TextDirection::eLTR ? u"ltr"_ns
+ : u"rtl"_ns);
+
+ // FYI: Oddly, Chrome does not dispatch beforeinput event in this case but
+ // dispatches input event.
+ rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return;
+ }
+
+ if ((aTextDirection == TextDirection::eLTR && IsRightToLeft()) ||
+ (aTextDirection == TextDirection::eRTL && IsLeftToRight())) {
+ // Do it only when the direction is still different from the original
+ // new direction. Note that "beforeinput" event listener may have already
+ // changed the direction here, but they may not cancel the event.
+ nsresult rv = SetTextDirectionTo(aTextDirection);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::SetTextDirectionTo() failed");
+ return;
+ }
+ }
+
+ editActionData.MarkAsHandled();
+
+ // XXX When we don't change the text direction, do we really need to
+ // dispatch input event?
+ DispatchInputEvent();
+}
+
+nsresult EditorBase::SetTextDirectionTo(TextDirection aTextDirection) {
+ Element* const editingHostOrTextControlElement =
+ IsHTMLEditor() ? AsHTMLEditor()->ComputeEditingHost(
+ HTMLEditor::LimitInBodyElement::No)
+ : GetExposedRoot();
+ if (!editingHostOrTextControlElement) { // Don't warn, HTMLEditor may have no
+ // active editing host
+ return NS_OK;
+ }
+
+ if (aTextDirection == TextDirection::eLTR) {
+ NS_ASSERTION(!IsLeftToRight(), "Unexpected mutually exclusive flag");
+ mFlags &= ~nsIEditor::eEditorRightToLeft;
+ mFlags |= nsIEditor::eEditorLeftToRight;
+ nsresult rv = editingHostOrTextControlElement->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::dir, u"ltr"_ns, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Element::SetAttr(nsGkAtoms::dir, ltr) failed");
+ return rv;
+ }
+
+ if (aTextDirection == TextDirection::eRTL) {
+ NS_ASSERTION(!IsRightToLeft(), "Unexpected mutually exclusive flag");
+ mFlags |= nsIEditor::eEditorRightToLeft;
+ mFlags &= ~nsIEditor::eEditorLeftToRight;
+ nsresult rv = editingHostOrTextControlElement->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::dir, u"rtl"_ns, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Element::SetAttr(nsGkAtoms::dir, rtl) failed");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+Element* EditorBase::GetFocusedElement() const {
+ EventTarget* eventTarget = GetDOMEventTarget();
+ if (!eventTarget) {
+ return nullptr;
+ }
+
+ nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return nullptr;
+ }
+
+ Element* focusedElement = focusManager->GetFocusedElement();
+ MOZ_ASSERT((focusedElement == eventTarget) ==
+ SameCOMIdentity(focusedElement, eventTarget));
+
+ return (focusedElement == eventTarget) ? focusedElement : nullptr;
+}
+
+bool EditorBase::IsActiveInDOMWindow() const {
+ EventTarget* piTarget = GetDOMEventTarget();
+ if (!piTarget) {
+ return false;
+ }
+
+ nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return false; // Do we need to check the singleton instance??
+ }
+
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return false;
+ }
+ nsPIDOMWindowOuter* ourWindow = document->GetWindow();
+ nsCOMPtr<nsPIDOMWindowOuter> win;
+ nsIContent* content = nsFocusManager::GetFocusedDescendant(
+ ourWindow, nsFocusManager::eOnlyCurrentWindow, getter_AddRefs(win));
+ return SameCOMIdentity(content, piTarget);
+}
+
+bool EditorBase::IsAcceptableInputEvent(WidgetGUIEvent* aGUIEvent) const {
+ // If the event is trusted, the event should always cause input.
+ if (NS_WARN_IF(!aGUIEvent)) {
+ return false;
+ }
+
+ // If this is dispatched by using cordinates but this editor doesn't have
+ // focus, we shouldn't handle it.
+ if (aGUIEvent->IsUsingCoordinates() && !GetFocusedElement()) {
+ return false;
+ }
+
+ // If a composition event isn't dispatched via widget, we need to ignore them
+ // since they cannot be managed by TextComposition. E.g., the event was
+ // created by chrome JS.
+ // Note that if we allow to handle such events, editor may be confused by
+ // strange event order.
+ bool needsWidget = false;
+ switch (aGUIEvent->mMessage) {
+ case eUnidentifiedEvent:
+ // If events are not created with proper event interface, their message
+ // are initialized with eUnidentifiedEvent. Let's ignore such event.
+ return false;
+ case eCompositionStart:
+ case eCompositionEnd:
+ case eCompositionUpdate:
+ case eCompositionChange:
+ case eCompositionCommitAsIs:
+ // Don't allow composition events whose internal event are not
+ // WidgetCompositionEvent.
+ if (!aGUIEvent->AsCompositionEvent()) {
+ return false;
+ }
+ needsWidget = true;
+ break;
+ default:
+ break;
+ }
+ if (needsWidget && !aGUIEvent->mWidget) {
+ return false;
+ }
+
+ // Accept all trusted events.
+ if (aGUIEvent->IsTrusted()) {
+ return true;
+ }
+
+ // Ignore untrusted mouse event.
+ // XXX Why are we handling other untrusted input events?
+ if (aGUIEvent->AsMouseEventBase()) {
+ return false;
+ }
+
+ // Otherwise, we shouldn't handle any input events when we're not an active
+ // element of the DOM window.
+ return IsActiveInDOMWindow();
+}
+
+nsresult EditorBase::FlushPendingSpellCheck() {
+ // If the spell check skip flag is still enabled from creation time,
+ // disable it because focused editors are allowed to spell check.
+ if (!ShouldSkipSpellCheck()) {
+ return NS_OK;
+ }
+ MOZ_ASSERT(!IsHTMLEditor(), "HTMLEditor should not has pending spell checks");
+ nsresult rv = RemoveFlags(nsIEditor::eEditorSkipSpellCheck);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::RemoveFlags(nsIEditor::eEditorSkipSpellCheck) failed");
+ return rv;
+}
+
+bool EditorBase::CanKeepHandlingFocusEvent(
+ const nsINode& aOriginalEventTargetNode) const {
+ if (MOZ_UNLIKELY(!IsListeningToEvents() || Destroyed())) {
+ return false;
+ }
+
+ nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
+ if (MOZ_UNLIKELY(!focusManager)) {
+ return false;
+ }
+
+ // If the event target is document mode, we only need to handle the focus
+ // event when the document is still in designMode. Otherwise, the
+ // mode has been disabled by somebody while we're handling the focus event.
+ if (aOriginalEventTargetNode.IsDocument()) {
+ return IsHTMLEditor() && aOriginalEventTargetNode.IsInDesignMode();
+ }
+ MOZ_ASSERT(aOriginalEventTargetNode.IsContent());
+
+ // If nobody has focus, the focus event target has been blurred by somebody
+ // else. So the editor shouldn't initialize itself to start to handle
+ // anything.
+ if (!focusManager->GetFocusedElement()) {
+ return false;
+ }
+
+ // If there's an HTMLEditor registered in the target document and we
+ // are not that HTMLEditor (for cases like nested documents), let
+ // that HTMLEditor to handle the focus event.
+ if (IsHTMLEditor()) {
+ const HTMLEditor* precedentHTMLEditor =
+ aOriginalEventTargetNode.OwnerDoc()->GetHTMLEditor();
+
+ if (precedentHTMLEditor && precedentHTMLEditor != this) {
+ return false;
+ }
+ }
+
+ const nsIContent* exposedTargetContent =
+ aOriginalEventTargetNode.AsContent()
+ ->FindFirstNonChromeOnlyAccessContent();
+ const nsIContent* exposedFocusedContent =
+ focusManager->GetFocusedElement()->FindFirstNonChromeOnlyAccessContent();
+ return exposedTargetContent && exposedFocusedContent &&
+ exposedTargetContent == exposedFocusedContent;
+}
+
+nsresult EditorBase::OnFocus(const nsINode& aOriginalEventTargetNode) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ InitializeSelection(aOriginalEventTargetNode);
+ mSpellCheckerDictionaryUpdated = false;
+ if (mInlineSpellChecker && CanEnableSpellCheck()) {
+ DebugOnly<nsresult> rvIgnored =
+ mInlineSpellChecker->UpdateCurrentDictionary();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "mozInlineSpellCHecker::UpdateCurrentDictionary() failed, but ignored");
+ mSpellCheckerDictionaryUpdated = true;
+ }
+ // XXX Why don't we stop handling focus with the spell checker immediately
+ // after calling InitializeSelection?
+ if (MOZ_UNLIKELY(!CanKeepHandlingFocusEvent(aOriginalEventTargetNode))) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+
+ const RefPtr<Element> focusedElement = GetFocusedElement();
+ RefPtr<nsPresContext> presContext =
+ focusedElement ? focusedElement->GetPresContext(
+ Element::PresContextFor::eForComposedDoc)
+ : GetPresContext();
+ if (NS_WARN_IF(!presContext)) {
+ return NS_ERROR_FAILURE;
+ }
+ IMEStateManager::OnFocusInEditor(*presContext, focusedElement, *this);
+
+ return NS_OK;
+}
+
+void EditorBase::HideCaret(bool aHide) {
+ if (mHidingCaret == aHide) {
+ return;
+ }
+
+ RefPtr<nsCaret> caret = GetCaret();
+ if (NS_WARN_IF(!caret)) {
+ return;
+ }
+
+ mHidingCaret = aHide;
+ if (aHide) {
+ caret->AddForceHide();
+ } else {
+ caret->RemoveForceHide();
+ }
+}
+
+NS_IMETHODIMP EditorBase::Unmask(uint32_t aStart, int64_t aEnd,
+ uint32_t aTimeout, uint8_t aArgc) {
+ if (NS_WARN_IF(!IsPasswordEditor())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ if (NS_WARN_IF(aArgc >= 1 && aStart == UINT32_MAX) ||
+ NS_WARN_IF(aArgc >= 2 && aEnd == 0) ||
+ NS_WARN_IF(aArgc >= 2 && aEnd > 0 && aStart >= aEnd)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eHidePassword);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ uint32_t start = aArgc < 1 ? 0 : aStart;
+ uint32_t length = aArgc < 2 || aEnd < 0 ? UINT32_MAX : aEnd - start;
+ uint32_t timeout = aArgc < 3 ? 0 : aTimeout;
+ nsresult rv = MOZ_KnownLive(AsTextEditor())
+ ->SetUnmaskRangeAndNotify(start, length, timeout);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::SetUnmaskRangeAndNotify() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Flush pending layout right now since the caller may access us before
+ // doing it.
+ if (RefPtr<PresShell> presShell = GetPresShell()) {
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::Mask() {
+ if (NS_WARN_IF(!IsPasswordEditor())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eHidePassword);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = MOZ_KnownLive(AsTextEditor())->MaskAllCharactersAndNotify();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::MaskAllCharactersAndNotify() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Flush pending layout right now since the caller may access us before
+ // doing it.
+ if (RefPtr<PresShell> presShell = GetPresShell()) {
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetUnmaskedStart(uint32_t* aResult) {
+ if (NS_WARN_IF(!IsPasswordEditor())) {
+ *aResult = 0;
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ *aResult =
+ AsTextEditor()->IsAllMasked() ? 0 : AsTextEditor()->UnmaskedStart();
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetUnmaskedEnd(uint32_t* aResult) {
+ if (NS_WARN_IF(!IsPasswordEditor())) {
+ *aResult = 0;
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ *aResult = AsTextEditor()->IsAllMasked() ? 0 : AsTextEditor()->UnmaskedEnd();
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetAutoMaskingEnabled(bool* aResult) {
+ if (NS_WARN_IF(!IsPasswordEditor())) {
+ *aResult = false;
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ *aResult = AsTextEditor()->IsMaskingPassword();
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::GetPasswordMask(nsAString& aPasswordMask) {
+ aPasswordMask.Assign(TextEditor::PasswordMask());
+ return NS_OK;
+}
+
+template <typename PT, typename CT>
+EditorBase::AutoCaretBidiLevelManager::AutoCaretBidiLevelManager(
+ const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPointBase<PT, CT>& aPointAtCaret) {
+ MOZ_ASSERT(aEditorBase.IsEditActionDataAvailable());
+
+ nsPresContext* presContext = aEditorBase.GetPresContext();
+ if (NS_WARN_IF(!presContext)) {
+ mFailed = true;
+ return;
+ }
+
+ if (!presContext->BidiEnabled()) {
+ return; // Perform the deletion
+ }
+
+ if (!aPointAtCaret.IsInContentNode()) {
+ mFailed = true;
+ return;
+ }
+
+ // XXX Not sure whether this requires strong reference here.
+ RefPtr<nsFrameSelection> frameSelection =
+ aEditorBase.SelectionRef().GetFrameSelection();
+ if (NS_WARN_IF(!frameSelection)) {
+ mFailed = true;
+ return;
+ }
+
+ nsPrevNextBidiLevels levels = frameSelection->GetPrevNextBidiLevels(
+ aPointAtCaret.template ContainerAs<nsIContent>(), aPointAtCaret.Offset(),
+ true);
+
+ mozilla::intl::BidiEmbeddingLevel levelBefore = levels.mLevelBefore;
+ mozilla::intl::BidiEmbeddingLevel levelAfter = levels.mLevelAfter;
+
+ mozilla::intl::BidiEmbeddingLevel currentCaretLevel =
+ frameSelection->GetCaretBidiLevel();
+
+ mozilla::intl::BidiEmbeddingLevel levelOfDeletion;
+ levelOfDeletion = (nsIEditor::eNext == aDirectionAndAmount ||
+ nsIEditor::eNextWord == aDirectionAndAmount)
+ ? levelAfter
+ : levelBefore;
+
+ if (currentCaretLevel == levelOfDeletion) {
+ return; // Perform the deletion
+ }
+
+ // Set the bidi level of the caret to that of the
+ // character that will be (or would have been) deleted
+ mNewCaretBidiLevel = Some(levelOfDeletion);
+ mCanceled =
+ !StaticPrefs::bidi_edit_delete_immediately() && levelBefore != levelAfter;
+}
+
+void EditorBase::AutoCaretBidiLevelManager::MaybeUpdateCaretBidiLevel(
+ const EditorBase& aEditorBase) const {
+ MOZ_ASSERT(!mFailed);
+ if (mNewCaretBidiLevel.isNothing()) {
+ return;
+ }
+ RefPtr<nsFrameSelection> frameSelection =
+ aEditorBase.SelectionRef().GetFrameSelection();
+ MOZ_ASSERT(frameSelection);
+ frameSelection->SetCaretBidiLevelAndMaybeSchedulePaint(
+ mNewCaretBidiLevel.value());
+}
+
+void EditorBase::UndefineCaretBidiLevel() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ /**
+ * After inserting text the caret Bidi level must be set to the level of the
+ * inserted text.This is difficult, because we cannot know what the level is
+ * until after the Bidi algorithm is applied to the whole paragraph.
+ *
+ * So we set the caret Bidi level to UNDEFINED here, and the caret code will
+ * set it correctly later
+ */
+ nsFrameSelection* frameSelection = SelectionRef().GetFrameSelection();
+ if (frameSelection) {
+ frameSelection->UndefineCaretBidiLevel();
+ }
+}
+
+NS_IMETHODIMP EditorBase::GetTextLength(uint32_t* aCount) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP EditorBase::GetNewlineHandling(int32_t* aNewlineHandling) {
+ if (NS_WARN_IF(!aNewlineHandling)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aNewlineHandling = mNewlineHandling;
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::SetNewlineHandling(int32_t aNewlineHandling) {
+ switch (aNewlineHandling) {
+ case nsIEditor::eNewlinesPasteIntact:
+ case nsIEditor::eNewlinesPasteToFirst:
+ case nsIEditor::eNewlinesReplaceWithSpaces:
+ case nsIEditor::eNewlinesStrip:
+ case nsIEditor::eNewlinesReplaceWithCommas:
+ case nsIEditor::eNewlinesStripSurroundingWhitespace:
+ mNewlineHandling = aNewlineHandling;
+ return NS_OK;
+ default:
+ NS_ERROR("SetNewlineHandling() is called with wrong value");
+ return NS_ERROR_INVALID_ARG;
+ }
+}
+
+bool EditorBase::IsSelectionRangeContainerNotContent() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // TODO: Make all callers use !AutoRangeArray::IsInContent() instead.
+ const uint32_t rangeCount = SelectionRef().RangeCount();
+ for (const uint32_t i : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(SelectionRef().RangeCount() == rangeCount);
+ const nsRange* range = SelectionRef().GetRangeAt(i);
+ MOZ_ASSERT(range);
+ if (MOZ_UNLIKELY(!range) || MOZ_UNLIKELY(!range->GetStartContainer()) ||
+ MOZ_UNLIKELY(!range->GetStartContainer()->IsContent()) ||
+ MOZ_UNLIKELY(!range->GetEndContainer()) ||
+ MOZ_UNLIKELY(!range->GetEndContainer()->IsContent())) {
+ return true;
+ }
+ }
+ return false;
+}
+
+NS_IMETHODIMP EditorBase::InsertText(const nsAString& aStringToInsert) {
+ nsresult rv = InsertTextAsAction(aStringToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsAction() failed");
+ return rv;
+}
+
+nsresult EditorBase::InsertTextAsAction(const nsAString& aStringToInsert,
+ nsIPrincipal* aPrincipal) {
+ // Showing this assertion is fine if this method is called by outside via
+ // mutation event listener or something. Otherwise, this is called by
+ // wrong method.
+ NS_ASSERTION(!mPlaceholderBatch,
+ "Should be called only when this is the only edit action of the "
+ "operation "
+ "unless mutation event listener nests some operations");
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText,
+ aPrincipal);
+ // Note that we don't need to replace native line breaks with XP line breaks
+ // here because Chrome does not do it.
+ MOZ_ASSERT(!aStringToInsert.IsVoid());
+ editActionData.SetData(aStringToInsert);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ nsString stringToInsert(aStringToInsert);
+ if (IsTextEditor()) {
+ nsContentUtils::PlatformToDOMLineBreaks(stringToInsert);
+ }
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertTextAsSubAction(stringToInsert, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult EditorBase::InsertTextAsSubAction(
+ const nsAString& aStringToInsert, SelectionHandling aSelectionHandling) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(mPlaceholderBatch);
+ MOZ_ASSERT(IsHTMLEditor() ||
+ aStringToInsert.FindChar(nsCRT::CR) == kNotFound);
+ MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore, mComposition);
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+
+ EditSubAction editSubAction = ShouldHandleIMEComposition()
+ ? EditSubAction::eInsertTextComingFromIME
+ : EditSubAction::eInsertText;
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, editSubAction, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ Result<EditActionResult, nsresult> result =
+ HandleInsertText(editSubAction, aStringToInsert, aSelectionHandling);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("EditorBase::HandleInsertText() failed");
+ return result.unwrapErr();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorBase::InsertLineBreak() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+/*****************************************************************************
+ * mozilla::EditorBase::AutoEditActionDataSetter
+ *****************************************************************************/
+
+EditorBase::AutoEditActionDataSetter::AutoEditActionDataSetter(
+ const EditorBase& aEditorBase, EditAction aEditAction,
+ nsIPrincipal* aPrincipal /* = nullptr */)
+ : mEditorBase(const_cast<EditorBase&>(aEditorBase)),
+ mPrincipal(aPrincipal),
+ mParentData(aEditorBase.mEditActionData),
+ mData(VoidString()),
+ mRawEditAction(aEditAction),
+ mTopLevelEditSubAction(EditSubAction::eNone),
+ mAborted(false),
+ mHasTriedToDispatchBeforeInputEvent(false),
+ mBeforeInputEventCanceled(false),
+ mMakeBeforeInputEventNonCancelable(false),
+ mHasTriedToDispatchClipboardEvent(false),
+ mEditorWasDestroyedDuringHandlingEditAction(
+ mParentData &&
+ mParentData->mEditorWasDestroyedDuringHandlingEditAction),
+ mHandled(false) {
+ // If we're nested edit action, copies necessary data from the parent.
+ if (mParentData) {
+ mSelection = mParentData->mSelection;
+ MOZ_ASSERT(!mSelection ||
+ (mSelection->GetType() == SelectionType::eNormal));
+
+ // If we're not editing something, we should inherit the parent's edit
+ // action. This may occur if creator or its callee use public methods which
+ // just returns something.
+ if (IsEditActionInOrderToEditSomething(aEditAction)) {
+ mEditAction = aEditAction;
+ } else {
+ mEditAction = mParentData->mEditAction;
+ // If we inherit an edit action whose handler needs to dispatch a
+ // clipboard event, we should inherit the clipboard dispatching state
+ // too because this nest occurs by a clipboard event listener or
+ // a beforeinput/mutation event listener is important for checking
+ // whether we've already called `MaybeDispatchBeforeInputEvent()`
+ // property in some points. If the former case, not yet dispatching
+ // beforeinput event is okay (not fine).
+ mHasTriedToDispatchClipboardEvent =
+ mParentData->mHasTriedToDispatchClipboardEvent;
+ }
+ mTopLevelEditSubAction = mParentData->mTopLevelEditSubAction;
+
+ // Parent's mTopLevelEditSubActionData should be referred instead so that
+ // we don't need to set mTopLevelEditSubActionData.mSelectedRange nor
+ // mTopLevelEditActionData.mChangedRange here.
+
+ mDirectionOfTopLevelEditSubAction =
+ mParentData->mDirectionOfTopLevelEditSubAction;
+ } else {
+ mSelection = mEditorBase.GetSelection();
+ if (NS_WARN_IF(!mSelection)) {
+ return;
+ }
+
+ MOZ_ASSERT(mSelection->GetType() == SelectionType::eNormal);
+
+ mEditAction = aEditAction;
+ mDirectionOfTopLevelEditSubAction = eNone;
+ if (mEditorBase.IsHTMLEditor()) {
+ mTopLevelEditSubActionData.mSelectedRange =
+ mEditorBase.AsHTMLEditor()
+ ->GetSelectedRangeItemForTopLevelEditSubAction();
+ mTopLevelEditSubActionData.mChangedRange =
+ mEditorBase.AsHTMLEditor()->GetChangedRangeForTopLevelEditSubAction();
+ mTopLevelEditSubActionData.mCachedPendingStyles.emplace();
+ }
+ }
+ mEditorBase.mEditActionData = this;
+}
+
+EditorBase::AutoEditActionDataSetter::~AutoEditActionDataSetter() {
+ MOZ_ASSERT(mHasCanHandleChecked);
+
+ if (!mSelection || NS_WARN_IF(mEditorBase.mEditActionData != this)) {
+ return;
+ }
+ mEditorBase.mEditActionData = mParentData;
+
+ MOZ_ASSERT(
+ !mTopLevelEditSubActionData.mSelectedRange ||
+ (!mTopLevelEditSubActionData.mSelectedRange->mStartContainer &&
+ !mTopLevelEditSubActionData.mSelectedRange->mEndContainer),
+ "mTopLevelEditSubActionData.mSelectedRange should've been cleared");
+}
+
+void EditorBase::AutoEditActionDataSetter::UpdateSelectionCache(
+ Selection& aSelection) {
+ MOZ_ASSERT(aSelection.GetType() == SelectionType::eNormal);
+
+ if (mSelection == &aSelection) {
+ return;
+ }
+
+ AutoEditActionDataSetter& topLevelEditActionData =
+ [&]() -> AutoEditActionDataSetter& {
+ for (AutoEditActionDataSetter* editActionData = this;;
+ editActionData = editActionData->mParentData) {
+ if (!editActionData->mParentData) {
+ return *editActionData;
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE("You do something wrong");
+ }();
+
+ // Keep grabbing the old selection in the top level edit action data until the
+ // all owners end handling it.
+ if (mSelection) {
+ topLevelEditActionData.mRetiredSelections.AppendElement(*mSelection);
+ }
+
+ // If the old selection is in batch, we should end the batch which
+ // `EditorBase::BeginUpdateViewBatch` started.
+ if (mEditorBase.mUpdateCount && mSelection) {
+ mSelection->EndBatchChanges(__FUNCTION__);
+ }
+
+ Selection* previousSelection = mSelection;
+ mSelection = &aSelection;
+ for (AutoEditActionDataSetter* parentActionData = mParentData;
+ parentActionData; parentActionData = parentActionData->mParentData) {
+ if (!parentActionData->mSelection) {
+ continue;
+ }
+ // Skip scanning mRetiredSelections if we've already handled the selection
+ // previous time.
+ if (parentActionData->mSelection != previousSelection) {
+ if (!topLevelEditActionData.mRetiredSelections.Contains(
+ OwningNonNull<Selection>(*parentActionData->mSelection))) {
+ topLevelEditActionData.mRetiredSelections.AppendElement(
+ *parentActionData->mSelection);
+ }
+ previousSelection = parentActionData->mSelection;
+ }
+ parentActionData->mSelection = &aSelection;
+ }
+
+ // Restart the batching in the new selection.
+ if (mEditorBase.mUpdateCount) {
+ aSelection.StartBatchChanges(__FUNCTION__);
+ }
+}
+
+void EditorBase::AutoEditActionDataSetter::SetColorData(
+ const nsAString& aData) {
+ MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(),
+ "It's too late to set data since this may have already dispatched "
+ "a beforeinput event");
+
+ if (aData.IsEmpty()) {
+ // When removing color/background-color, let's use empty string.
+ mData.Truncate();
+ MOZ_ASSERT(!mData.IsVoid());
+ return;
+ }
+
+ DebugOnly<bool> validColorValue = HTMLEditUtils::GetNormalizedCSSColorValue(
+ aData, HTMLEditUtils::ZeroAlphaColor::RGBAValue, mData);
+ MOZ_ASSERT_IF(validColorValue, !mData.IsVoid());
+}
+
+void EditorBase::AutoEditActionDataSetter::InitializeDataTransfer(
+ DataTransfer* aDataTransfer) {
+ MOZ_ASSERT(aDataTransfer);
+ MOZ_ASSERT(aDataTransfer->IsReadOnly());
+ MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(),
+ "It's too late to set dataTransfer since this may have already "
+ "dispatched a beforeinput event");
+
+ mDataTransfer = aDataTransfer;
+}
+
+void EditorBase::AutoEditActionDataSetter::InitializeDataTransfer(
+ nsITransferable* aTransferable) {
+ MOZ_ASSERT(aTransferable);
+ MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(),
+ "It's too late to set dataTransfer since this may have already "
+ "dispatched a beforeinput event");
+
+ Document* document = mEditorBase.GetDocument();
+ nsIGlobalObject* scopeObject =
+ document ? document->GetScopeObject() : nullptr;
+ mDataTransfer = new DataTransfer(scopeObject, eEditorInput, aTransferable);
+}
+
+void EditorBase::AutoEditActionDataSetter::InitializeDataTransfer(
+ const nsAString& aString) {
+ MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(),
+ "It's too late to set dataTransfer since this may have already "
+ "dispatched a beforeinput event");
+ Document* document = mEditorBase.GetDocument();
+ nsIGlobalObject* scopeObject =
+ document ? document->GetScopeObject() : nullptr;
+ mDataTransfer = new DataTransfer(scopeObject, eEditorInput, aString);
+}
+
+void EditorBase::AutoEditActionDataSetter::InitializeDataTransferWithClipboard(
+ SettingDataTransfer aSettingDataTransfer, int32_t aClipboardType) {
+ MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(),
+ "It's too late to set dataTransfer since this may have already "
+ "dispatched a beforeinput event");
+
+ Document* document = mEditorBase.GetDocument();
+ nsIGlobalObject* scopeObject =
+ document ? document->GetScopeObject() : nullptr;
+ // mDataTransfer will be used for eEditorInput event, but we can keep
+ // using ePaste and ePasteNoFormatting here. If we need to use eEditorInput,
+ // we need to create eEditorInputNoFormatting or something...
+ mDataTransfer =
+ new DataTransfer(scopeObject,
+ aSettingDataTransfer == SettingDataTransfer::eWithFormat
+ ? ePaste
+ : ePasteNoFormatting,
+ true /* is external */, aClipboardType);
+}
+
+void EditorBase::AutoEditActionDataSetter::AppendTargetRange(
+ StaticRange& aTargetRange) {
+ mTargetRanges.AppendElement(aTargetRange);
+}
+
+bool EditorBase::AutoEditActionDataSetter::IsBeforeInputEventEnabled() const {
+ // Don't dispatch "beforeinput" event when the editor user makes us stop
+ // dispatching input event.
+ if (mEditorBase.IsSuppressingDispatchingInputEvent()) {
+ return false;
+ }
+ return EditorBase::TreatAsUserInput(mPrincipal);
+}
+
+// static
+bool EditorBase::TreatAsUserInput(nsIPrincipal* aPrincipal) {
+ // If aPrincipal it not nullptr, it means that the caller is handling an edit
+ // action which is requested by JS. If it's not chrome script, we shouldn't
+ // dispatch "beforeinput" event.
+ if (aPrincipal && !aPrincipal->IsSystemPrincipal()) {
+ // But if it's content script of an addon, `execCommand` calls are a
+ // part of browser's default action from point of view of web apps.
+ // Therefore, we should dispatch `beforeinput` event.
+ // https://github.com/w3c/input-events/issues/91
+ if (!aPrincipal->GetIsAddonOrExpandedAddonPrincipal()) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+nsresult EditorBase::AutoEditActionDataSetter::MaybeFlushPendingNotifications()
+ const {
+ MOZ_ASSERT(CanHandle());
+ if (!MayEditActionRequireLayout(mRawEditAction)) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ OwningNonNull<EditorBase> editorBase = mEditorBase;
+ RefPtr<PresShell> presShell = editorBase->GetPresShell();
+ if (MOZ_UNLIKELY(NS_WARN_IF(!presShell))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ if (MOZ_UNLIKELY(NS_WARN_IF(editorBase->Destroyed()))) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ return NS_OK;
+}
+
+nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent(
+ nsIEditor::EDirection aDeleteDirectionAndAmount /* = nsIEditor::eNone */) {
+ MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(),
+ "We've already handled beforeinput event");
+ MOZ_ASSERT(CanHandle());
+ MOZ_ASSERT_IF(IsBeforeInputEventEnabled(),
+ ShouldAlreadyHaveHandledBeforeInputEventDispatching());
+ MOZ_ASSERT_IF(!MayEditActionDeleteAroundCollapsedSelection(mEditAction),
+ aDeleteDirectionAndAmount == nsIEditor::eNone);
+
+ mHasTriedToDispatchBeforeInputEvent = true;
+
+ if (!IsBeforeInputEventEnabled()) {
+ return NS_OK;
+ }
+
+ // If we're called from OnCompositionEnd(), we shouldn't dispatch
+ // "beforeinput" event since the preceding OnCompositionChange() call has
+ // already dispatched "beforeinput" event for this.
+ if (mEditAction == EditAction::eCommitComposition ||
+ mEditAction == EditAction::eCancelComposition) {
+ return NS_OK;
+ }
+
+ RefPtr<Element> targetElement = mEditorBase.GetInputEventTargetElement();
+ if (!targetElement) {
+ // If selection is not in editable element and it is outside of any
+ // editing hosts, there may be no target element to dispatch `beforeinput`
+ // event. In this case, the caller shouldn't keep handling the edit
+ // action since web apps cannot override it with `beforeinput` event
+ // listener, but for backward compatibility, we should return a special
+ // success code instead of error.
+ return NS_OK;
+ }
+ OwningNonNull<EditorBase> editorBase = mEditorBase;
+ EditorInputType inputType = ToInputType(mEditAction);
+ if (editorBase->IsHTMLEditor() && mTargetRanges.IsEmpty()) {
+ // If the edit action will delete selected ranges, compute the range
+ // strictly.
+ if (MayEditActionDeleteAroundCollapsedSelection(mEditAction) ||
+ (!editorBase->SelectionRef().IsCollapsed() &&
+ MayEditActionDeleteSelection(mEditAction))) {
+ if (!editorBase
+ ->FlushPendingNotificationsIfToHandleDeletionWithFrameSelection(
+ aDeleteDirectionAndAmount)) {
+ NS_WARNING(
+ "Flusing pending notifications caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+
+ AutoRangeArray rangesToDelete(editorBase->SelectionRef());
+ if (!rangesToDelete.Ranges().IsEmpty()) {
+ nsresult rv = MOZ_KnownLive(editorBase->AsHTMLEditor())
+ ->ComputeTargetRanges(aDeleteDirectionAndAmount,
+ rangesToDelete);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::ComputeTargetRanges() destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (rv == NS_ERROR_EDITOR_NO_EDITABLE_RANGE) {
+ // For now, keep dispatching `beforeinput` event even if no selection
+ // range can be editable.
+ rv = NS_OK;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::ComputeTargetRanges() failed, but ignored");
+ for (auto& range : rangesToDelete.Ranges()) {
+ RefPtr<StaticRange> staticRange =
+ StaticRange::Create(range, IgnoreErrors());
+ if (NS_WARN_IF(!staticRange)) {
+ continue;
+ }
+ AppendTargetRange(*staticRange);
+ }
+ }
+ }
+ // Otherwise, just set target ranges to selection ranges.
+ else if (MayHaveTargetRangesOnHTMLEditor(inputType)) {
+ if (uint32_t rangeCount = editorBase->SelectionRef().RangeCount()) {
+ mTargetRanges.SetCapacity(rangeCount);
+ for (const uint32_t i : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(editorBase->SelectionRef().RangeCount() == rangeCount);
+ const nsRange* range = editorBase->SelectionRef().GetRangeAt(i);
+ MOZ_ASSERT(range);
+ MOZ_ASSERT(range->IsPositioned());
+ if (MOZ_UNLIKELY(NS_WARN_IF(!range)) ||
+ MOZ_UNLIKELY(NS_WARN_IF(!range->IsPositioned()))) {
+ continue;
+ }
+ // Now, we need to fix the offset of target range because it may
+ // be referred after modifying the DOM tree and range boundaries
+ // of `range` may have not computed offset yet.
+ RefPtr<StaticRange> targetRange = StaticRange::Create(
+ range->GetStartContainer(), range->StartOffset(),
+ range->GetEndContainer(), range->EndOffset(), IgnoreErrors());
+ if (NS_WARN_IF(!targetRange) ||
+ NS_WARN_IF(!targetRange->IsPositioned())) {
+ continue;
+ }
+ mTargetRanges.AppendElement(std::move(targetRange));
+ }
+ }
+ }
+ }
+ nsEventStatus status = nsEventStatus_eIgnore;
+ InputEventOptions::NeverCancelable neverCancelable =
+ mMakeBeforeInputEventNonCancelable
+ ? InputEventOptions::NeverCancelable::Yes
+ : InputEventOptions::NeverCancelable::No;
+ nsresult rv = nsContentUtils::DispatchInputEvent(
+ targetElement, eEditorBeforeInput, inputType, editorBase,
+ mDataTransfer
+ ? InputEventOptions(mDataTransfer, std::move(mTargetRanges),
+ neverCancelable)
+ : InputEventOptions(mData, std::move(mTargetRanges), neverCancelable),
+ &status);
+ if (NS_WARN_IF(mEditorBase.Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsContentUtils::DispatchInputEvent() failed");
+ return rv;
+ }
+ mBeforeInputEventCanceled = status == nsEventStatus_eConsumeNoDefault;
+ if (mBeforeInputEventCanceled && mEditorBase.IsHTMLEditor()) {
+ mEditorBase.AsHTMLEditor()->mHasBeforeInputBeenCanceled = true;
+ }
+ return mBeforeInputEventCanceled ? NS_ERROR_EDITOR_ACTION_CANCELED : NS_OK;
+}
+
+/*****************************************************************************
+ * mozilla::EditorBase::TopLevelEditSubActionData
+ *****************************************************************************/
+
+nsresult EditorBase::TopLevelEditSubActionData::AddNodeToChangedRange(
+ const HTMLEditor& aHTMLEditor, nsINode& aNode) {
+ EditorRawDOMPoint startPoint(&aNode);
+ EditorRawDOMPoint endPoint(&aNode);
+ DebugOnly<bool> advanced = endPoint.AdvanceOffset();
+ NS_WARNING_ASSERTION(advanced, "Failed to set endPoint to next to aNode");
+ nsresult rv = AddRangeToChangedRange(aHTMLEditor, startPoint, endPoint);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "TopLevelEditSubActionData::AddRangeToChangedRange() failed");
+ return rv;
+}
+
+nsresult EditorBase::TopLevelEditSubActionData::AddPointToChangedRange(
+ const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aPoint) {
+ nsresult rv = AddRangeToChangedRange(aHTMLEditor, aPoint, aPoint);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "TopLevelEditSubActionData::AddRangeToChangedRange() failed");
+ return rv;
+}
+
+nsresult EditorBase::TopLevelEditSubActionData::AddRangeToChangedRange(
+ const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aStart,
+ const EditorRawDOMPoint& aEnd) {
+ if (NS_WARN_IF(!aStart.IsSet()) || NS_WARN_IF(!aEnd.IsSet())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aHTMLEditor.IsDescendantOfRoot(aStart.GetContainer()) ||
+ (aStart.GetContainer() != aEnd.GetContainer() &&
+ !aHTMLEditor.IsDescendantOfRoot(aEnd.GetContainer()))) {
+ return NS_OK;
+ }
+
+ // If mChangedRange hasn't been set, we can just set it to `aStart` and
+ // `aEnd`.
+ if (!mChangedRange->IsPositioned()) {
+ nsresult rv = mChangedRange->SetStartAndEnd(aStart.ToRawRangeBoundary(),
+ aEnd.ToRawRangeBoundary());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed");
+ return rv;
+ }
+
+ Maybe<int32_t> relation =
+ mChangedRange->StartRef().IsSet()
+ ? nsContentUtils::ComparePoints(mChangedRange->StartRef(),
+ aStart.ToRawRangeBoundary())
+ : Some(1);
+ if (NS_WARN_IF(!relation)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If aStart is before start of mChangedRange, reset the start.
+ if (*relation > 0) {
+ ErrorResult error;
+ mChangedRange->SetStart(aStart.ToRawRangeBoundary(), error);
+ if (error.Failed()) {
+ NS_WARNING("nsRange::SetStart() failed");
+ return error.StealNSResult();
+ }
+ }
+
+ relation = mChangedRange->EndRef().IsSet()
+ ? nsContentUtils::ComparePoints(mChangedRange->EndRef(),
+ aEnd.ToRawRangeBoundary())
+ : Some(1);
+ if (NS_WARN_IF(!relation)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If aEnd is after end of mChangedRange, reset the end.
+ if (*relation < 0) {
+ ErrorResult error;
+ mChangedRange->SetEnd(aEnd.ToRawRangeBoundary(), error);
+ if (error.Failed()) {
+ NS_WARNING("nsRange::SetEnd() failed");
+ return error.StealNSResult();
+ }
+ }
+
+ return NS_OK;
+}
+
+void EditorBase::TopLevelEditSubActionData::DidCreateElement(
+ EditorBase& aEditorBase, Element& aNewElement) {
+ MOZ_ASSERT(aEditorBase.AsHTMLEditor());
+
+ if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) {
+ return; // We have not been initialized yet or already been destroyed.
+ }
+
+ if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) {
+ return; // Temporarily disabled by edit sub-action handler.
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ AddNodeToChangedRange(*aEditorBase.AsHTMLEditor(), aNewElement);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "TopLevelEditSubActionData::AddNodeToChangedRange() failed, but ignored");
+}
+
+void EditorBase::TopLevelEditSubActionData::DidInsertContent(
+ EditorBase& aEditorBase, nsIContent& aNewContent) {
+ MOZ_ASSERT(aEditorBase.AsHTMLEditor());
+
+ if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) {
+ return; // We have not been initialized yet or already been destroyed.
+ }
+
+ if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) {
+ return; // Temporarily disabled by edit sub-action handler.
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ AddNodeToChangedRange(*aEditorBase.AsHTMLEditor(), aNewContent);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "TopLevelEditSubActionData::AddNodeToChangedRange() failed, but ignored");
+}
+
+void EditorBase::TopLevelEditSubActionData::WillDeleteContent(
+ EditorBase& aEditorBase, nsIContent& aRemovingContent) {
+ MOZ_ASSERT(aEditorBase.AsHTMLEditor());
+
+ if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) {
+ return; // We have not been initialized yet or already been destroyed.
+ }
+
+ if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) {
+ return; // Temporarily disabled by edit sub-action handler.
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ AddNodeToChangedRange(*aEditorBase.AsHTMLEditor(), aRemovingContent);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "TopLevelEditSubActionData::AddNodeToChangedRange() failed, but ignored");
+}
+
+void EditorBase::TopLevelEditSubActionData::DidSplitContent(
+ EditorBase& aEditorBase, nsIContent& aSplitContent,
+ nsIContent& aNewContent) {
+ MOZ_ASSERT(aEditorBase.AsHTMLEditor());
+
+ if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) {
+ return; // We have not been initialized yet or already been destroyed.
+ }
+
+ if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) {
+ return; // Temporarily disabled by edit sub-action handler.
+ }
+
+ DebugOnly<nsresult> rvIgnored = AddRangeToChangedRange(
+ *aEditorBase.AsHTMLEditor(), EditorRawDOMPoint::AtEndOf(aSplitContent),
+ EditorRawDOMPoint::AtEndOf(aNewContent));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TopLevelEditSubActionData::AddRangeToChangedRange() "
+ "failed, but ignored");
+}
+
+void EditorBase::TopLevelEditSubActionData::DidJoinContents(
+ EditorBase& aEditorBase, const EditorRawDOMPoint& aJoinedPoint) {
+ MOZ_ASSERT(aEditorBase.AsHTMLEditor());
+
+ if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) {
+ return; // We have not been initialized yet or already been destroyed.
+ }
+
+ if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) {
+ return; // Temporarily disabled by edit sub-action handler.
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ AddPointToChangedRange(*aEditorBase.AsHTMLEditor(), aJoinedPoint);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TopLevelEditSubActionData::AddPointToChangedRange() "
+ "failed, but ignored");
+}
+
+void EditorBase::TopLevelEditSubActionData::DidInsertText(
+ EditorBase& aEditorBase, const EditorRawDOMPoint& aInsertionBegin,
+ const EditorRawDOMPoint& aInsertionEnd) {
+ MOZ_ASSERT(aEditorBase.AsHTMLEditor());
+
+ if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) {
+ return; // We have not been initialized yet or already been destroyed.
+ }
+
+ if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) {
+ return; // Temporarily disabled by edit sub-action handler.
+ }
+
+ DebugOnly<nsresult> rvIgnored = AddRangeToChangedRange(
+ *aEditorBase.AsHTMLEditor(), aInsertionBegin, aInsertionEnd);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TopLevelEditSubActionData::AddRangeToChangedRange() "
+ "failed, but ignored");
+}
+
+void EditorBase::TopLevelEditSubActionData::DidDeleteText(
+ EditorBase& aEditorBase, const EditorRawDOMPoint& aStartInTextNode) {
+ MOZ_ASSERT(aEditorBase.AsHTMLEditor());
+
+ if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) {
+ return; // We have not been initialized yet or already been destroyed.
+ }
+
+ if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) {
+ return; // Temporarily disabled by edit sub-action handler.
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ AddPointToChangedRange(*aEditorBase.AsHTMLEditor(), aStartInTextNode);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TopLevelEditSubActionData::AddPointToChangedRange() "
+ "failed, but ignored");
+}
+
+void EditorBase::TopLevelEditSubActionData::WillDeleteRange(
+ EditorBase& aEditorBase, const EditorRawDOMPoint& aStart,
+ const EditorRawDOMPoint& aEnd) {
+ MOZ_ASSERT(aEditorBase.AsHTMLEditor());
+ MOZ_ASSERT(aStart.IsSet());
+ MOZ_ASSERT(aEnd.IsSet());
+
+ if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) {
+ return; // We have not been initialized yet or already been destroyed.
+ }
+
+ if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) {
+ return; // Temporarily disabled by edit sub-action handler.
+ }
+
+ // XXX Looks like that this is wrong. We delete multiple selection ranges
+ // once, but this adds only first range into the changed range.
+ // Anyway, we should take the range as an argument.
+ DebugOnly<nsresult> rvIgnored =
+ AddRangeToChangedRange(*aEditorBase.AsHTMLEditor(), aStart, aEnd);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TopLevelEditSubActionData::AddRangeToChangedRange() "
+ "failed, but ignored");
+}
+
+nsPIDOMWindowOuter* EditorBase::GetWindow() const {
+ return mDocument ? mDocument->GetWindow() : nullptr;
+}
+
+nsPIDOMWindowInner* EditorBase::GetInnerWindow() const {
+ return mDocument ? mDocument->GetInnerWindow() : nullptr;
+}
+
+PresShell* EditorBase::GetPresShell() const {
+ return mDocument ? mDocument->GetPresShell() : nullptr;
+}
+
+nsPresContext* EditorBase::GetPresContext() const {
+ PresShell* presShell = GetPresShell();
+ return presShell ? presShell->GetPresContext() : nullptr;
+}
+
+already_AddRefed<nsCaret> EditorBase::GetCaret() const {
+ PresShell* presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return nullptr;
+ }
+ return presShell->GetCaret();
+}
+
+nsISelectionController* EditorBase::GetSelectionController() const {
+ if (mSelectionController) {
+ return mSelectionController;
+ }
+ if (!mDocument) {
+ return nullptr;
+ }
+ return mDocument->GetPresShell();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorBase.h b/editor/libeditor/EditorBase.h
new file mode 100644
index 0000000000..952b8aa3f6
--- /dev/null
+++ b/editor/libeditor/EditorBase.h
@@ -0,0 +1,2945 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditorBase_h
+#define mozilla_EditorBase_h
+
+#include "mozilla/intl/BidiEmbeddingLevel.h"
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
+#include "mozilla/EditAction.h" // for EditAction and EditSubAction
+#include "mozilla/EditorDOMPoint.h" // for EditorDOMPoint
+#include "mozilla/EditorForwards.h"
+#include "mozilla/EventForwards.h" // for InputEventTargetRanges
+#include "mozilla/Likely.h" // for MOZ_UNLIKELY, MOZ_LIKELY
+#include "mozilla/Maybe.h" // for Maybe
+#include "mozilla/OwningNonNull.h" // for OwningNonNull
+#include "mozilla/PendingStyles.h" // for PendingStyle, PendingStyleCache
+#include "mozilla/RangeBoundary.h" // for RawRangeBoundary, RangeBoundary
+#include "mozilla/SelectionState.h" // for RangeUpdater, etc.
+#include "mozilla/StyleSheet.h" // for StyleSheet
+#include "mozilla/TransactionManager.h" // for TransactionManager
+#include "mozilla/WeakPtr.h" // for WeakPtr
+#include "mozilla/dom/DataTransfer.h" // for dom::DataTransfer
+#include "mozilla/dom/HTMLBRElement.h" // for dom::HTMLBRElement
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Text.h"
+#include "nsAtom.h" // for nsAtom, nsStaticAtom
+#include "nsCOMPtr.h" // for already_AddRefed, nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsGkAtoms.h"
+#include "nsIContentInlines.h" // for nsINode::IsEditable()
+#include "nsIEditor.h" // for nsIEditor, etc.
+#include "nsISelectionController.h" // for nsISelectionController constants
+#include "nsISelectionListener.h" // for nsISelectionListener
+#include "nsISupportsImpl.h" // for EditorBase::Release, etc.
+#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsPIDOMWindow.h" // for nsPIDOMWindowInner, etc.
+#include "nsString.h" // for nsCString
+#include "nsTArray.h" // for nsTArray and nsAutoTArray
+#include "nsWeakReference.h" // for nsSupportsWeakReference
+#include "nscore.h" // for nsresult, nsAString, etc.
+
+#include <tuple> // for std::tuple
+
+class mozInlineSpellChecker;
+class nsAtom;
+class nsCaret;
+class nsIContent;
+class nsIDocumentEncoder;
+class nsIDocumentStateListener;
+class nsIEditActionListener;
+class nsINode;
+class nsIPrincipal;
+class nsISupports;
+class nsITransferable;
+class nsITransaction;
+class nsIWidget;
+class nsRange;
+
+namespace mozilla {
+class AlignStateAtSelection;
+class AutoTransactionsConserveSelection;
+class AutoUpdateViewBatch;
+class ErrorResult;
+class IMEContentObserver;
+class ListElementSelectionState;
+class ListItemElementSelectionState;
+class ParagraphStateAtSelection;
+class PresShell;
+class TextComposition;
+class TextInputListener;
+class TextServicesDocument;
+namespace dom {
+class AbstractRange;
+class DataTransfer;
+class Document;
+class DragEvent;
+class Element;
+class EventTarget;
+class HTMLBRElement;
+} // namespace dom
+
+namespace widget {
+struct IMEState;
+} // namespace widget
+
+/**
+ * Implementation of an editor object. it will be the controller/focal point
+ * for the main editor services. i.e. the GUIManager, publishing, transaction
+ * manager, event interfaces. the idea for the event interfaces is to have them
+ * delegate the actual commands to the editor independent of the XPFE
+ * implementation.
+ */
+class EditorBase : public nsIEditor,
+ public nsISelectionListener,
+ public nsSupportsWeakReference {
+ public:
+ /****************************************************************************
+ * NOTE: DO NOT MAKE YOUR NEW METHODS PUBLIC IF they are called by other
+ * classes under libeditor except EditorEventListener and
+ * HTMLEditorEventListener because each public method which may fire
+ * eEditorInput event will need to instantiate new stack class for
+ * managing input type value of eEditorInput and cache some objects
+ * for smarter handling. In other words, when you add new root
+ * method to edit the DOM tree, you can make your new method public.
+ ****************************************************************************/
+
+ using Document = dom::Document;
+ using Element = dom::Element;
+ using InterlinePosition = dom::Selection::InterlinePosition;
+ using Selection = dom::Selection;
+ using Text = dom::Text;
+
+ enum class EditorType { Text, HTML };
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(EditorBase, nsIEditor)
+
+ // nsIEditor methods
+ NS_DECL_NSIEDITOR
+
+ // nsISelectionListener method
+ NS_DECL_NSISELECTIONLISTENER
+
+ /**
+ * The default constructor. This should suffice. the setting of the
+ * interfaces is done after the construction of the editor class.
+ */
+ explicit EditorBase(EditorType aEditorType);
+
+ [[nodiscard]] bool IsInitialized() const {
+ return mDocument && mDidPostCreate;
+ }
+ [[nodiscard]] bool IsBeingInitialized() const {
+ return mDocument && !mDidPostCreate;
+ }
+ [[nodiscard]] bool Destroyed() const { return mDidPreDestroy; }
+
+ Document* GetDocument() const { return mDocument; }
+ nsPIDOMWindowOuter* GetWindow() const;
+ nsPIDOMWindowInner* GetInnerWindow() const;
+
+ /**
+ * MayHaveMutationEventListeners() returns true when the window may have
+ * mutation event listeners.
+ *
+ * @param aMutationEventType One or multiple of NS_EVENT_BITS_MUTATION_*.
+ * @return true if the editor is an HTMLEditor instance,
+ * and at least one of NS_EVENT_BITS_MUTATION_* is
+ * set to the window or in debug build.
+ */
+ bool MayHaveMutationEventListeners(
+ uint32_t aMutationEventType = 0xFFFFFFFF) const {
+ if (IsTextEditor()) {
+ // DOM mutation event listeners cannot catch the changes of
+ // <input type="text"> nor <textarea>.
+ return false;
+ }
+#ifdef DEBUG
+ // On debug build, this should always return true for testing complicated
+ // path without mutation event listeners because when mutation event
+ // listeners do not touch the DOM, editor needs to run as there is no
+ // mutation event listeners.
+ return true;
+#else // #ifdef DEBUG
+ nsPIDOMWindowInner* window = GetInnerWindow();
+ return window ? window->HasMutationListeners(aMutationEventType) : false;
+#endif // #ifdef DEBUG #else
+ }
+
+ /**
+ * MayHaveBeforeInputEventListenersForTelemetry() returns true when the
+ * window may have or have had one or more `beforeinput` event listeners.
+ * Note that this may return false even if there is a `beforeinput`.
+ * See nsPIDOMWindowInner::HasBeforeInputEventListenersForTelemetry()'s
+ * comment for the detail.
+ */
+ bool MayHaveBeforeInputEventListenersForTelemetry() const {
+ if (const nsPIDOMWindowInner* window = GetInnerWindow()) {
+ return window->HasBeforeInputEventListenersForTelemetry();
+ }
+ return false;
+ }
+
+ /**
+ * MutationObserverHasObservedNodeForTelemetry() returns true when a node in
+ * the window may have been observed by the web apps with a mutation observer
+ * (i.e., `MutationObserver.observe()` called by chrome script and addon's
+ * script does not make this returns true).
+ * Note that this may return false even if there is a node observed by
+ * a MutationObserver. See
+ * nsPIDOMWindowInner::MutationObserverHasObservedNodeForTelemetry()'s comment
+ * for the detail.
+ */
+ bool MutationObserverHasObservedNodeForTelemetry() const {
+ if (const nsPIDOMWindowInner* window = GetInnerWindow()) {
+ return window->MutationObserverHasObservedNodeForTelemetry();
+ }
+ return false;
+ }
+
+ /**
+ * This checks whether the call with aPrincipal should or should not be
+ * treated as user input.
+ */
+ [[nodiscard]] static bool TreatAsUserInput(nsIPrincipal* aPrincipal);
+
+ PresShell* GetPresShell() const;
+ nsPresContext* GetPresContext() const;
+ already_AddRefed<nsCaret> GetCaret() const;
+
+ already_AddRefed<nsIWidget> GetWidget() const;
+
+ nsISelectionController* GetSelectionController() const;
+
+ nsresult GetSelection(SelectionType aSelectionType,
+ Selection** aSelection) const;
+
+ Selection* GetSelection(
+ SelectionType aSelectionType = SelectionType::eNormal) const {
+ if (aSelectionType == SelectionType::eNormal &&
+ IsEditActionDataAvailable()) {
+ return &SelectionRef();
+ }
+ nsISelectionController* sc = GetSelectionController();
+ if (!sc) {
+ return nullptr;
+ }
+ Selection* selection = sc->GetSelection(ToRawSelectionType(aSelectionType));
+ return selection;
+ }
+
+ /**
+ * @return Ancestor limiter of normal selection
+ */
+ [[nodiscard]] nsIContent* GetSelectionAncestorLimiter() const {
+ Selection* selection = GetSelection(SelectionType::eNormal);
+ return selection ? selection->GetAncestorLimiter() : nullptr;
+ }
+
+ /**
+ * Fast non-refcounting editor root element accessor
+ */
+ Element* GetRoot() const { return mRootElement; }
+
+ /**
+ * Likewise, but gets the text control element instead of the root for
+ * plaintext editors.
+ */
+ Element* GetExposedRoot() const;
+
+ /**
+ * Set or unset TextInputListener. If setting non-nullptr when the editor
+ * already has a TextInputListener, this will crash in debug build.
+ */
+ void SetTextInputListener(TextInputListener* aTextInputListener);
+
+ /**
+ * Set or unset IMEContentObserver. If setting non-nullptr when the editor
+ * already has an IMEContentObserver, this will crash in debug build.
+ */
+ void SetIMEContentObserver(IMEContentObserver* aIMEContentObserver);
+
+ /**
+ * Returns current composition.
+ */
+ TextComposition* GetComposition() const;
+
+ /**
+ * Get preferred IME status of current widget.
+ */
+ virtual nsresult GetPreferredIMEState(widget::IMEState* aState);
+
+ /**
+ * Returns true if there is composition string and not fixed.
+ */
+ bool IsIMEComposing() const;
+
+ /**
+ * Commit composition if there is.
+ * Note that when there is a composition, this requests to commit composition
+ * to native IME. Therefore, when there is composition, this can do anything.
+ * For example, the editor instance, the widget or the process itself may
+ * be destroyed.
+ */
+ nsresult CommitComposition();
+
+ /**
+ * ToggleTextDirection() toggles text-direction of the root element.
+ *
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ ToggleTextDirectionAsAction(nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * SwitchTextDirectionTo() sets the text-direction of the root element to
+ * LTR or RTL.
+ */
+ enum class TextDirection {
+ eLTR,
+ eRTL,
+ };
+ MOZ_CAN_RUN_SCRIPT void SwitchTextDirectionTo(TextDirection aTextDirection);
+
+ /**
+ * Finalizes selection and caret for the editor.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult FinalizeSelection();
+
+ /**
+ * Returns true if selection is in an editable element and both the range
+ * start and the range end are editable. E.g., even if the selection range
+ * includes non-editable elements, returns true when one of common ancestors
+ * of the range start and the range end is editable. Otherwise, false.
+ */
+ bool IsSelectionEditable();
+
+ /**
+ * Returns number of undo or redo items.
+ */
+ size_t NumberOfUndoItems() const {
+ return mTransactionManager ? mTransactionManager->NumberOfUndoItems() : 0;
+ }
+ size_t NumberOfRedoItems() const {
+ return mTransactionManager ? mTransactionManager->NumberOfRedoItems() : 0;
+ }
+
+ /**
+ * Returns number of maximum undo/redo transactions.
+ */
+ int32_t NumberOfMaximumTransactions() const {
+ return mTransactionManager
+ ? mTransactionManager->NumberOfMaximumTransactions()
+ : 0;
+ }
+
+ /**
+ * Returns true if this editor can store transactions for undo/redo.
+ */
+ bool IsUndoRedoEnabled() const {
+ return mTransactionManager &&
+ mTransactionManager->NumberOfMaximumTransactions();
+ }
+
+ /**
+ * Return true if it's possible to undo/redo right now.
+ */
+ bool CanUndo() const {
+ return IsUndoRedoEnabled() && NumberOfUndoItems() > 0;
+ }
+ bool CanRedo() const {
+ return IsUndoRedoEnabled() && NumberOfRedoItems() > 0;
+ }
+
+ /**
+ * Enables or disables undo/redo feature. Returns true if it succeeded,
+ * otherwise, e.g., we're undoing or redoing, returns false.
+ */
+ bool EnableUndoRedo(int32_t aMaxTransactionCount = -1) {
+ if (!mTransactionManager) {
+ mTransactionManager = new TransactionManager();
+ }
+ return mTransactionManager->EnableUndoRedo(aMaxTransactionCount);
+ }
+ bool DisableUndoRedo() {
+ if (!mTransactionManager) {
+ return true;
+ }
+ return mTransactionManager->DisableUndoRedo();
+ }
+ bool ClearUndoRedo() {
+ if (!mTransactionManager) {
+ return true;
+ }
+ return mTransactionManager->ClearUndoRedo();
+ }
+
+ /**
+ * See Document::AreClipboardCommandsUnconditionallyEnabled.
+ */
+ bool AreClipboardCommandsUnconditionallyEnabled() const;
+
+ /**
+ * IsCutCommandEnabled() returns whether cut command can be enabled or
+ * disabled. This always returns true if we're in non-chrome HTML/XHTML
+ * document. Otherwise, same as the result of `IsCopyToClipboardAllowed()`.
+ */
+ MOZ_CAN_RUN_SCRIPT bool IsCutCommandEnabled() const;
+
+ /**
+ * IsCopyCommandEnabled() returns copy command can be enabled or disabled.
+ * This always returns true if we're in non-chrome HTML/XHTML document.
+ * Otherwise, same as the result of `IsCopyToClipboardAllowed()`.
+ */
+ MOZ_CAN_RUN_SCRIPT bool IsCopyCommandEnabled() const;
+
+ /**
+ * IsCopyToClipboardAllowed() returns true if the selected content can
+ * be copied into the clipboard. This returns true when:
+ * - `Selection` is not collapsed and we're not a password editor.
+ * - `Selection` is not collapsed and we're a password editor but selection
+ * range is in unmasked range.
+ */
+ bool IsCopyToClipboardAllowed() const {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return false;
+ }
+ return IsCopyToClipboardAllowedInternal();
+ }
+
+ /**
+ * HandleDropEvent() is called from EditorEventListener::Drop that is handler
+ * of drop event.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult HandleDropEvent(dom::DragEvent* aDropEvent);
+
+ MOZ_CAN_RUN_SCRIPT virtual nsresult HandleKeyPressEvent(
+ WidgetKeyboardEvent* aKeyboardEvent);
+
+ virtual dom::EventTarget* GetDOMEventTarget() const = 0;
+
+ /**
+ * OnCompositionStart() is called when editor receives eCompositionStart
+ * event which should be handled in this editor.
+ */
+ nsresult OnCompositionStart(WidgetCompositionEvent& aCompositionStartEvent);
+
+ /**
+ * OnCompositionChange() is called when editor receives an eCompositioChange
+ * event which should be handled in this editor.
+ *
+ * @param aCompositionChangeEvent eCompositionChange event which should
+ * be handled in this editor.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ OnCompositionChange(WidgetCompositionEvent& aCompositionChangeEvent);
+
+ /**
+ * OnCompositionEnd() is called when editor receives an eCompositionChange
+ * event and it's followed by eCompositionEnd event and after
+ * OnCompositionChange() is called.
+ */
+ MOZ_CAN_RUN_SCRIPT void OnCompositionEnd(
+ WidgetCompositionEvent& aCompositionEndEvent);
+
+ /**
+ * Accessor methods to flags.
+ */
+ uint32_t Flags() const { return mFlags; }
+
+ MOZ_CAN_RUN_SCRIPT nsresult AddFlags(uint32_t aFlags) {
+ const uint32_t kOldFlags = Flags();
+ const uint32_t kNewFlags = (kOldFlags | aFlags);
+ if (kNewFlags == kOldFlags) {
+ return NS_OK;
+ }
+ return SetFlags(kNewFlags); // virtual call and may be expensive.
+ }
+ MOZ_CAN_RUN_SCRIPT nsresult RemoveFlags(uint32_t aFlags) {
+ const uint32_t kOldFlags = Flags();
+ const uint32_t kNewFlags = (kOldFlags & ~aFlags);
+ if (kNewFlags == kOldFlags) {
+ return NS_OK;
+ }
+ return SetFlags(kNewFlags); // virtual call and may be expensive.
+ }
+ MOZ_CAN_RUN_SCRIPT nsresult AddAndRemoveFlags(uint32_t aAddingFlags,
+ uint32_t aRemovingFlags) {
+ MOZ_ASSERT(!(aAddingFlags & aRemovingFlags),
+ "Same flags are specified both adding and removing");
+ const uint32_t kOldFlags = Flags();
+ const uint32_t kNewFlags = ((kOldFlags | aAddingFlags) & ~aRemovingFlags);
+ if (kNewFlags == kOldFlags) {
+ return NS_OK;
+ }
+ return SetFlags(kNewFlags); // virtual call and may be expensive.
+ }
+
+ bool IsSingleLineEditor() const {
+ const bool isSingleLineEditor =
+ (mFlags & nsIEditor::eEditorSingleLineMask) != 0;
+ MOZ_ASSERT_IF(isSingleLineEditor, IsTextEditor());
+ return isSingleLineEditor;
+ }
+
+ bool IsPasswordEditor() const {
+ const bool isPasswordEditor =
+ (mFlags & nsIEditor::eEditorPasswordMask) != 0;
+ MOZ_ASSERT_IF(isPasswordEditor, IsTextEditor());
+ return isPasswordEditor;
+ }
+
+ // FYI: Both IsRightToLeft() and IsLeftToRight() may return false if
+ // the editor inherits the content node's direction.
+ bool IsRightToLeft() const {
+ return (mFlags & nsIEditor::eEditorRightToLeft) != 0;
+ }
+ bool IsLeftToRight() const {
+ return (mFlags & nsIEditor::eEditorLeftToRight) != 0;
+ }
+
+ bool IsReadonly() const {
+ return (mFlags & nsIEditor::eEditorReadonlyMask) != 0;
+ }
+
+ bool IsMailEditor() const {
+ return (mFlags & nsIEditor::eEditorMailMask) != 0;
+ }
+
+ bool IsInteractionAllowed() const {
+ const bool isInteractionAllowed =
+ (mFlags & nsIEditor::eEditorAllowInteraction) != 0;
+ MOZ_ASSERT_IF(isInteractionAllowed, IsHTMLEditor());
+ return isInteractionAllowed;
+ }
+
+ bool ShouldSkipSpellCheck() const {
+ return (mFlags & nsIEditor::eEditorSkipSpellCheck) != 0;
+ }
+
+ bool HasIndependentSelection() const {
+ MOZ_ASSERT_IF(mSelectionController, IsTextEditor());
+ return !!mSelectionController;
+ }
+
+ bool IsModifiable() const { return !IsReadonly(); }
+
+ /**
+ * IsInEditSubAction() return true while the instance is handling an edit
+ * sub-action. Otherwise, false.
+ */
+ bool IsInEditSubAction() const { return mIsInEditSubAction; }
+
+ /**
+ * IsEmpty() checks whether the editor is empty. If editor has only padding
+ * <br> element for empty editor, returns true. If editor's root element has
+ * non-empty text nodes or other nodes like <br>, returns false.
+ */
+ virtual bool IsEmpty() const = 0;
+
+ /**
+ * SuppressDispatchingInputEvent() suppresses or unsuppresses dispatching
+ * "input" event.
+ */
+ void SuppressDispatchingInputEvent(bool aSuppress) {
+ mDispatchInputEvent = !aSuppress;
+ }
+
+ /**
+ * IsSuppressingDispatchingInputEvent() returns true if the editor stops
+ * dispatching input event. Otherwise, false.
+ */
+ bool IsSuppressingDispatchingInputEvent() const {
+ return !mDispatchInputEvent;
+ }
+
+ /**
+ * Returns true if markNodeDirty() has any effect. Returns false if
+ * markNodeDirty() is a no-op.
+ */
+ bool OutputsMozDirty() const {
+ // Return true for Composer (!IsInteractionAllowed()) or mail
+ // (IsMailEditor()), but false for webpages.
+ return !IsInteractionAllowed() || IsMailEditor();
+ }
+
+ /**
+ * Get the focused element, if we're focused. Returns null otherwise.
+ */
+ virtual Element* GetFocusedElement() const;
+
+ /**
+ * Whether the aGUIEvent should be handled by this editor or not. When this
+ * returns false, The aGUIEvent shouldn't be handled on this editor,
+ * i.e., The aGUIEvent should be handled by another inner editor or ancestor
+ * elements.
+ */
+ virtual bool IsAcceptableInputEvent(WidgetGUIEvent* aGUIEvent) const;
+
+ /**
+ * FindSelectionRoot() returns a selection root of this editor when aNode
+ * gets focus. aNode must be a content node or a document node. When the
+ * target isn't a part of this editor, returns nullptr. If this is for
+ * designMode, you should set the document node to aNode except that an
+ * element in the document has focus.
+ */
+ [[nodiscard]] virtual Element* FindSelectionRoot(const nsINode& aNode) const;
+
+ /**
+ * OnFocus() is called when we get a focus event.
+ *
+ * @param aOriginalEventTargetNode The original event target node of the
+ * focus event.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult OnFocus(
+ const nsINode& aOriginalEventTargetNode);
+
+ /**
+ * OnBlur() is called when we're blurred.
+ *
+ * @param aEventTarget The event target of the blur event.
+ */
+ virtual nsresult OnBlur(const dom::EventTarget* aEventTarget) = 0;
+
+ /** Resyncs spellchecking state (enabled/disabled). This should be called
+ * when anything that affects spellchecking state changes, such as the
+ * spellcheck attribute value.
+ */
+ void SyncRealTimeSpell();
+
+ /**
+ * Do "cut".
+ *
+ * @param aPrincipal If you know current context is subject
+ * principal or system principal, set it.
+ * When nullptr, this checks it automatically.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult CutAsAction(nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * CanPaste() returns true if user can paste something at current selection.
+ */
+ virtual bool CanPaste(int32_t aClipboardType) const = 0;
+
+ /**
+ * Do "undo" or "redo".
+ *
+ * @param aCount How many count of transactions should be
+ * handled.
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult UndoAsAction(uint32_t aCount,
+ nsIPrincipal* aPrincipal = nullptr);
+ MOZ_CAN_RUN_SCRIPT nsresult RedoAsAction(uint32_t aCount,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * InsertTextAsAction() inserts aStringToInsert at selection.
+ * Although this method is implementation of nsIEditor.insertText(),
+ * this treats the input is an edit action. If you'd like to insert text
+ * as part of edit action, you probably should use InsertTextAsSubAction().
+ *
+ * @param aStringToInsert The string to insert.
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult InsertTextAsAction(
+ const nsAString& aStringToInsert, nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * InsertLineBreakAsAction() is called when user inputs a line break with
+ * Enter or something. If the instance is `HTMLEditor`, this is called
+ * when Shift + Enter or "insertlinebreak" command.
+ *
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult InsertLineBreakAsAction(
+ nsIPrincipal* aPrincipal = nullptr) = 0;
+
+ /**
+ * CanDeleteSelection() returns true if `Selection` is not collapsed and
+ * it's allowed to be removed.
+ */
+ bool CanDeleteSelection() const {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return false;
+ }
+ return IsModifiable() && !SelectionRef().IsCollapsed();
+ }
+
+ /**
+ * DeleteSelectionAsAction() removes selection content or content around
+ * caret with transactions. This should be used for handling it as an
+ * edit action. If you'd like to remove selection for preparing to insert
+ * something, you probably should use DeleteSelectionAsSubAction().
+ *
+ * @param aDirectionAndAmount How much range should be removed.
+ * @param aStripWrappers Whether the parent blocks should be removed
+ * when they become empty.
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteSelectionAsAction(nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ enum class AllowBeforeInputEventCancelable {
+ No,
+ Yes,
+ };
+
+ /**
+ * Replace text in aReplaceRange or all text in this editor with aString and
+ * treat the change as inserting the string.
+ *
+ * @param aString The string to set.
+ * @param aReplaceRange The range to be replaced.
+ * If nullptr, all contents will be replaced.
+ * NOTE: Currently, nullptr is not allowed if
+ * the editor is an HTMLEditor.
+ * @param aAllowBeforeInputEventCancelable
+ * Whether `beforeinput` event which will be
+ * dispatched for this can be cancelable or not.
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult ReplaceTextAsAction(
+ const nsAString& aString, nsRange* aReplaceRange,
+ AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * Can we paste |aTransferable| or, if |aTransferable| is null, will a call
+ * to pasteTransferable later possibly succeed if given an instance of
+ * nsITransferable then? True if the doc is modifiable, and, if
+ * |aTransfeable| is non-null, we have pasteable data in |aTransfeable|.
+ */
+ virtual bool CanPasteTransferable(nsITransferable* aTransferable) = 0;
+
+ /**
+ * PasteAsAction() pastes clipboard content to Selection. This method
+ * may dispatch ePaste event first. If its defaultPrevent() is called,
+ * this does nothing but returns NS_OK.
+ *
+ * @param aClipboardType nsIClipboard::kGlobalClipboard or
+ * nsIClipboard::kSelectionClipboard.
+ * @param aDispatchPasteEvent Yes if this should dispatch ePaste event
+ * before pasting. Otherwise, No.
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ enum class DispatchPasteEvent { No, Yes };
+ MOZ_CAN_RUN_SCRIPT nsresult
+ PasteAsAction(int32_t aClipboardType, DispatchPasteEvent aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * Paste aTransferable at Selection.
+ *
+ * @param aTransferable Must not be nullptr.
+ * @param aDispatchPasteEvent Yes if this should dispatch ePaste event
+ * before pasting. Otherwise, No.
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult PasteTransferableAsAction(
+ nsITransferable* aTransferable, DispatchPasteEvent aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * PasteAsQuotationAsAction() pastes content in clipboard as quotation.
+ * If the editor is TextEditor or in plaintext mode, will paste the content
+ * with appending ">" to start of each line.
+ * if the editor is HTMLEditor and is not in plaintext mode, will patste it
+ * into newly created blockquote element.
+ *
+ * @param aClipboardType nsIClipboard::kGlobalClipboard or
+ * nsIClipboard::kSelectionClipboard.
+ * @param aDispatchPasteEvent Yes if this should dispatch ePaste event
+ * before pasting. Otherwise, No.
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult PasteAsQuotationAsAction(
+ int32_t aClipboardType, DispatchPasteEvent aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ protected: // May be used by friends.
+ class AutoEditActionDataSetter;
+
+ /**
+ * TopLevelEditSubActionData stores temporary data while we're handling
+ * top-level edit sub-action.
+ */
+ struct MOZ_STACK_CLASS TopLevelEditSubActionData final {
+ friend class AutoEditActionDataSetter;
+
+ // Set selected range before edit. Then, RangeUpdater keep modifying
+ // the range while we're changing the DOM tree.
+ RefPtr<RangeItem> mSelectedRange;
+
+ // Computing changed range while we're handling sub actions.
+ RefPtr<nsRange> mChangedRange;
+
+ // XXX In strict speaking, mCachedPendingStyles isn't enough to cache
+ // inline styles because inline style can be specified with "style"
+ // attribute and/or CSS in <style> elements or CSS files. So, we need
+ // to look for better implementation about this.
+ // FYI: Initialization cost of AutoPendingStyleCacheArray is expensive and
+ // it is not used by TextEditor so that we should construct it only
+ // when we're an HTMLEditor.
+ Maybe<AutoPendingStyleCacheArray> mCachedPendingStyles;
+
+ // If we tried to delete selection, set to true.
+ bool mDidDeleteSelection;
+
+ // If we have explicitly set selection inter line, set to true.
+ // `AfterEdit()` or something shouldn't overwrite it in such case.
+ bool mDidExplicitlySetInterLine;
+
+ // If we have deleted non-collapsed range set to true, there are only 2
+ // cases for now:
+ // - non-collapsed range was selected.
+ // - selection was collapsed in a text node and a Unicode character
+ // was removed.
+ bool mDidDeleteNonCollapsedRange;
+
+ // If we have deleted parent empty blocks, set to true.
+ bool mDidDeleteEmptyParentBlocks;
+
+ // If we're a contenteditable editor, we temporarily increase edit count
+ // of the document between `BeforeEdit()` and `AfterEdit()`. I.e., if
+ // we increased the count in `BeforeEdit()`, we need to decrease it in
+ // `AfterEdit()`, however, the document may be changed to designMode or
+ // non-editable. Therefore, we need to store with this whether we need
+ // to restore it.
+ bool mRestoreContentEditableCount;
+
+ // If we explicitly normalized whitespaces around the changed range,
+ // set to true.
+ bool mDidNormalizeWhitespaces;
+
+ // Set to true by default. If somebody inserts an HTML fragment
+ // intentionally, any empty elements shouldn't be cleaned up later. In the
+ // case this is set to false.
+ // TODO: We should not do this by default. If it's necessary, each edit
+ // action handler do it by itself instead. Then, we can avoid such
+ // unnecessary DOM tree scan.
+ bool mNeedsToCleanUpEmptyElements;
+
+ /**
+ * The following methods modifies some data of this struct and
+ * `EditSubActionData` struct. Currently, these are required only
+ * by `HTMLEditor`. Therefore, for cutting the runtime cost of
+ * `TextEditor`, these methods should be called only by `HTMLEditor`.
+ * But it's fine to use these methods in `TextEditor` if necessary.
+ * If so, you need to call `DidDeleteText()` and `DidInsertText()`
+ * from `SetTextNodeWithoutTransaction()`.
+ */
+ void DidCreateElement(EditorBase& aEditorBase, Element& aNewElement);
+ void DidInsertContent(EditorBase& aEditorBase, nsIContent& aNewContent);
+ void WillDeleteContent(EditorBase& aEditorBase,
+ nsIContent& aRemovingContent);
+ void DidSplitContent(EditorBase& aEditorBase, nsIContent& aSplitContent,
+ nsIContent& aNewContent);
+ void DidJoinContents(EditorBase& aEditorBase,
+ const EditorRawDOMPoint& aJoinedPoint);
+ void DidInsertText(EditorBase& aEditorBase,
+ const EditorRawDOMPoint& aInsertionBegin,
+ const EditorRawDOMPoint& aInsertionEnd);
+ void DidDeleteText(EditorBase& aEditorBase,
+ const EditorRawDOMPoint& aStartInTextNode);
+ void WillDeleteRange(EditorBase& aEditorBase,
+ const EditorRawDOMPoint& aStart,
+ const EditorRawDOMPoint& aEnd);
+
+ private:
+ void Clear() {
+ mDidExplicitlySetInterLine = false;
+ // We don't need to clear other members which are referred only when the
+ // editor is an HTML editor anymore. Note that if `mSelectedRange` is
+ // non-nullptr, that means that we're in `HTMLEditor`.
+ if (!mSelectedRange) {
+ return;
+ }
+ mSelectedRange->Clear();
+ mChangedRange->Reset();
+ if (mCachedPendingStyles.isSome()) {
+ mCachedPendingStyles->Clear();
+ }
+ mDidDeleteSelection = false;
+ mDidDeleteNonCollapsedRange = false;
+ mDidDeleteEmptyParentBlocks = false;
+ mRestoreContentEditableCount = false;
+ mDidNormalizeWhitespaces = false;
+ mNeedsToCleanUpEmptyElements = true;
+ }
+
+ /**
+ * Extend mChangedRange to include `aNode`.
+ */
+ nsresult AddNodeToChangedRange(const HTMLEditor& aHTMLEditor,
+ nsINode& aNode);
+
+ /**
+ * Extend mChangedRange to include `aPoint`.
+ */
+ nsresult AddPointToChangedRange(const HTMLEditor& aHTMLEditor,
+ const EditorRawDOMPoint& aPoint);
+
+ /**
+ * Extend mChangedRange to include `aStart` and `aEnd`.
+ */
+ nsresult AddRangeToChangedRange(const HTMLEditor& aHTMLEditor,
+ const EditorRawDOMPoint& aStart,
+ const EditorRawDOMPoint& aEnd);
+
+ TopLevelEditSubActionData() = default;
+ TopLevelEditSubActionData(const TopLevelEditSubActionData& aOther) = delete;
+ };
+
+ struct MOZ_STACK_CLASS EditSubActionData final {
+ // While this is set to false, TopLevelEditSubActionData::mChangedRange
+ // shouldn't be modified since in some cases, modifying it in the setter
+ // itself may be faster. Note that we should affect this only for current
+ // edit sub action since mutation event listener may edit different range.
+ bool mAdjustChangedRangeFromListener;
+
+ private:
+ void Clear() { mAdjustChangedRangeFromListener = true; }
+
+ friend EditorBase;
+ };
+
+ protected: // AutoEditActionDataSetter, this shouldn't be accessed by friends.
+ /**
+ * SettingDataTransfer enum class is used to specify whether DataTransfer
+ * should be initialized with or without format. For example, when user
+ * uses Accel + Shift + V to paste text without format, DataTransfer should
+ * have only plain/text data to make web apps treat it without format.
+ */
+ enum class SettingDataTransfer {
+ eWithFormat,
+ eWithoutFormat,
+ };
+
+ /**
+ * AutoEditActionDataSetter grabs some necessary objects for handling any
+ * edit actions and store the edit action what we're handling. When this is
+ * created, its pointer is set to the mEditActionData, and this guarantees
+ * the lifetime of grabbing objects until it's destroyed.
+ */
+ class MOZ_STACK_CLASS AutoEditActionDataSetter final {
+ public:
+ // NOTE: aPrincipal will be used when we implement "beforeinput" event.
+ // It's set only when maybe we shouldn't dispatch it because of
+ // called by JS. I.e., if this is nullptr, we can always dispatch
+ // it.
+ AutoEditActionDataSetter(const EditorBase& aEditorBase,
+ EditAction aEditAction,
+ nsIPrincipal* aPrincipal = nullptr);
+ ~AutoEditActionDataSetter();
+
+ void SetSelectionCreatedByDoubleclick(bool aSelectionCreatedByDoubleclick) {
+ mSelectionCreatedByDoubleclick = aSelectionCreatedByDoubleclick;
+ }
+
+ [[nodiscard]] bool SelectionCreatedByDoubleclick() const {
+ return mSelectionCreatedByDoubleclick;
+ }
+
+ void UpdateEditAction(EditAction aEditAction) {
+ MOZ_ASSERT(!mHasTriedToDispatchBeforeInputEvent,
+ "It's too late to update EditAction since this may have "
+ "already dispatched a beforeinput event");
+ mEditAction = aEditAction;
+ }
+
+ /**
+ * CanHandle() or CanHandleAndHandleBeforeInput() must be called
+ * immediately after creating the instance. If caller does not need to
+ * handle "beforeinput" event or caller needs to set additional information
+ * the events later, use the former. Otherwise, use the latter. If caller
+ * uses the former, it's required to call MaybeDispatchBeforeInputEvent() by
+ * itself.
+ *
+ */
+ [[nodiscard]] bool CanHandle() const {
+#ifdef DEBUG
+ mHasCanHandleChecked = true;
+#endif // #ifdefn DEBUG
+ // Don't allow to run new edit action when an edit action caused
+ // destroying the editor while it's being handled.
+ if (mEditAction != EditAction::eInitializing &&
+ mEditorWasDestroyedDuringHandlingEditAction) {
+ NS_WARNING("Editor was destroyed during an edit action being handled");
+ return false;
+ }
+ return IsDataAvailable();
+ }
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ CanHandleAndMaybeDispatchBeforeInputEvent() {
+ if (MOZ_UNLIKELY(NS_WARN_IF(!CanHandle()))) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv = MaybeFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ return rv;
+ }
+ return MaybeDispatchBeforeInputEvent();
+ }
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ CanHandleAndFlushPendingNotifications() {
+ if (MOZ_UNLIKELY(NS_WARN_IF(!CanHandle()))) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ MOZ_ASSERT(MayEditActionRequireLayout(mRawEditAction));
+ return MaybeFlushPendingNotifications();
+ }
+
+ [[nodiscard]] bool IsDataAvailable() const {
+ return mSelection && mEditorBase.mDocument;
+ }
+
+ /**
+ * MaybeDispatchBeforeInputEvent() considers whether this instance needs to
+ * dispatch "beforeinput" event or not. Then,
+ * mHasTriedToDispatchBeforeInputEvent is set to true.
+ *
+ * @param aDeleteDirectionAndAmount
+ * If `MayEditActionDeleteAroundCollapsedSelection(
+ * mEditAction)` returns true, this must be set.
+ * Otherwise, don't set explicitly.
+ * @return If this method actually dispatches "beforeinput" event
+ * and it's canceled, returns
+ * NS_ERROR_EDITOR_ACTION_CANCELED.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MaybeDispatchBeforeInputEvent(
+ nsIEditor::EDirection aDeleteDirectionAndAmount = nsIEditor::eNone);
+
+ /**
+ * MarkAsBeforeInputHasBeenDispatched() should be called only when updating
+ * the DOM occurs asynchronously from user input (e.g., inserting blob
+ * object which is loaded asynchronously) and `beforeinput` has already
+ * been dispatched (always should be so).
+ */
+ void MarkAsBeforeInputHasBeenDispatched() {
+ MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent());
+ MOZ_ASSERT(mEditAction == EditAction::ePaste ||
+ mEditAction == EditAction::ePasteAsQuotation ||
+ mEditAction == EditAction::eDrop);
+ mHasTriedToDispatchBeforeInputEvent = true;
+ }
+
+ /**
+ * MarkAsHandled() is called before dispatching `input` event and notifying
+ * editor observers. After this is called, any nested edit action become
+ * non illegal case.
+ */
+ void MarkAsHandled() {
+ MOZ_ASSERT(!mHandled);
+ mHandled = true;
+ }
+
+ /**
+ * ShouldAlreadyHaveHandledBeforeInputEventDispatching() returns true if the
+ * edit action requires to handle "beforeinput" event but not yet dispatched
+ * it nor considered as not dispatched it and can dispatch it when this is
+ * called.
+ */
+ bool ShouldAlreadyHaveHandledBeforeInputEventDispatching() const {
+ return !HasTriedToDispatchBeforeInputEvent() &&
+ NeedsBeforeInputEventHandling(mEditAction) &&
+ IsBeforeInputEventEnabled() /* &&
+ // If we still need to dispatch a clipboard event, we should
+ // dispatch it first, then, we need to dispatch beforeinput
+ // event later.
+ !NeedsToDispatchClipboardEvent()*/
+ ;
+ }
+
+ /**
+ * HasTriedToDispatchBeforeInputEvent() returns true if the instance's
+ * MaybeDispatchBeforeInputEvent() has already been called.
+ */
+ bool HasTriedToDispatchBeforeInputEvent() const {
+ return mHasTriedToDispatchBeforeInputEvent;
+ }
+
+ bool IsCanceled() const { return mBeforeInputEventCanceled; }
+
+ /**
+ * Returns a `Selection` for normal selection. The lifetime is guaranteed
+ * during alive this instance in the stack.
+ */
+ MOZ_KNOWN_LIVE Selection& SelectionRef() const {
+ MOZ_ASSERT(!mSelection ||
+ (mSelection->GetType() == SelectionType::eNormal));
+ return *mSelection;
+ }
+
+ nsIPrincipal* GetPrincipal() const { return mPrincipal; }
+ EditAction GetEditAction() const { return mEditAction; }
+
+ template <typename PT, typename CT>
+ void SetSpellCheckRestartPoint(const EditorDOMPointBase<PT, CT>& aPoint) {
+ MOZ_ASSERT(aPoint.IsSet());
+ // We should store only container and offset because new content may
+ // be inserted before referring child.
+ // XXX Shouldn't we compare whether aPoint is before
+ // mSpellCheckRestartPoint if it's set.
+ mSpellCheckRestartPoint =
+ EditorDOMPoint(aPoint.GetContainer(), aPoint.Offset());
+ }
+ void ClearSpellCheckRestartPoint() { mSpellCheckRestartPoint.Clear(); }
+ const EditorDOMPoint& GetSpellCheckRestartPoint() const {
+ return mSpellCheckRestartPoint;
+ }
+
+ void SetData(const nsAString& aData) {
+ MOZ_ASSERT(!mHasTriedToDispatchBeforeInputEvent,
+ "It's too late to set data since this may have already "
+ "dispatched a beforeinput event");
+ mData = aData;
+ }
+ const nsString& GetData() const { return mData; }
+
+ void SetColorData(const nsAString& aData);
+
+ /**
+ * InitializeDataTransfer(DataTransfer*) sets mDataTransfer to
+ * aDataTransfer. In this case, aDataTransfer should not be read/write
+ * because it'll be set to InputEvent.dataTransfer and which should be
+ * read-only.
+ */
+ void InitializeDataTransfer(dom::DataTransfer* aDataTransfer);
+ /**
+ * InitializeDataTransfer(nsITransferable*) creates new DataTransfer
+ * instance, initializes it with aTransferable and sets mDataTransfer to
+ * it.
+ */
+ void InitializeDataTransfer(nsITransferable* aTransferable);
+ /**
+ * InitializeDataTransfer(const nsAString&) creates new DataTransfer
+ * instance, initializes it with aString and sets mDataTransfer to it.
+ */
+ void InitializeDataTransfer(const nsAString& aString);
+ /**
+ * InitializeDataTransferWithClipboard() creates new DataTransfer instance,
+ * initializes it with clipboard and sets mDataTransfer to it.
+ */
+ void InitializeDataTransferWithClipboard(
+ SettingDataTransfer aSettingDataTransfer, int32_t aClipboardType);
+ dom::DataTransfer* GetDataTransfer() const { return mDataTransfer; }
+
+ /**
+ * AppendTargetRange() appends aTargetRange to target ranges. This should
+ * be used only by edit action handlers which do not want to set target
+ * ranges to selection ranges.
+ */
+ void AppendTargetRange(dom::StaticRange& aTargetRange);
+
+ /**
+ * Make dispatching `beforeinput` forcibly non-cancelable.
+ */
+ void MakeBeforeInputEventNonCancelable() {
+ mMakeBeforeInputEventNonCancelable = true;
+ }
+
+ /**
+ * NotifyOfDispatchingClipboardEvent() is called after dispatching
+ * a clipboard event.
+ */
+ void NotifyOfDispatchingClipboardEvent() {
+ MOZ_ASSERT(NeedsToDispatchClipboardEvent());
+ MOZ_ASSERT(!mHasTriedToDispatchClipboardEvent);
+ mHasTriedToDispatchClipboardEvent = true;
+ }
+
+ void Abort() { mAborted = true; }
+ bool IsAborted() const { return mAborted; }
+
+ void OnEditorDestroy() {
+ if (!mHandled && mHasTriedToDispatchBeforeInputEvent) {
+ // Remember the editor was destroyed only when this edit action is being
+ // handled because they are caused by mutation event listeners or
+ // something other unexpected event listeners. In the cases, new child
+ // edit action shouldn't been aborted.
+ mEditorWasDestroyedDuringHandlingEditAction = true;
+ }
+ if (mParentData) {
+ mParentData->OnEditorDestroy();
+ }
+ }
+ bool HasEditorDestroyedDuringHandlingEditAction() const {
+ return mEditorWasDestroyedDuringHandlingEditAction;
+ }
+
+ void SetTopLevelEditSubAction(EditSubAction aEditSubAction,
+ EDirection aDirection = eNone) {
+ mTopLevelEditSubAction = aEditSubAction;
+ TopLevelEditSubActionDataRef().Clear();
+ switch (mTopLevelEditSubAction) {
+ case EditSubAction::eInsertNode:
+ case EditSubAction::eMoveNode:
+ case EditSubAction::eCreateNode:
+ case EditSubAction::eSplitNode:
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eSetTextProperty:
+ case EditSubAction::eRemoveTextProperty:
+ case EditSubAction::eRemoveAllTextProperties:
+ case EditSubAction::eSetText:
+ case EditSubAction::eInsertLineBreak:
+ case EditSubAction::eInsertParagraphSeparator:
+ case EditSubAction::eCreateOrChangeList:
+ case EditSubAction::eIndent:
+ case EditSubAction::eOutdent:
+ case EditSubAction::eSetOrClearAlignment:
+ case EditSubAction::eCreateOrRemoveBlock:
+ case EditSubAction::eFormatBlockForHTMLCommand:
+ case EditSubAction::eMergeBlockContents:
+ case EditSubAction::eRemoveList:
+ case EditSubAction::eCreateOrChangeDefinitionListItem:
+ case EditSubAction::eInsertElement:
+ case EditSubAction::eInsertQuotation:
+ case EditSubAction::eInsertQuotedText:
+ case EditSubAction::ePasteHTMLContent:
+ case EditSubAction::eInsertHTMLSource:
+ case EditSubAction::eSetPositionToAbsolute:
+ case EditSubAction::eSetPositionToStatic:
+ case EditSubAction::eDecreaseZIndex:
+ case EditSubAction::eIncreaseZIndex:
+ MOZ_ASSERT(aDirection == eNext);
+ mDirectionOfTopLevelEditSubAction = eNext;
+ break;
+ case EditSubAction::eJoinNodes:
+ case EditSubAction::eDeleteText:
+ MOZ_ASSERT(aDirection == ePrevious);
+ mDirectionOfTopLevelEditSubAction = ePrevious;
+ break;
+ case EditSubAction::eUndo:
+ case EditSubAction::eRedo:
+ case EditSubAction::eComputeTextToOutput:
+ case EditSubAction::eCreatePaddingBRElementForEmptyEditor:
+ case EditSubAction::eNone:
+ case EditSubAction::eReplaceHeadWithHTMLSource:
+ MOZ_ASSERT(aDirection == eNone);
+ mDirectionOfTopLevelEditSubAction = eNone;
+ break;
+ case EditSubAction::eDeleteNode:
+ case EditSubAction::eDeleteSelectedContent:
+ // Unfortunately, eDeleteNode and eDeleteSelectedContent is used with
+ // any direction. We might have specific sub-action for each
+ // direction, but there are some points referencing
+ // eDeleteSelectedContent so that we should keep storing direction
+ // as-is for now.
+ mDirectionOfTopLevelEditSubAction = aDirection;
+ break;
+ }
+ }
+ EditSubAction GetTopLevelEditSubAction() const {
+ MOZ_ASSERT(IsDataAvailable());
+ return mTopLevelEditSubAction;
+ }
+ EDirection GetDirectionOfTopLevelEditSubAction() const {
+ return mDirectionOfTopLevelEditSubAction;
+ }
+
+ const TopLevelEditSubActionData& TopLevelEditSubActionDataRef() const {
+ return mParentData ? mParentData->TopLevelEditSubActionDataRef()
+ : mTopLevelEditSubActionData;
+ }
+ TopLevelEditSubActionData& TopLevelEditSubActionDataRef() {
+ return mParentData ? mParentData->TopLevelEditSubActionDataRef()
+ : mTopLevelEditSubActionData;
+ }
+
+ const EditSubActionData& EditSubActionDataRef() const {
+ return mEditSubActionData;
+ }
+ EditSubActionData& EditSubActionDataRef() { return mEditSubActionData; }
+
+ SelectionState& SavedSelectionRef() {
+ return mParentData ? mParentData->SavedSelectionRef() : mSavedSelection;
+ }
+ const SelectionState& SavedSelectionRef() const {
+ return mParentData ? mParentData->SavedSelectionRef() : mSavedSelection;
+ }
+
+ RangeUpdater& RangeUpdaterRef() {
+ return mParentData ? mParentData->RangeUpdaterRef() : mRangeUpdater;
+ }
+ const RangeUpdater& RangeUpdaterRef() const {
+ return mParentData ? mParentData->RangeUpdaterRef() : mRangeUpdater;
+ }
+
+ void UpdateSelectionCache(Selection& aSelection);
+
+ private:
+ bool IsBeforeInputEventEnabled() const;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ MaybeFlushPendingNotifications() const;
+
+ static bool NeedsBeforeInputEventHandling(EditAction aEditAction) {
+ MOZ_ASSERT(aEditAction != EditAction::eNone);
+ switch (aEditAction) {
+ case EditAction::eNone:
+ // If we're not handling edit action, we don't need to handle
+ // "beforeinput" event.
+ case EditAction::eNotEditing:
+ // If we're being initialized, we may need to create a padding <br>
+ // element, but it shouldn't cause `beforeinput` event.
+ case EditAction::eInitializing:
+ // If we're just selecting or getting table cells, we shouldn't
+ // dispatch `beforeinput` event.
+ case NS_EDIT_ACTION_CASES_ACCESSING_TABLE_DATA_WITHOUT_EDITING:
+ // If raw level transaction API is used, the API user needs to handle
+ // both "beforeinput" event and "input" event if it's necessary.
+ case EditAction::eUnknown:
+ // Hiding/showing password affects only layout so that we don't need
+ // to handle beforeinput event for it.
+ case EditAction::eHidePassword:
+ // We don't need to dispatch "beforeinput" event before
+ // "compositionstart".
+ case EditAction::eStartComposition:
+ // We don't need to let web apps know the mode change.
+ case EditAction::eEnableOrDisableCSS:
+ case EditAction::eEnableOrDisableAbsolutePositionEditor:
+ case EditAction::eEnableOrDisableResizer:
+ case EditAction::eEnableOrDisableInlineTableEditingUI:
+ // We don't need to let contents in chrome's editor to know the size
+ // change.
+ case EditAction::eSetWrapWidth:
+ // While resizing or moving element, we update only shadow, i.e.,
+ // don't touch to the DOM in content. Therefore, we don't need to
+ // dispatch "beforeinput" event.
+ case EditAction::eResizingElement:
+ case EditAction::eMovingElement:
+ // Perhaps, we don't need to dispatch "beforeinput" event for
+ // padding `<br>` element for empty editor because it's internal
+ // handling and it should be occurred by another change.
+ case EditAction::eCreatePaddingBRElementForEmptyEditor:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ bool NeedsToDispatchClipboardEvent() const {
+ if (mHasTriedToDispatchClipboardEvent) {
+ return false;
+ }
+ switch (mEditAction) {
+ case EditAction::ePaste:
+ case EditAction::ePasteAsQuotation:
+ case EditAction::eCut:
+ case EditAction::eCopy:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ EditorBase& mEditorBase;
+ RefPtr<Selection> mSelection;
+ nsTArray<OwningNonNull<Selection>> mRetiredSelections;
+
+ // True if the selection was created by doubleclicking a word.
+ bool mSelectionCreatedByDoubleclick{false};
+
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ // EditAction may be nested, for example, a command may be executed
+ // from mutation event listener which is run while editor changes
+ // the DOM tree. In such case, we need to handle edit action separately.
+ AutoEditActionDataSetter* mParentData;
+
+ // Cached selection for HTMLEditor::AutoSelectionRestorer.
+ SelectionState mSavedSelection;
+
+ // Utility class object for maintaining preserved ranges.
+ RangeUpdater mRangeUpdater;
+
+ // The data should be set to InputEvent.data.
+ nsString mData;
+
+ // The dataTransfer should be set to InputEvent.dataTransfer.
+ RefPtr<dom::DataTransfer> mDataTransfer;
+
+ // They are used for result of InputEvent.getTargetRanges() of beforeinput.
+ OwningNonNullStaticRangeArray mTargetRanges;
+
+ // Start point where spell checker should check from. This is used only
+ // by TextEditor.
+ EditorDOMPoint mSpellCheckRestartPoint;
+
+ // Different from mTopLevelEditSubAction, its data should be stored only
+ // in the most ancestor AutoEditActionDataSetter instance since we don't
+ // want to pay the copying cost and sync cost.
+ TopLevelEditSubActionData mTopLevelEditSubActionData;
+
+ // Different from mTopLevelEditSubActionData, this stores temporaly data
+ // for current edit sub action.
+ EditSubActionData mEditSubActionData;
+
+ // mEditAction and mRawEditActions stores edit action. The difference of
+ // them is, if and only if edit actions are nested and parent edit action
+ // is one of trying to edit something, but nested one is not so, it's
+ // overwritten by the parent edit action.
+ EditAction mEditAction;
+ EditAction mRawEditAction;
+
+ // Different from its data, you can refer "current" AutoEditActionDataSetter
+ // instance's mTopLevelEditSubAction member since it's copied from the
+ // parent instance at construction and it's always cleared before this
+ // won't be overwritten and cleared before destruction.
+ EditSubAction mTopLevelEditSubAction;
+
+ EDirection mDirectionOfTopLevelEditSubAction;
+
+ bool mAborted;
+
+ // Set to true when this handles "beforeinput" event dispatching. Note
+ // that even if "beforeinput" event shouldn't be dispatched for this,
+ // instance, this is set to true when it's considered.
+ bool mHasTriedToDispatchBeforeInputEvent;
+ // Set to true if "beforeinput" event was dispatched and it's canceled.
+ bool mBeforeInputEventCanceled;
+ // Set to true if `beforeinput` event must not be cancelable even if
+ // its inputType is defined as cancelable by the standards.
+ bool mMakeBeforeInputEventNonCancelable;
+ // Set to true when the edit action handler tries to dispatch a clipboard
+ // event.
+ bool mHasTriedToDispatchClipboardEvent;
+ // The editor instance may be destroyed once temporarily if `document.write`
+ // etc runs. In such case, we should mark this flag of being handled
+ // edit action.
+ bool mEditorWasDestroyedDuringHandlingEditAction;
+ // This is set before dispatching `input` event and notifying editor
+ // observers.
+ bool mHandled;
+
+#ifdef DEBUG
+ mutable bool mHasCanHandleChecked = false;
+#endif // #ifdef DEBUG
+
+ AutoEditActionDataSetter() = delete;
+ AutoEditActionDataSetter(const AutoEditActionDataSetter& aOther) = delete;
+ };
+
+ void UpdateEditActionData(const nsAString& aData) {
+ mEditActionData->SetData(aData);
+ }
+
+ void NotifyOfDispatchingClipboardEvent() {
+ MOZ_ASSERT(mEditActionData);
+ mEditActionData->NotifyOfDispatchingClipboardEvent();
+ }
+
+ protected: // May be called by friends.
+ /****************************************************************************
+ * Some friend classes are allowed to call the following protected methods.
+ * However, those methods won't prepare caches of some objects which are
+ * necessary for them. So, if you call them from friend classes, you need
+ * to make sure that AutoEditActionDataSetter is created.
+ ****************************************************************************/
+
+ bool IsEditActionCanceled() const {
+ MOZ_ASSERT(mEditActionData);
+ return mEditActionData->IsCanceled();
+ }
+
+ bool ShouldAlreadyHaveHandledBeforeInputEventDispatching() const {
+ MOZ_ASSERT(mEditActionData);
+ return mEditActionData
+ ->ShouldAlreadyHaveHandledBeforeInputEventDispatching();
+ }
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MaybeDispatchBeforeInputEvent() {
+ MOZ_ASSERT(mEditActionData);
+ return mEditActionData->MaybeDispatchBeforeInputEvent();
+ }
+
+ void MarkAsBeforeInputHasBeenDispatched() {
+ MOZ_ASSERT(mEditActionData);
+ return mEditActionData->MarkAsBeforeInputHasBeenDispatched();
+ }
+
+ bool HasTriedToDispatchBeforeInputEvent() const {
+ return mEditActionData &&
+ mEditActionData->HasTriedToDispatchBeforeInputEvent();
+ }
+
+ bool IsEditActionDataAvailable() const {
+ return mEditActionData && mEditActionData->IsDataAvailable();
+ }
+
+ bool IsTopLevelEditSubActionDataAvailable() const {
+ return mEditActionData && !!GetTopLevelEditSubAction();
+ }
+
+ bool IsEditActionAborted() const {
+ MOZ_ASSERT(mEditActionData);
+ return mEditActionData->IsAborted();
+ }
+
+ /**
+ * SelectionRef() returns cached normal Selection. This is pretty faster than
+ * EditorBase::GetSelection() if available.
+ * Note that this never crash unless public methods ignore the result of
+ * AutoEditActionDataSetter::CanHandle() and keep handling edit action but any
+ * methods should stop handling edit action if it returns false.
+ */
+ MOZ_KNOWN_LIVE Selection& SelectionRef() const {
+ MOZ_ASSERT(mEditActionData);
+ MOZ_ASSERT(mEditActionData->SelectionRef().GetType() ==
+ SelectionType::eNormal);
+ return mEditActionData->SelectionRef();
+ }
+
+ nsIPrincipal* GetEditActionPrincipal() const {
+ MOZ_ASSERT(mEditActionData);
+ return mEditActionData->GetPrincipal();
+ }
+
+ /**
+ * GetEditAction() returns EditAction which is being handled. If some
+ * edit actions are nested, this returns the innermost edit action.
+ */
+ EditAction GetEditAction() const {
+ return mEditActionData ? mEditActionData->GetEditAction()
+ : EditAction::eNone;
+ }
+
+ /**
+ * GetInputEventData() returns inserting or inserted text value with
+ * current edit action. The result is proper for InputEvent.data value.
+ */
+ const nsString& GetInputEventData() const {
+ return mEditActionData ? mEditActionData->GetData() : VoidString();
+ }
+
+ /**
+ * GetInputEventDataTransfer() returns inserting or inserted transferable
+ * content with current edit action. The result is proper for
+ * InputEvent.dataTransfer value.
+ */
+ dom::DataTransfer* GetInputEventDataTransfer() const {
+ return mEditActionData ? mEditActionData->GetDataTransfer() : nullptr;
+ }
+
+ /**
+ * GetTopLevelEditSubAction() returns the top level edit sub-action.
+ * For example, if selected content is being replaced with inserted text,
+ * while removing selected content, the top level edit sub-action may be
+ * EditSubAction::eDeleteSelectedContent. However, while inserting new
+ * text, the top level edit sub-action may be EditSubAction::eInsertText.
+ * So, this result means what we are doing right now unless you're looking
+ * for a case which the method is called via mutation event listener or
+ * selectionchange event listener which are fired while handling the edit
+ * sub-action.
+ */
+ EditSubAction GetTopLevelEditSubAction() const {
+ return mEditActionData ? mEditActionData->GetTopLevelEditSubAction()
+ : EditSubAction::eNone;
+ }
+
+ /**
+ * GetDirectionOfTopLevelEditSubAction() returns direction which user
+ * intended for doing the edit sub-action.
+ */
+ EDirection GetDirectionOfTopLevelEditSubAction() const {
+ return mEditActionData
+ ? mEditActionData->GetDirectionOfTopLevelEditSubAction()
+ : eNone;
+ }
+
+ /**
+ * SavedSelection() returns reference to saved selection which are
+ * stored by HTMLEditor::AutoSelectionRestorer.
+ */
+ SelectionState& SavedSelectionRef() {
+ MOZ_ASSERT(IsHTMLEditor());
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->SavedSelectionRef();
+ }
+ const SelectionState& SavedSelectionRef() const {
+ MOZ_ASSERT(IsHTMLEditor());
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->SavedSelectionRef();
+ }
+
+ RangeUpdater& RangeUpdaterRef() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->RangeUpdaterRef();
+ }
+ const RangeUpdater& RangeUpdaterRef() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->RangeUpdaterRef();
+ }
+
+ template <typename PT, typename CT>
+ void SetSpellCheckRestartPoint(const EditorDOMPointBase<PT, CT>& aPoint) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->SetSpellCheckRestartPoint(aPoint);
+ }
+
+ void ClearSpellCheckRestartPoint() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->ClearSpellCheckRestartPoint();
+ }
+
+ const EditorDOMPoint& GetSpellCheckRestartPoint() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->GetSpellCheckRestartPoint();
+ }
+
+ const TopLevelEditSubActionData& TopLevelEditSubActionDataRef() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->TopLevelEditSubActionDataRef();
+ }
+ TopLevelEditSubActionData& TopLevelEditSubActionDataRef() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->TopLevelEditSubActionDataRef();
+ }
+
+ const EditSubActionData& EditSubActionDataRef() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->EditSubActionDataRef();
+ }
+ EditSubActionData& EditSubActionDataRef() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return mEditActionData->EditSubActionDataRef();
+ }
+
+ /**
+ * GetFirstIMESelectionStartPoint() and GetLastIMESelectionEndPoint() returns
+ * start of first IME selection range or end of last IME selection range if
+ * there is. Otherwise, returns non-set DOM point.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType GetFirstIMESelectionStartPoint() const;
+ template <typename EditorDOMPointType>
+ EditorDOMPointType GetLastIMESelectionEndPoint() const;
+
+ /**
+ * IsSelectionRangeContainerNotContent() returns true if one of container
+ * of selection ranges is not a content node, i.e., a Document node.
+ */
+ bool IsSelectionRangeContainerNotContent() const;
+
+ /**
+ * OnInputText() is called when user inputs text with keyboard or something.
+ *
+ * @param aStringToInsert The string to insert.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ OnInputText(const nsAString& aStringToInsert);
+
+ /**
+ * InsertTextAsSubAction() inserts aStringToInsert at selection. This
+ * should be used for handling it as an edit sub-action.
+ *
+ * @param aStringToInsert The string to insert.
+ * @param aSelectionHandling Specify whether selected content should be
+ * deleted or ignored.
+ */
+ enum class SelectionHandling { Ignore, Delete };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertTextAsSubAction(
+ const nsAString& aStringToInsert, SelectionHandling aSelectionHandling);
+
+ /**
+ * Insert aStringToInsert to aPointToInsert or better insertion point around
+ * it. If aPointToInsert isn't in a text node, this method looks for the
+ * nearest point in a text node with TextEditor::FindBetterInsertionPoint()
+ * or EditorDOMPoint::GetPointInTextNodeIfPointingAroundTextNode().
+ * If there is no text node, this creates new text node and put
+ * aStringToInsert to it.
+ *
+ * @param aDocument The document of this editor.
+ * @param aStringToInsert The string to insert.
+ * @param aPointToInsert The point to insert aStringToInsert.
+ * Must be valid DOM point.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Result<InsertTextResult, nsresult>
+ InsertTextWithTransaction(Document& aDocument,
+ const nsAString& aStringToInsert,
+ const EditorDOMPoint& aPointToInsert);
+
+ /**
+ * Insert aStringToInsert to aPointToInsert.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertTextResult, nsresult>
+ InsertTextIntoTextNodeWithTransaction(
+ const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert);
+
+ /**
+ * SetTextNodeWithoutTransaction() is optimized path to set new value to
+ * the text node directly and without transaction. This is used when
+ * setting `<input>.value` and `<textarea>.value`.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ SetTextNodeWithoutTransaction(const nsAString& aString, Text& aTextNode);
+
+ /**
+ * DeleteNodeWithTransaction() removes aContent from the DOM tree.
+ *
+ * @param aContent The node which will be removed form the DOM tree.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteNodeWithTransaction(nsIContent& aContent);
+
+ /**
+ * InsertNodeWithTransaction() inserts aContentToInsert before the child
+ * specified by aPointToInsert.
+ *
+ * @param aContentToInsert The node to be inserted.
+ * @param aPointToInsert The insertion point of aContentToInsert.
+ * If this refers end of the container, the
+ * transaction will append the node to the
+ * container. Otherwise, will insert the node
+ * before child node referred by this.
+ * @return If succeeded, returns the new content node and
+ * point to put caret.
+ */
+ template <typename ContentNodeType>
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT
+ Result<CreateNodeResultBase<ContentNodeType>, nsresult>
+ InsertNodeWithTransaction(ContentNodeType& aContentToInsert,
+ const EditorDOMPoint& aPointToInsert);
+
+ /**
+ * InsertPaddingBRElementForEmptyLastLineWithTransaction() creates a padding
+ * <br> element with setting flags to NS_PADDING_FOR_EMPTY_LAST_LINE and
+ * inserts it around aPointToInsert.
+ *
+ * @param aPointToInsert The DOM point where should be <br> node inserted
+ * before.
+ * @return If succeeded, returns the new <br> element and
+ * point to put caret around it.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ const EditorDOMPoint& aPointToInsert);
+
+ /**
+ * CloneAttributesWithTransaction() clones all attributes from
+ * aSourceElement to aDestElement after removing all attributes in
+ * aDestElement.
+ */
+ MOZ_CAN_RUN_SCRIPT void CloneAttributesWithTransaction(
+ Element& aDestElement, Element& aSourceElement);
+
+ /**
+ * CloneAttributeWithTransaction() copies aAttribute of aSourceElement to
+ * aDestElement. If aSourceElement doesn't have aAttribute, this removes
+ * aAttribute from aDestElement.
+ *
+ * @param aAttribute Attribute name to be cloned.
+ * @param aDestElement Element node which will be set aAttribute or
+ * whose aAttribute will be removed.
+ * @param aSourceElement Element node which provides the value of
+ * aAttribute in aDestElement.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult CloneAttributeWithTransaction(
+ nsAtom& aAttribute, Element& aDestElement, Element& aSourceElement);
+
+ /**
+ * RemoveAttributeWithTransaction() removes aAttribute from aElement.
+ *
+ * @param aElement Element node which will lose aAttribute.
+ * @param aAttribute Attribute name to be removed from aElement.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ RemoveAttributeWithTransaction(Element& aElement, nsAtom& aAttribute);
+
+ MOZ_CAN_RUN_SCRIPT virtual nsresult RemoveAttributeOrEquivalent(
+ Element* aElement, nsAtom* aAttribute, bool aSuppressTransaction) = 0;
+
+ /**
+ * SetAttributeWithTransaction() sets aAttribute of aElement to aValue.
+ *
+ * @param aElement Element node which will have aAttribute.
+ * @param aAttribute Attribute name to be set.
+ * @param aValue Attribute value be set to aAttribute.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetAttributeWithTransaction(
+ Element& aElement, nsAtom& aAttribute, const nsAString& aValue);
+
+ MOZ_CAN_RUN_SCRIPT virtual nsresult SetAttributeOrEquivalent(
+ Element* aElement, nsAtom* aAttribute, const nsAString& aValue,
+ bool aSuppressTransaction) = 0;
+
+ /**
+ * Method to replace certain CreateElementNS() calls.
+ *
+ * @param aTag Tag you want.
+ */
+ already_AddRefed<Element> CreateHTMLContent(const nsAtom* aTag) const;
+
+ /**
+ * Creates text node which is marked as "maybe modified frequently" and
+ * "maybe masked" if this is a password editor.
+ */
+ already_AddRefed<nsTextNode> CreateTextNode(const nsAString& aData) const;
+
+ /**
+ * DoInsertText(), DoDeleteText(), DoReplaceText() and DoSetText() are
+ * wrapper of `CharacterData::InsertData()`, `CharacterData::DeleteData()`,
+ * `CharacterData::ReplaceData()` and `CharacterData::SetData()`.
+ */
+ MOZ_CAN_RUN_SCRIPT void DoInsertText(dom::Text& aText, uint32_t aOffset,
+ const nsAString& aStringToInsert,
+ ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void DoDeleteText(dom::Text& aText, uint32_t aOffset,
+ uint32_t aCount, ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void DoReplaceText(dom::Text& aText, uint32_t aOffset,
+ uint32_t aCount,
+ const nsAString& aStringToInsert,
+ ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void DoSetText(dom::Text& aText,
+ const nsAString& aStringToSet,
+ ErrorResult& aRv);
+
+ /**
+ * Delete text in the range in aTextNode. Use
+ * `HTMLEditor::ReplaceTextWithTransaction` if you'll insert text there (and
+ * if you want to use it in `TextEditor`, move it into `EditorBase`).
+ *
+ * @param aTextNode The text node which should be modified.
+ * @param aOffset Start offset of removing text in aTextNode.
+ * @param aLength Length of removing text.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ DeleteTextWithTransaction(dom::Text& aTextNode, uint32_t aOffset,
+ uint32_t aLength);
+
+ /**
+ * MarkElementDirty() sets a special dirty attribute on the element.
+ * Usually this will be called immediately after creating a new node.
+ *
+ * @param aElement The element for which to insert formatting.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ MarkElementDirty(Element& aElement) const;
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DoTransactionInternal(nsITransaction* aTransaction);
+
+ /**
+ * Returns true if aNode is our root node. The root is:
+ * If TextEditor, the anonymous <div> element.
+ * If HTMLEditor, a <body> element or the document element which may not be
+ * editable if it's not in the design mode.
+ */
+ bool IsRoot(const nsINode* inNode) const;
+
+ /**
+ * Returns true if aNode is a descendant of our root node.
+ * See the comment for IsRoot() for what the root node means.
+ */
+ bool IsDescendantOfRoot(const nsINode* inNode) const;
+
+ /**
+ * Returns true when inserting text should be a part of current composition.
+ */
+ bool ShouldHandleIMEComposition() const;
+
+ template <typename EditorDOMPointType>
+ EditorDOMPointType GetFirstSelectionStartPoint() const;
+ template <typename EditorDOMPointType>
+ EditorDOMPointType GetFirstSelectionEndPoint() const;
+
+ static nsresult GetEndChildNode(const Selection& aSelection,
+ nsIContent** aEndNode);
+
+ template <typename PT, typename CT>
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ CollapseSelectionTo(const EditorDOMPointBase<PT, CT>& aPoint) const {
+ // We don't need to throw exception directly for a failure of updating
+ // selection. Therefore, let's use IgnoredErrorResult for the performance.
+ IgnoredErrorResult error;
+ CollapseSelectionTo(aPoint, error);
+ return error.StealNSResult();
+ }
+
+ template <typename PT, typename CT>
+ MOZ_CAN_RUN_SCRIPT void CollapseSelectionTo(
+ const EditorDOMPointBase<PT, CT>& aPoint, ErrorResult& aRv) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRv.Failed());
+
+ if (aPoint.GetInterlinePosition() != InterlinePosition::Undefined) {
+ if (MOZ_UNLIKELY(NS_FAILED(SelectionRef().SetInterlinePosition(
+ aPoint.GetInterlinePosition())))) {
+ NS_WARNING("Selection::SetInterlinePosition() failed");
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+
+ SelectionRef().CollapseInLimiter(aPoint, aRv);
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING("Selection::CollapseInLimiter() caused destroying the editor");
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+ NS_WARNING_ASSERTION(!aRv.Failed(),
+ "Selection::CollapseInLimiter() failed");
+ }
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ CollapseSelectionToStartOf(nsINode& aNode) const {
+ return CollapseSelectionTo(EditorRawDOMPoint(&aNode, 0u));
+ }
+
+ MOZ_CAN_RUN_SCRIPT void CollapseSelectionToStartOf(nsINode& aNode,
+ ErrorResult& aRv) const {
+ CollapseSelectionTo(EditorRawDOMPoint(&aNode, 0u), aRv);
+ }
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ CollapseSelectionToEndOf(nsINode& aNode) const {
+ return CollapseSelectionTo(EditorRawDOMPoint::AtEndOf(aNode));
+ }
+
+ MOZ_CAN_RUN_SCRIPT void CollapseSelectionToEndOf(nsINode& aNode,
+ ErrorResult& aRv) const {
+ CollapseSelectionTo(EditorRawDOMPoint::AtEndOf(aNode), aRv);
+ }
+
+ /**
+ * AllowsTransactionsToChangeSelection() returns true if editor allows any
+ * transactions to change Selection. Otherwise, transactions shouldn't
+ * change Selection.
+ */
+ inline bool AllowsTransactionsToChangeSelection() const {
+ return mAllowsTransactionsToChangeSelection;
+ }
+
+ /**
+ * MakeThisAllowTransactionsToChangeSelection() with true makes this editor
+ * allow transactions to change Selection. Otherwise, i.e., with false,
+ * makes this editor not allow transactions to change Selection.
+ */
+ inline void MakeThisAllowTransactionsToChangeSelection(bool aAllow) {
+ mAllowsTransactionsToChangeSelection = aAllow;
+ }
+
+ nsresult HandleInlineSpellCheck(
+ const EditorDOMPoint& aPreviouslySelectedStart,
+ const dom::AbstractRange* aRange = nullptr);
+
+ /**
+ * Whether the editor is active on the DOM window. Note that when this
+ * returns true but GetFocusedElement() returns null, it means that this
+ * editor was focused when the DOM window was active.
+ */
+ virtual bool IsActiveInDOMWindow() const;
+
+ /**
+ * HideCaret() hides caret with nsCaret::AddForceHide() or may show carent
+ * with nsCaret::RemoveForceHide(). This does NOT set visibility of
+ * nsCaret. Therefore, this is stateless.
+ */
+ void HideCaret(bool aHide);
+
+ protected: // Edit sub-action handler
+ /**
+ * AutoCaretBidiLevelManager() computes bidi level of caret, deleting
+ * character(s) from aPointAtCaret at construction. Then, if you'll
+ * need to extend the selection, you should calls `UpdateCaretBidiLevel()`,
+ * then, this class may update caret bidi level for you if it's required.
+ */
+ class MOZ_RAII AutoCaretBidiLevelManager final {
+ public:
+ /**
+ * @param aEditorBase The editor.
+ * @param aPointAtCaret Collapsed `Selection` point.
+ * @param aDirectionAndAmount The direction and amount to delete.
+ */
+ template <typename PT, typename CT>
+ AutoCaretBidiLevelManager(const EditorBase& aEditorBase,
+ nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPointBase<PT, CT>& aPointAtCaret);
+
+ /**
+ * Failed() returns true if the constructor failed to handle the bidi
+ * information.
+ */
+ bool Failed() const { return mFailed; }
+
+ /**
+ * Canceled() returns true if when the caller should stop deleting
+ * characters since caret position is not visually adjacent the deleting
+ * characters and user does not wand to delete them in that case.
+ */
+ bool Canceled() const { return mCanceled; }
+
+ /**
+ * MaybeUpdateCaretBidiLevel() may update caret bidi level and schedule to
+ * paint it if they are necessary.
+ */
+ void MaybeUpdateCaretBidiLevel(const EditorBase& aEditorBase) const;
+
+ private:
+ Maybe<mozilla::intl::BidiEmbeddingLevel> mNewCaretBidiLevel;
+ bool mFailed = false;
+ bool mCanceled = false;
+ };
+
+ /**
+ * UndefineCaretBidiLevel() resets bidi level of the caret.
+ */
+ void UndefineCaretBidiLevel() const;
+
+ /**
+ * Flushing pending notifications if nsFrameSelection requires the latest
+ * layout information to compute deletion range. This may destroy the
+ * editor instance itself. When this returns false, don't keep doing
+ * anything.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool
+ FlushPendingNotificationsIfToHandleDeletionWithFrameSelection(
+ nsIEditor::EDirection aDirectionAndAmount) const;
+
+ /**
+ * DeleteSelectionAsSubAction() removes selection content or content around
+ * caret with transactions. This should be used for handling it as an
+ * edit sub-action.
+ *
+ * @param aDirectionAndAmount How much range should be removed.
+ * @param aStripWrappers Whether the parent blocks should be removed
+ * when they become empty. If this instance is
+ * a TextEditor, Must be nsIEditor::eNoStrip.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteSelectionAsSubAction(nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers);
+
+ /**
+ * This method handles "delete selection" commands.
+ * NOTE: Don't call this method recursively from the helper methods since
+ * when nobody handled it without canceling and returing an error,
+ * this falls it back to `DeleteSelectionWithTransaction()`.
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aStripWrappers Must be nsIEditor::eNoStrip if this is a
+ * TextEditor instance. Otherwise,
+ * nsIEditor::eStrip is also valid.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Result<EditActionResult, nsresult>
+ HandleDeleteSelection(nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) = 0;
+
+ /**
+ * ReplaceSelectionAsSubAction() replaces selection with aString.
+ *
+ * @param aString The string to replace.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ ReplaceSelectionAsSubAction(const nsAString& aString);
+
+ /**
+ * HandleInsertText() handles inserting text at selection.
+ *
+ * @param aEditSubAction Must be EditSubAction::eInsertText or
+ * EditSubAction::eInsertTextComingFromIME.
+ * @param aInsertionString String to be inserted at selection.
+ * @param aSelectionHandling Specify whether selected content should be
+ * deleted or ignored.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Result<EditActionResult, nsresult>
+ HandleInsertText(EditSubAction aEditSubAction,
+ const nsAString& aInsertionString,
+ SelectionHandling aSelectionHandling) = 0;
+
+ /**
+ * InsertWithQuotationsAsSubAction() inserts aQuotedText with appending ">"
+ * to start of every line.
+ *
+ * @param aQuotedText String to insert. This will be quoted by ">"
+ * automatically.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual nsresult
+ InsertWithQuotationsAsSubAction(const nsAString& aQuotedText) = 0;
+
+ /**
+ * PrepareInsertContent() is a helper method of InsertTextAt(),
+ * HTMLEditor::HTMLWithContextInserter::Run(). They insert content coming
+ * from clipboard or drag and drop. Before that, they may need to remove
+ * selected contents and adjust selection. This does them instead.
+ *
+ * @param aPointToInsert Point to insert. Must be set. Callers
+ * shouldn't use this instance after calling this
+ * method because this method may cause changing
+ * the DOM tree and Selection.
+ */
+ enum class DeleteSelectedContent : bool {
+ No, // Don't delete selection
+ Yes, // Delete selected content
+ };
+ MOZ_CAN_RUN_SCRIPT nsresult
+ PrepareToInsertContent(const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent);
+
+ /**
+ * InsertTextAt() inserts aStringToInsert at aPointToInsert.
+ *
+ * @param aStringToInsert The string which you want to insert.
+ * @param aPointToInsert The insertion point.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult InsertTextAt(
+ const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent);
+
+ /**
+ * Return whether the data is safe to insert as the source and destination
+ * principals match, or we are in a editor context where this doesn't matter.
+ * Otherwise, the data must be sanitized first.
+ */
+ enum class SafeToInsertData : bool { No, Yes };
+ SafeToInsertData IsSafeToInsertData(nsIPrincipal* aSourcePrincipal) const;
+
+ protected: // Called by helper classes.
+ /**
+ * OnStartToHandleTopLevelEditSubAction() is called when
+ * GetTopLevelEditSubAction() is EditSubAction::eNone and somebody starts to
+ * handle aEditSubAction.
+ *
+ * @param aTopLevelEditSubAction Top level edit sub action which
+ * will be handled soon.
+ * @param aDirectionOfTopLevelEditSubAction Direction of aEditSubAction.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual void OnStartToHandleTopLevelEditSubAction(
+ EditSubAction aTopLevelEditSubAction,
+ nsIEditor::EDirection aDirectionOfTopLevelEditSubAction,
+ ErrorResult& aRv);
+
+ /**
+ * OnEndHandlingTopLevelEditSubAction() is called after
+ * SetTopLevelEditSubAction() is handled.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult OnEndHandlingTopLevelEditSubAction();
+
+ /**
+ * OnStartToHandleEditSubAction() and OnEndHandlingEditSubAction() are called
+ * when starting to handle an edit sub action and ending handling an edit
+ * sub action.
+ */
+ void OnStartToHandleEditSubAction() { EditSubActionDataRef().Clear(); }
+ void OnEndHandlingEditSubAction() { EditSubActionDataRef().Clear(); }
+
+ /**
+ * (Begin|End)PlaceholderTransaction() are called by AutoPlaceholderBatch.
+ * This set of methods are similar to the (Begin|End)Transaction(), but do
+ * not use the transaction managers batching feature. Instead we use a
+ * placeholder transaction to wrap up any further transaction while the
+ * batch is open. The advantage of this is that placeholder transactions
+ * can later merge, if needed. Merging is unavailable between transaction
+ * manager batches.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void BeginPlaceholderTransaction(
+ nsStaticAtom& aTransactionName, const char* aRequesterFuncName);
+ enum class ScrollSelectionIntoView { No, Yes };
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void EndPlaceholderTransaction(
+ ScrollSelectionIntoView aScrollSelectionIntoView,
+ const char* aRequesterFuncName);
+
+ void BeginUpdateViewBatch(const char* aRequesterFuncName);
+ MOZ_CAN_RUN_SCRIPT void EndUpdateViewBatch(const char* aRequesterFuncName);
+
+ /**
+ * Used by HTMLEditor::AutoTransactionBatch, nsIEditor::BeginTransaction
+ * and nsIEditor::EndTransation. After calling BeginTransactionInternal(),
+ * all transactions will be treated as an atomic transaction. I.e., two or
+ * more transactions are undid once.
+ * XXX What's the difference with PlaceholderTransaction? Should we always
+ * use it instead?
+ */
+ MOZ_CAN_RUN_SCRIPT void BeginTransactionInternal(
+ const char* aRequesterFuncName);
+ MOZ_CAN_RUN_SCRIPT void EndTransactionInternal(
+ const char* aRequesterFuncName);
+
+ protected: // Shouldn't be used by friend classes
+ /**
+ * The default destructor. This should suffice. Should this be pure virtual
+ * for someone to derive from the EditorBase later? I don't believe so.
+ */
+ virtual ~EditorBase();
+
+ /**
+ * @param aDocument The dom document interface being observed
+ * @param aRootElement
+ * This is the root of the editable section of this
+ * document. If it is null then we get root from document
+ * body.
+ * @param aSelectionController
+ * The selection controller of selections which will be
+ * used in this editor.
+ * @param aFlags Some of nsIEditor::eEditor*Mask flags.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ InitInternal(Document& aDocument, Element* aRootElement,
+ nsISelectionController& aSelectionController, uint32_t aFlags);
+
+ /**
+ * PostCreateInternal() should be called after InitInternal(), and is the time
+ * that the editor tells its documentStateObservers that the document has been
+ * created.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult PostCreateInternal();
+
+ /**
+ * PreDestroyInternal() is called before the editor goes away, and gives the
+ * editor a chance to tell its documentStateObservers that the document is
+ * going away.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual void PreDestroyInternal();
+
+ MOZ_ALWAYS_INLINE EditorType GetEditorType() const {
+ return mIsHTMLEditorClass ? EditorType::HTML : EditorType::Text;
+ }
+
+ /**
+ * Check whether the caller can keep handling focus event.
+ *
+ * @param aOriginalEventTargetNode The original event target of the focus
+ * event.
+ */
+ [[nodiscard]] bool CanKeepHandlingFocusEvent(
+ const nsINode& aOriginalEventTargetNode) const;
+
+ /**
+ * If this editor has skipped spell checking and not yet flushed, this runs
+ * the spell checker.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult FlushPendingSpellCheck();
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult EnsureEmptyTextFirstChild();
+
+ int32_t WrapWidth() const { return mWrapColumn; }
+
+ /**
+ * ToGenericNSResult() computes proper nsresult value for the editor users.
+ * This should be used only when public methods return result of internal
+ * methods.
+ */
+ static inline nsresult ToGenericNSResult(nsresult aRv) {
+ switch (aRv) {
+ // If the editor is destroyed while handling an edit action, editor needs
+ // to stop handling it. However, editor throw exception in this case
+ // because Chrome does not throw exception even in this case.
+ case NS_ERROR_EDITOR_DESTROYED:
+ return NS_OK;
+ // If editor meets unexpected DOM tree due to modified by mutation event
+ // listener, editor needs to stop handling it. However, editor shouldn't
+ // return error for the users because Chrome does not throw exception in
+ // this case.
+ case NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE:
+ return NS_OK;
+ // If the editing action is canceled by event listeners, editor needs
+ // to stop handling it. However, editor shouldn't return error for
+ // the callers but they should be able to distinguish whether it's
+ // canceled or not. Although it's DOM specific code, let's return
+ // DOM_SUCCESS_DOM_NO_OPERATION here.
+ case NS_ERROR_EDITOR_ACTION_CANCELED:
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ // If there is no selection range or editable selection ranges, editor
+ // needs to stop handling it. However, editor shouldn't return error for
+ // the callers to avoid throwing exception. However, they may want to
+ // check whether it works or not. Therefore, we should return
+ // NS_SUCCESS_DOM_NO_OPERATION instead.
+ case NS_ERROR_EDITOR_NO_EDITABLE_RANGE:
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ // If CreateNodeResultBase::SuggestCaretPointTo etc is called with
+ // SuggestCaret::AndIgnoreTrivialErrors and CollapseSelectionTo returns
+ // non-critical error e.g., not NS_ERROR_EDITOR_DESTROYED, it returns
+ // this success code instead of actual error code for making the caller
+ // handle the case easier. Therefore, this should be mapped to NS_OK
+ // for the users of editor.
+ case NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR:
+ return NS_OK;
+ default:
+ return aRv;
+ }
+ }
+
+ /**
+ * GetDocumentCharsetInternal() returns charset of the document.
+ */
+ nsresult GetDocumentCharsetInternal(nsACString& aCharset) const;
+
+ /**
+ * ComputeValueInternal() computes string value of this editor for given
+ * format. This may be too expensive if it's in hot path.
+ *
+ * @param aFormatType MIME type like "text/plain".
+ * @param aDocumentEncoderFlags Flags of nsIDocumentEncoder.
+ * @param aCharset Encoding of the document.
+ */
+ nsresult ComputeValueInternal(const nsAString& aFormatType,
+ uint32_t aDocumentEncoderFlags,
+ nsAString& aOutputString) const;
+
+ /**
+ * GetAndInitDocEncoder() returns a document encoder instance for aFormatType
+ * after initializing it. The result may be cached for saving recreation
+ * cost.
+ *
+ * @param aFormatType MIME type like "text/plain".
+ * @param aDocumentEncoderFlags Flags of nsIDocumentEncoder.
+ * @param aCharset Encoding of the document.
+ */
+ already_AddRefed<nsIDocumentEncoder> GetAndInitDocEncoder(
+ const nsAString& aFormatType, uint32_t aDocumentEncoderFlags,
+ const nsACString& aCharset) const;
+
+ /**
+ * EnsurePaddingBRElementInMultilineEditor() creates a padding `<br>` element
+ * at end of multiline text editor.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ EnsurePaddingBRElementInMultilineEditor();
+
+ /**
+ * SelectAllInternal() should be used instead of SelectAll() in editor
+ * because SelectAll() creates AutoEditActionSetter but we should avoid
+ * to create it as far as possible.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult SelectAllInternal();
+
+ nsresult DetermineCurrentDirection();
+
+ /**
+ * DispatchInputEvent() dispatches an "input" event synchronously or
+ * asynchronously if it's not safe to dispatch.
+ */
+ MOZ_CAN_RUN_SCRIPT void DispatchInputEvent();
+
+ /**
+ * Called after a transaction is done successfully.
+ */
+ MOZ_CAN_RUN_SCRIPT void DoAfterDoTransaction(nsITransaction* aTransaction);
+
+ /**
+ * Called after a transaction is undone successfully.
+ */
+
+ MOZ_CAN_RUN_SCRIPT void DoAfterUndoTransaction();
+
+ /**
+ * Called after a transaction is redone successfully.
+ */
+ MOZ_CAN_RUN_SCRIPT void DoAfterRedoTransaction();
+
+ /**
+ * Tell the doc state listeners that the doc state has changed.
+ */
+ enum TDocumentListenerNotification {
+ eDocumentCreated,
+ eDocumentToBeDestroyed,
+ eDocumentStateChanged
+ };
+ MOZ_CAN_RUN_SCRIPT nsresult
+ NotifyDocumentListeners(TDocumentListenerNotification aNotificationType);
+
+ /**
+ * Make the given selection span the entire document.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult SelectEntireDocument() = 0;
+
+ /**
+ * Helper method for scrolling the selection into view after
+ * an edit operation.
+ *
+ * Editor methods *should* call this method instead of the versions
+ * in the various selection interfaces, since this makes sure that
+ * the editor's sync/async settings for reflowing, painting, and scrolling
+ * match.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ ScrollSelectionFocusIntoView() const;
+
+ virtual nsresult InstallEventListeners();
+ virtual void CreateEventListeners();
+ void RemoveEventListeners();
+ [[nodiscard]] bool IsListeningToEvents() const;
+
+ /**
+ * Called if and only if this editor is in readonly mode.
+ */
+ void HandleKeyPressEventInReadOnlyMode(
+ WidgetKeyboardEvent& aKeyboardEvent) const;
+
+ /**
+ * Get the input event target. This might return null.
+ */
+ virtual already_AddRefed<Element> GetInputEventTargetElement() const = 0;
+
+ /**
+ * Return true if spellchecking should be enabled for this editor.
+ */
+ [[nodiscard]] bool GetDesiredSpellCheckState();
+
+ [[nodiscard]] bool CanEnableSpellCheck() const {
+ // Check for password/readonly/disabled, which are not spellchecked
+ // regardless of DOM. Also, check to see if spell check should be skipped
+ // or not.
+ return !IsPasswordEditor() && !IsReadonly() && !ShouldSkipSpellCheck();
+ }
+
+ /**
+ * InitializeSelectionAncestorLimit() is called by InitializeSelection().
+ * When this is called, each implementation has to call
+ * Selection::SetAncestorLimiter() with aAnotherLimit.
+ *
+ * @param aAncestorLimit New ancestor limit of Selection. This always
+ * has parent node. So, it's always safe to
+ * call SetAncestorLimit() with this node.
+ */
+ virtual void InitializeSelectionAncestorLimit(
+ nsIContent& aAncestorLimit) const;
+
+ /**
+ * Initializes selection and caret for the editor at getting focus. If
+ * aOriginalEventTargetNode isn't a host of the editor, i.e., the editor
+ * doesn't get focus, this does nothing.
+ *
+ * @param aOriginalEventTargetNode The original event target node of the
+ * focus event.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ InitializeSelection(const nsINode& aOriginalEventTargetNode);
+
+ enum NotificationForEditorObservers {
+ eNotifyEditorObserversOfEnd,
+ eNotifyEditorObserversOfBefore,
+ eNotifyEditorObserversOfCancel
+ };
+ MOZ_CAN_RUN_SCRIPT void NotifyEditorObservers(
+ NotificationForEditorObservers aNotification);
+
+ /**
+ * HowToHandleCollapsedRange indicates how collapsed range should be treated.
+ */
+ enum class HowToHandleCollapsedRange {
+ // Ignore collapsed range.
+ Ignore,
+ // Extend collapsed range for removing previous content.
+ ExtendBackward,
+ // Extend collapsed range for removing next content.
+ ExtendForward,
+ };
+
+ static HowToHandleCollapsedRange HowToHandleCollapsedRangeFor(
+ nsIEditor::EDirection aDirectionAndAmount) {
+ switch (aDirectionAndAmount) {
+ case nsIEditor::eNone:
+ return HowToHandleCollapsedRange::Ignore;
+ case nsIEditor::ePrevious:
+ return HowToHandleCollapsedRange::ExtendBackward;
+ case nsIEditor::eNext:
+ return HowToHandleCollapsedRange::ExtendForward;
+ case nsIEditor::ePreviousWord:
+ case nsIEditor::eNextWord:
+ case nsIEditor::eToBeginningOfLine:
+ case nsIEditor::eToEndOfLine:
+ // If the amount is word or
+ // line,`AutoRangeArray::ExtendAnchorFocusRangeFor()` must have already
+ // been extended collapsed ranges before.
+ return HowToHandleCollapsedRange::Ignore;
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid nsIEditor::EDirection value");
+ return HowToHandleCollapsedRange::Ignore;
+ }
+
+ /**
+ * InsertDroppedDataTransferAsAction() inserts all data items in aDataTransfer
+ * at aDroppedAt unless the editor is destroyed.
+ *
+ * @param aEditActionData The edit action data whose edit action must be
+ * EditAction::eDrop.
+ * @param aDataTransfer The data transfer object which is dropped.
+ * @param aDroppedAt The DOM tree position whether aDataTransfer
+ * is dropped.
+ * @param aSourcePrincipal Principal of the source of the drag.
+ * May be nullptr if it comes from another app
+ * or process.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual nsresult
+ InsertDroppedDataTransferAsAction(AutoEditActionDataSetter& aEditActionData,
+ dom::DataTransfer& aDataTransfer,
+ const EditorDOMPoint& aDroppedAt,
+ nsIPrincipal* aSourcePrincipal) = 0;
+
+ /**
+ * DeleteSelectionByDragAsAction() removes selection and dispatch "input"
+ * event whose inputType is "deleteByDrag".
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteSelectionByDragAsAction(bool aDispatchInputEvent);
+
+ /**
+ * DeleteSelectionWithTransaction() removes selected content or content
+ * around caret with transactions and remove empty inclusive ancestor
+ * inline elements of collapsed selection after removing the contents.
+ *
+ * @param aDirectionAndAmount How much range should be removed.
+ * @param aStripWrappers Whether the parent blocks should be removed
+ * when they become empty.
+ * Note that this must be `nsIEditor::eNoStrip`
+ * if this is a TextEditor because anyway it'll
+ * be ignored.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteSelectionWithTransaction(nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers);
+
+ /**
+ * DeleteRangesWithTransaction() removes content in aRangesToDelete or content
+ * around collapsed ranges in aRangesToDelete with transactions and remove
+ * empty inclusive ancestor inline elements of collapsed ranges after
+ * removing the contents.
+ *
+ * @param aDirectionAndAmount How much range should be removed.
+ * @param aStripWrappers Whether the parent blocks should be removed
+ * when they become empty.
+ * Note that this must be `nsIEditor::eNoStrip`
+ * if this is a TextEditor because anyway it'll
+ * be ignored.
+ * @param aRangesToDelete The ranges to delete content.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ DeleteRangesWithTransaction(nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ const AutoRangeArray& aRangesToDelete);
+
+ /**
+ * Create a transaction for delete the content in aRangesToDelete.
+ * The result may include DeleteRangeTransaction (for deleting non-collapsed
+ * range), DeleteNodeTransactions and DeleteTextTransactions (for deleting
+ * collapsed range) as its children.
+ *
+ * @param aHowToHandleCollapsedRange
+ * How to handle collapsed ranges.
+ * @param aRangesToDelete The ranges to delete content.
+ */
+ already_AddRefed<DeleteMultipleRangesTransaction>
+ CreateTransactionForDeleteSelection(
+ HowToHandleCollapsedRange aHowToHandleCollapsedRange,
+ const AutoRangeArray& aRangesToDelete);
+
+ /**
+ * Create a DeleteNodeTransaction or DeleteTextTransaction for removing a
+ * nodes or some text around aRangeToDelete.
+ *
+ * @param aCollapsedRange The range to be removed. This must be
+ * collapsed.
+ * @param aHowToHandleCollapsedRange
+ * How to handle aCollapsedRange. Must
+ * be HowToHandleCollapsedRange::ExtendBackward or
+ * HowToHandleCollapsedRange::ExtendForward.
+ */
+ already_AddRefed<DeleteContentTransactionBase>
+ CreateTransactionForCollapsedRange(
+ const nsRange& aCollapsedRange,
+ HowToHandleCollapsedRange aHowToHandleCollapsedRange);
+
+ /**
+ * ComputeInsertedRange() returns actual range modified by inserting string
+ * in a text node. If mutation event listener changed the text data, this
+ * returns a range which covers all over the text data.
+ */
+ std::tuple<EditorDOMPointInText, EditorDOMPointInText> ComputeInsertedRange(
+ const EditorDOMPointInText& aInsertedPoint,
+ const nsAString& aInsertedString) const;
+
+ /**
+ * EnsureComposition() should be called by composition event handlers. This
+ * tries to get the composition for the event and set it to mComposition.
+ * However, this may fail because the composition may be committed before
+ * the event comes to the editor.
+ *
+ * @return true if there is a composition. Otherwise, for example,
+ * a composition event handler in web contents moved focus
+ * for committing the composition, returns false.
+ */
+ bool EnsureComposition(WidgetCompositionEvent& aCompositionEvent);
+
+ /**
+ * See comment of IsCopyToClipboardAllowed() for the detail.
+ */
+ virtual bool IsCopyToClipboardAllowedInternal() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ return !SelectionRef().IsCollapsed();
+ }
+
+ /**
+ * Helper for Is{Cut|Copy}CommandEnabled.
+ * Look for a listener for the given command, including up the target chain.
+ */
+ MOZ_CAN_RUN_SCRIPT bool CheckForClipboardCommandListener(
+ nsAtom* aCommand, EventMessage aEventMessage) const;
+
+ /**
+ * DispatchClipboardEventAndUpdateClipboard() may dispatch a clipboard event
+ * and update clipboard if aEventMessage is eCopy or eCut.
+ *
+ * @param aEventMessage The event message which may be set to the
+ * dispatching event.
+ * @param aClipboardType Working with global clipboard or selection.
+ */
+ enum class ClipboardEventResult {
+ // We have met an error in nsCopySupport::FireClipboardEvent,
+ // or, default of dispatched event is NOT prevented, the event is "cut"
+ // and the event target is not editable.
+ IgnoredOrError,
+ // A "paste" event is dispatched and prevented its default.
+ DefaultPreventedOfPaste,
+ // Default of a "copy" or "cut" event is prevented but the clipboard is
+ // updated unless the dataTransfer of the event is cleared by the listener.
+ // Or, default of the event is NOT prevented but selection is collapsed
+ // when the event target is editable or the event is "copy".
+ CopyOrCutHandled,
+ // A clipboard event is maybe dispatched and not canceled by the web app.
+ // In this case, the clipboard has been updated if aEventMessage is eCopy
+ // or eCut.
+ DoDefault,
+ };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<ClipboardEventResult, nsresult>
+ DispatchClipboardEventAndUpdateClipboard(EventMessage aEventMessage,
+ int32_t aClipboardType);
+
+ /**
+ * Called after PasteAsAction() dispatches "paste" event and it's not
+ * canceled.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual nsresult HandlePaste(
+ AutoEditActionDataSetter& aEditActionData, int32_t aClipboardType) = 0;
+
+ /**
+ * Called after PasteAsQuotationAsAction() dispatches "paste" event and it's
+ * not canceled.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual nsresult HandlePasteAsQuotation(
+ AutoEditActionDataSetter& aEditActionData, int32_t aClipboardType) = 0;
+
+ /**
+ * Called after PasteTransferableAsAction() dispatches "paste" event and it's
+ * not canceled.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual nsresult HandlePasteTransferable(
+ AutoEditActionDataSetter& aEditActionData,
+ nsITransferable& aTransferable) = 0;
+
+ private:
+ nsCOMPtr<nsISelectionController> mSelectionController;
+ RefPtr<Document> mDocument;
+
+ AutoEditActionDataSetter* mEditActionData;
+
+ /**
+ * SetTextDirectionTo() sets text-direction of the root element.
+ * Should use SwitchTextDirectionTo() or ToggleTextDirection() instead.
+ * This is a helper class of them.
+ */
+ nsresult SetTextDirectionTo(TextDirection aTextDirection);
+
+ protected: // helper classes which may be used by friends
+ /**
+ * Stack based helper class for batching a collection of transactions inside
+ * a placeholder transaction. Different from AutoTransactionBatch, this
+ * notifies editor observers of before/end edit action handling, and
+ * dispatches "input" event if it's necessary.
+ */
+ class MOZ_RAII AutoPlaceholderBatch final {
+ public:
+ /**
+ * @param aRequesterFuncName function name which wants to end the batch.
+ * This won't be stored nor exposed to selection listeners etc, used only
+ * for logging. This MUST be alive when the destructor runs.
+ */
+ AutoPlaceholderBatch(EditorBase& aEditorBase,
+ ScrollSelectionIntoView aScrollSelectionIntoView,
+ const char* aRequesterFuncName)
+ : mEditorBase(aEditorBase),
+ mScrollSelectionIntoView(aScrollSelectionIntoView),
+ mRequesterFuncName(aRequesterFuncName) {
+ mEditorBase->BeginPlaceholderTransaction(*nsGkAtoms::_empty,
+ mRequesterFuncName);
+ }
+
+ AutoPlaceholderBatch(EditorBase& aEditorBase,
+ nsStaticAtom& aTransactionName,
+ ScrollSelectionIntoView aScrollSelectionIntoView,
+ const char* aRequesterFuncName)
+ : mEditorBase(aEditorBase),
+ mScrollSelectionIntoView(aScrollSelectionIntoView),
+ mRequesterFuncName(aRequesterFuncName) {
+ mEditorBase->BeginPlaceholderTransaction(aTransactionName,
+ mRequesterFuncName);
+ }
+
+ ~AutoPlaceholderBatch() {
+ mEditorBase->EndPlaceholderTransaction(mScrollSelectionIntoView,
+ mRequesterFuncName);
+ }
+
+ protected:
+ const OwningNonNull<EditorBase> mEditorBase;
+ const ScrollSelectionIntoView mScrollSelectionIntoView;
+ const char* const mRequesterFuncName;
+ };
+
+ /**
+ * AutoEditSubActionNotifier notifies editor of start to handle
+ * top level edit sub-action and end handling top level edit sub-action.
+ */
+ class MOZ_RAII AutoEditSubActionNotifier final {
+ public:
+ MOZ_CAN_RUN_SCRIPT AutoEditSubActionNotifier(
+ EditorBase& aEditorBase, EditSubAction aEditSubAction,
+ nsIEditor::EDirection aDirection, ErrorResult& aRv)
+ : mEditorBase(aEditorBase), mIsTopLevel(true) {
+ // The top level edit sub action has already be set if this is nested call
+ // XXX Looks like that this is not aware of unexpected nested edit action
+ // handling via selectionchange event listener or mutation event
+ // listener.
+ if (!mEditorBase.GetTopLevelEditSubAction()) {
+ MOZ_KnownLive(mEditorBase)
+ .OnStartToHandleTopLevelEditSubAction(aEditSubAction, aDirection,
+ aRv);
+ } else {
+ mIsTopLevel = false;
+ }
+ mEditorBase.OnStartToHandleEditSubAction();
+ }
+
+ MOZ_CAN_RUN_SCRIPT ~AutoEditSubActionNotifier() {
+ mEditorBase.OnEndHandlingEditSubAction();
+ if (mIsTopLevel) {
+ MOZ_KnownLive(mEditorBase).OnEndHandlingTopLevelEditSubAction();
+ }
+ }
+
+ protected:
+ EditorBase& mEditorBase;
+ bool mIsTopLevel;
+ };
+
+ /**
+ * Stack based helper class for turning off active selection adjustment
+ * by low level transactions
+ */
+ class MOZ_RAII AutoTransactionsConserveSelection final {
+ public:
+ explicit AutoTransactionsConserveSelection(EditorBase& aEditorBase)
+ : mEditorBase(aEditorBase),
+ mAllowedTransactionsToChangeSelection(
+ aEditorBase.AllowsTransactionsToChangeSelection()) {
+ mEditorBase.MakeThisAllowTransactionsToChangeSelection(false);
+ }
+
+ ~AutoTransactionsConserveSelection() {
+ mEditorBase.MakeThisAllowTransactionsToChangeSelection(
+ mAllowedTransactionsToChangeSelection);
+ }
+
+ protected:
+ EditorBase& mEditorBase;
+ bool mAllowedTransactionsToChangeSelection;
+ };
+
+ /***************************************************************************
+ * stack based helper class for batching reflow and paint requests.
+ */
+ class MOZ_RAII AutoUpdateViewBatch final {
+ public:
+ /**
+ * @param aRequesterFuncName function name which wants to end the batch.
+ * This won't be stored nor exposed to selection listeners etc, used only
+ * for logging. This MUST be alive when the destructor runs.
+ */
+ MOZ_CAN_RUN_SCRIPT explicit AutoUpdateViewBatch(
+ EditorBase& aEditorBase, const char* aRequesterFuncName)
+ : mEditorBase(aEditorBase), mRequesterFuncName(aRequesterFuncName) {
+ mEditorBase.BeginUpdateViewBatch(mRequesterFuncName);
+ }
+
+ MOZ_CAN_RUN_SCRIPT ~AutoUpdateViewBatch() {
+ MOZ_KnownLive(mEditorBase).EndUpdateViewBatch(mRequesterFuncName);
+ }
+
+ protected:
+ EditorBase& mEditorBase;
+ const char* const mRequesterFuncName;
+ };
+
+ protected:
+ enum Tristate { eTriUnset, eTriFalse, eTriTrue };
+
+ // MIME type of the doc we are editing.
+ nsString mContentMIMEType;
+
+ RefPtr<mozInlineSpellChecker> mInlineSpellChecker;
+ // Reference to text services document for mInlineSpellChecker.
+ RefPtr<TextServicesDocument> mTextServicesDocument;
+
+ RefPtr<TransactionManager> mTransactionManager;
+ // Cached root node.
+ RefPtr<Element> mRootElement;
+
+ // The form field as an event receiver.
+ nsCOMPtr<dom::EventTarget> mEventTarget;
+ RefPtr<EditorEventListener> mEventListener;
+ // Strong reference to placeholder for begin/end batch purposes.
+ RefPtr<PlaceholderTransaction> mPlaceholderTransaction;
+ // Name of placeholder transaction.
+ nsStaticAtom* mPlaceholderName;
+ // Saved selection state for placeholder transaction batching.
+ mozilla::Maybe<SelectionState> mSelState;
+ // IME composition this is not null between compositionstart and
+ // compositionend.
+ RefPtr<TextComposition> mComposition;
+
+ RefPtr<TextInputListener> mTextInputListener;
+
+ RefPtr<IMEContentObserver> mIMEContentObserver;
+
+ // These members cache last encoder and its type for the performance in
+ // TextEditor::ComputeTextValue() which is the implementation of
+ // `<input>.value` and `<textarea>.value`. See `GetAndInitDocEncoder()`.
+ mutable nsCOMPtr<nsIDocumentEncoder> mCachedDocumentEncoder;
+ mutable nsString mCachedDocumentEncoderType;
+
+ // Listens to all low level actions on the doc.
+ // Edit action listener is currently used by highlighter of the findbar and
+ // the spellchecker. So, we should reserve only 2 items.
+ using AutoActionListenerArray =
+ AutoTArray<OwningNonNull<nsIEditActionListener>, 2>;
+ AutoActionListenerArray mActionListeners;
+ // Listen to overall doc state (dirty or not, just created, etc.).
+ // Document state listener is currently used by FinderHighlighter and
+ // BlueGriffon so that reserving only one is enough.
+ using AutoDocumentStateListenerArray =
+ AutoTArray<OwningNonNull<nsIDocumentStateListener>, 1>;
+ AutoDocumentStateListenerArray mDocStateListeners;
+
+ // Number of modifications (for undo/redo stack).
+ uint32_t mModCount;
+ // Behavior flags. See nsIEditor.idl for the flags we use.
+ uint32_t mFlags;
+
+ int32_t mUpdateCount;
+
+ // Nesting count for batching.
+ int32_t mPlaceholderBatch;
+
+ int32_t mWrapColumn = 0;
+ int32_t mNewlineHandling;
+ int32_t mCaretStyle;
+
+ // -1 = not initialized
+ int8_t mDocDirtyState;
+ // A Tristate value.
+ uint8_t mSpellcheckCheckboxState;
+
+ // If true, initialization was succeeded.
+ bool mInitSucceeded;
+ // If false, transactions should not change Selection even after modifying
+ // the DOM tree.
+ bool mAllowsTransactionsToChangeSelection;
+ // Whether PreDestroy has been called.
+ bool mDidPreDestroy;
+ // Whether PostCreate has been called.
+ bool mDidPostCreate;
+ bool mDispatchInputEvent;
+ // True while the instance is handling an edit sub-action.
+ bool mIsInEditSubAction;
+ // Whether caret is hidden forcibly.
+ bool mHidingCaret;
+ // Whether spellchecker dictionary is initialized after focused.
+ bool mSpellCheckerDictionaryUpdated;
+ // Whether we are an HTML editor class.
+ bool mIsHTMLEditorClass;
+
+ friend class AlignStateAtSelection; // AutoEditActionDataSetter,
+ // ToGenericNSResult
+ friend class AutoRangeArray; // IsSEditActionDataAvailable, SelectionRef
+ friend class CaretPoint; // AllowsTransactionsToChangeSelection,
+ // CollapseSelectionTo
+ friend class CompositionTransaction; // CollapseSelectionTo, DoDeleteText,
+ // DoInsertText, DoReplaceText,
+ // HideCaret, RangeupdaterRef
+ friend class DeleteNodeTransaction; // RangeUpdaterRef
+ friend class DeleteRangeTransaction; // AllowsTransactionsToChangeSelection,
+ // CollapseSelectionTo
+ friend class DeleteTextTransaction; // AllowsTransactionsToChangeSelection,
+ // DoDeleteText, DoInsertText,
+ // RangeUpdaterRef
+ friend class InsertNodeTransaction; // AllowsTransactionsToChangeSelection,
+ // CollapseSelectionTo,
+ // MarkElementDirty, ToGenericNSResult
+ friend class InsertTextTransaction; // AllowsTransactionsToChangeSelection,
+ // CollapseSelectionTo, DoDeleteText,
+ // DoInsertText, RangeUpdaterRef
+ friend class ListElementSelectionState; // AutoEditActionDataSetter,
+ // ToGenericNSResult
+ friend class ListItemElementSelectionState; // AutoEditActionDataSetter,
+ // ToGenericNSResult
+ friend class MoveNodeTransaction; // ToGenericNSResult
+ friend class ParagraphStateAtSelection; // AutoEditActionDataSetter,
+ // ToGenericNSResult
+ friend class PendingStyles; // GetEditAction,
+ // GetFirstSelectionStartPoint,
+ // SelectionRef
+ friend class ReplaceTextTransaction; // AllowsTransactionsToChangeSelection,
+ // CollapseSelectionTo, DoReplaceText,
+ // RangeUpdaterRef
+ friend class SplitNodeTransaction; // ToGenericNSResult
+ friend class WhiteSpaceVisibilityKeeper; // AutoTransactionsConserveSelection
+ friend class nsIEditor; // mIsHTMLEditorClass
+};
+
+} // namespace mozilla
+
+bool nsIEditor::IsTextEditor() const {
+ return !AsEditorBase()->mIsHTMLEditorClass;
+}
+
+bool nsIEditor::IsHTMLEditor() const {
+ return AsEditorBase()->mIsHTMLEditorClass;
+}
+
+mozilla::EditorBase* nsIEditor::AsEditorBase() {
+ return static_cast<mozilla::EditorBase*>(this);
+}
+
+const mozilla::EditorBase* nsIEditor::AsEditorBase() const {
+ return static_cast<const mozilla::EditorBase*>(this);
+}
+
+#endif // #ifndef mozilla_EditorBase_h
diff --git a/editor/libeditor/EditorCommands.cpp b/editor/libeditor/EditorCommands.cpp
new file mode 100644
index 0000000000..beb4f060ad
--- /dev/null
+++ b/editor/libeditor/EditorCommands.cpp
@@ -0,0 +1,981 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditorCommands.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/FlushType.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MozPromise.h" // for mozilla::detail::Any
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Selection.h"
+#include "nsCommandParams.h"
+#include "nsIClipboard.h"
+#include "nsIEditingSession.h"
+#include "nsIPrincipal.h"
+#include "nsISelectionController.h"
+#include "nsITransferable.h"
+#include "nsString.h"
+#include "nsAString.h"
+
+class nsISupports;
+
+#define STATE_ENABLED "state_enabled"
+#define STATE_ATTRIBUTE "state_attribute"
+#define STATE_DATA "state_data"
+
+namespace mozilla {
+
+using detail::Any;
+
+/******************************************************************************
+ * mozilla::EditorCommand
+ ******************************************************************************/
+
+NS_IMPL_ISUPPORTS(EditorCommand, nsIControllerCommand)
+
+NS_IMETHODIMP EditorCommand::IsCommandEnabled(const char* aCommandName,
+ nsISupports* aCommandRefCon,
+ bool* aIsEnabled) {
+ if (NS_WARN_IF(!aCommandName) || NS_WARN_IF(!aIsEnabled)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ EditorBase* editorBase = editor ? editor->AsEditorBase() : nullptr;
+ *aIsEnabled = IsCommandEnabled(GetInternalCommand(aCommandName),
+ MOZ_KnownLive(editorBase));
+ return NS_OK;
+}
+
+NS_IMETHODIMP EditorCommand::DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon) {
+ if (NS_WARN_IF(!aCommandName) || NS_WARN_IF(!aCommandRefCon)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (NS_WARN_IF(!editor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv = DoCommand(GetInternalCommand(aCommandName),
+ MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Failed to do command from nsIControllerCommand::DoCommand()");
+ return rv;
+}
+
+NS_IMETHODIMP EditorCommand::DoCommandParams(const char* aCommandName,
+ nsICommandParams* aParams,
+ nsISupports* aCommandRefCon) {
+ if (NS_WARN_IF(!aCommandName) || NS_WARN_IF(!aCommandRefCon)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (NS_WARN_IF(!editor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCommandParams* params = aParams ? aParams->AsCommandParams() : nullptr;
+ Command command = GetInternalCommand(aCommandName, params);
+ EditorCommandParamType paramType = EditorCommand::GetParamType(command);
+ if (paramType == EditorCommandParamType::None) {
+ nsresult rv = DoCommandParam(
+ command, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Failed to do command from nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+
+ if (Any(paramType & EditorCommandParamType::Bool)) {
+ if (Any(paramType & EditorCommandParamType::StateAttribute)) {
+ Maybe<bool> boolParam = Nothing();
+ if (params) {
+ ErrorResult error;
+ boolParam = Some(params->GetBool(STATE_ATTRIBUTE, error));
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+ }
+ nsresult rv = DoCommandParam(
+ command, boolParam, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Failed to do command from nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+ MOZ_ASSERT_UNREACHABLE("Unexpected state for bool");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ // Special case for MultiStateCommandBase. It allows both CString and String
+ // in STATE_ATTRIBUTE and CString is preferred.
+ if (Any(paramType & EditorCommandParamType::CString) &&
+ Any(paramType & EditorCommandParamType::String)) {
+ if (!params) {
+ nsresult rv =
+ DoCommandParam(command, VoidString(),
+ MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to do command from "
+ "nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+ if (Any(paramType & EditorCommandParamType::StateAttribute)) {
+ nsCString cStringParam;
+ nsresult rv = params->GetCString(STATE_ATTRIBUTE, cStringParam);
+ if (NS_SUCCEEDED(rv)) {
+ NS_ConvertUTF8toUTF16 stringParam(cStringParam);
+ nsresult rv =
+ DoCommandParam(command, stringParam,
+ MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to do command from "
+ "nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+ nsString stringParam;
+ DebugOnly<nsresult> rvIgnored =
+ params->GetString(STATE_ATTRIBUTE, stringParam);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to get string from STATE_ATTRIBUTE");
+ rv = DoCommandParam(command, stringParam,
+ MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to do command from "
+ "nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+ MOZ_ASSERT_UNREACHABLE("Unexpected state for CString/String");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ if (Any(paramType & EditorCommandParamType::CString)) {
+ if (!params) {
+ nsresult rv =
+ DoCommandParam(command, VoidCString(),
+ MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Failed to do command from nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+ if (Any(paramType & EditorCommandParamType::StateAttribute)) {
+ nsCString cStringParam;
+ nsresult rv = params->GetCString(STATE_ATTRIBUTE, cStringParam);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = DoCommandParam(command, cStringParam,
+ MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Failed to do command from nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+ MOZ_ASSERT_UNREACHABLE("Unexpected state for CString");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ if (Any(paramType & EditorCommandParamType::String)) {
+ if (!params) {
+ nsresult rv =
+ DoCommandParam(command, VoidString(),
+ MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Failed to do command from nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+ nsString stringParam;
+ if (Any(paramType & EditorCommandParamType::StateAttribute)) {
+ nsresult rv = params->GetString(STATE_ATTRIBUTE, stringParam);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else if (Any(paramType & EditorCommandParamType::StateData)) {
+ nsresult rv = params->GetString(STATE_DATA, stringParam);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Unexpected state for String");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ nsresult rv = DoCommandParam(
+ command, stringParam, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Failed to do command from nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+
+ if (Any(paramType & EditorCommandParamType::Transferable)) {
+ nsCOMPtr<nsITransferable> transferable;
+ if (params) {
+ nsCOMPtr<nsISupports> supports = params->GetISupports("transferable");
+ transferable = do_QueryInterface(supports);
+ }
+ nsresult rv = DoCommandParam(
+ command, transferable, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Failed to do command from nsIControllerCommand::DoCommandParams()");
+ return rv;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unexpected param type");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP EditorCommand::GetCommandStateParams(
+ const char* aCommandName, nsICommandParams* aParams,
+ nsISupports* aCommandRefCon) {
+ if (NS_WARN_IF(!aCommandName) || NS_WARN_IF(!aParams)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
+ if (editor) {
+ return GetCommandStateParams(GetInternalCommand(aCommandName),
+ MOZ_KnownLive(*aParams->AsCommandParams()),
+ MOZ_KnownLive(editor->AsEditorBase()),
+ nullptr);
+ }
+ nsCOMPtr<nsIEditingSession> editingSession =
+ do_QueryInterface(aCommandRefCon);
+ if (editingSession) {
+ return GetCommandStateParams(GetInternalCommand(aCommandName),
+ MOZ_KnownLive(*aParams->AsCommandParams()),
+ nullptr, editingSession);
+ }
+ return GetCommandStateParams(GetInternalCommand(aCommandName),
+ MOZ_KnownLive(*aParams->AsCommandParams()),
+ nullptr, nullptr);
+}
+
+/******************************************************************************
+ * mozilla::UndoCommand
+ ******************************************************************************/
+
+StaticRefPtr<UndoCommand> UndoCommand::sInstance;
+
+bool UndoCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable() && aEditorBase->CanUndo();
+}
+
+nsresult UndoCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ nsresult rv = aEditorBase.UndoAsAction(1, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::UndoAsAction() failed");
+ return rv;
+}
+
+nsresult UndoCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::RedoCommand
+ ******************************************************************************/
+
+StaticRefPtr<RedoCommand> RedoCommand::sInstance;
+
+bool RedoCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable() && aEditorBase->CanRedo();
+}
+
+nsresult RedoCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ nsresult rv = aEditorBase.RedoAsAction(1, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::RedoAsAction() failed");
+ return rv;
+}
+
+nsresult RedoCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::CutCommand
+ ******************************************************************************/
+
+StaticRefPtr<CutCommand> CutCommand::sInstance;
+
+bool CutCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable() &&
+ aEditorBase->IsCutCommandEnabled();
+}
+
+nsresult CutCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ nsresult rv = aEditorBase.CutAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CutAsAction() failed");
+ return rv;
+}
+
+nsresult CutCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::CutOrDeleteCommand
+ ******************************************************************************/
+
+StaticRefPtr<CutOrDeleteCommand> CutOrDeleteCommand::sInstance;
+
+bool CutOrDeleteCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable();
+}
+
+nsresult CutOrDeleteCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ dom::Selection* selection = aEditorBase.GetSelection();
+ if (selection && selection->IsCollapsed()) {
+ nsresult rv = aEditorBase.DeleteSelectionAsAction(
+ nsIEditor::eNext, nsIEditor::eStrip, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsAction() failed");
+ return rv;
+ }
+ nsresult rv = aEditorBase.CutAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CutAsAction() failed");
+ return rv;
+}
+
+nsresult CutOrDeleteCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::CopyCommand
+ ******************************************************************************/
+
+StaticRefPtr<CopyCommand> CopyCommand::sInstance;
+
+bool CopyCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsCopyCommandEnabled();
+}
+
+nsresult CopyCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ // Shouldn't cause "beforeinput" event so that we don't need to specify
+ // the given principal.
+ return aEditorBase.Copy();
+}
+
+nsresult CopyCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::CopyOrDeleteCommand
+ ******************************************************************************/
+
+StaticRefPtr<CopyOrDeleteCommand> CopyOrDeleteCommand::sInstance;
+
+bool CopyOrDeleteCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable();
+}
+
+nsresult CopyOrDeleteCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ dom::Selection* selection = aEditorBase.GetSelection();
+ if (selection && selection->IsCollapsed()) {
+ nsresult rv = aEditorBase.DeleteSelectionAsAction(
+ nsIEditor::eNextWord, nsIEditor::eStrip, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsAction() failed");
+ return rv;
+ }
+ // Shouldn't cause "beforeinput" event so that we don't need to specify
+ // the given principal.
+ nsresult rv = aEditorBase.Copy();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::Copy() failed");
+ return rv;
+}
+
+nsresult CopyOrDeleteCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::PasteCommand
+ ******************************************************************************/
+
+StaticRefPtr<PasteCommand> PasteCommand::sInstance;
+
+bool PasteCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable() &&
+ aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard);
+}
+
+nsresult PasteCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ nsresult rv = aEditorBase.PasteAsAction(nsIClipboard::kGlobalClipboard,
+ EditorBase::DispatchPasteEvent::Yes,
+ aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::PasteAsAction(nsIClipboard::"
+ "kGlobalClipboard, DispatchPasteEvent::Yes) failed");
+ return rv;
+}
+
+nsresult PasteCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::PasteTransferableCommand
+ ******************************************************************************/
+
+StaticRefPtr<PasteTransferableCommand> PasteTransferableCommand::sInstance;
+
+bool PasteTransferableCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable() &&
+ aEditorBase->CanPasteTransferable(nullptr);
+}
+
+nsresult PasteTransferableCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ return NS_ERROR_FAILURE;
+}
+
+nsresult PasteTransferableCommand::DoCommandParam(
+ Command aCommand, nsITransferable* aTransferableParam,
+ EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(!aTransferableParam)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv = aEditorBase.PasteTransferableAsAction(
+ aTransferableParam, EditorBase::DispatchPasteEvent::Yes, aPrincipal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::PasteTransferableAsAction(DispatchPasteEvent::Yes) failed");
+ return rv;
+}
+
+nsresult PasteTransferableCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ if (NS_WARN_IF(!aEditorBase)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsISupports> supports = aParams.GetISupports("transferable");
+ if (NS_WARN_IF(!supports)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsITransferable> trans;
+ trans = do_QueryInterface(supports);
+ if (NS_WARN_IF(!trans)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return aParams.SetBool(STATE_ENABLED,
+ aEditorBase->CanPasteTransferable(trans));
+}
+
+/******************************************************************************
+ * mozilla::SwitchTextDirectionCommand
+ ******************************************************************************/
+
+StaticRefPtr<SwitchTextDirectionCommand> SwitchTextDirectionCommand::sInstance;
+
+bool SwitchTextDirectionCommand::IsCommandEnabled(
+ Command aCommand, EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable();
+}
+
+nsresult SwitchTextDirectionCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ nsresult rv = aEditorBase.ToggleTextDirectionAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::ToggleTextDirectionAsAction() failed");
+ return rv;
+}
+
+nsresult SwitchTextDirectionCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::DeleteCommand
+ ******************************************************************************/
+
+StaticRefPtr<DeleteCommand> DeleteCommand::sInstance;
+
+bool DeleteCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ // We can generally delete whenever the selection is editable. However,
+ // cmd_delete doesn't make sense if the selection is collapsed because it's
+ // directionless.
+ bool isEnabled = aEditorBase->IsSelectionEditable();
+
+ if (aCommand == Command::Delete && isEnabled) {
+ return aEditorBase->CanDeleteSelection();
+ }
+ return isEnabled;
+}
+
+nsresult DeleteCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ nsIEditor::EDirection deleteDir = nsIEditor::eNone;
+ switch (aCommand) {
+ case Command::Delete:
+ // Really this should probably be eNone, but it only makes a difference
+ // if the selection is collapsed, and then this command is disabled. So
+ // let's keep it as it always was to avoid breaking things.
+ deleteDir = nsIEditor::ePrevious;
+ break;
+ case Command::DeleteCharForward:
+ deleteDir = nsIEditor::eNext;
+ break;
+ case Command::DeleteCharBackward:
+ deleteDir = nsIEditor::ePrevious;
+ break;
+ case Command::DeleteWordBackward:
+ deleteDir = nsIEditor::ePreviousWord;
+ break;
+ case Command::DeleteWordForward:
+ deleteDir = nsIEditor::eNextWord;
+ break;
+ case Command::DeleteToBeginningOfLine:
+ deleteDir = nsIEditor::eToBeginningOfLine;
+ break;
+ case Command::DeleteToEndOfLine:
+ deleteDir = nsIEditor::eToEndOfLine;
+ break;
+ default:
+ MOZ_CRASH("Unrecognized nsDeleteCommand");
+ }
+ nsresult rv = aEditorBase.DeleteSelectionAsAction(
+ deleteDir, nsIEditor::eStrip, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsAction() failed");
+ return rv;
+}
+
+nsresult DeleteCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::SelectAllCommand
+ ******************************************************************************/
+
+StaticRefPtr<SelectAllCommand> SelectAllCommand::sInstance;
+
+bool SelectAllCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ // You can always select all, unless the selection is editable,
+ // and the editable region is empty!
+ if (!aEditorBase) {
+ return true;
+ }
+
+ // You can select all if there is an editor which is non-empty
+ return !aEditorBase->IsEmpty();
+}
+
+nsresult SelectAllCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ // Shouldn't cause "beforeinput" event so that we don't need to specify
+ // aPrincipal.
+ nsresult rv = aEditorBase.SelectAll();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::SelectAll() failed");
+ return rv;
+}
+
+nsresult SelectAllCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::SelectionMoveCommands
+ ******************************************************************************/
+
+StaticRefPtr<SelectionMoveCommands> SelectionMoveCommands::sInstance;
+
+bool SelectionMoveCommands::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable();
+}
+
+static const struct ScrollCommand {
+ Command mReverseScroll;
+ Command mForwardScroll;
+ nsresult (NS_STDCALL nsISelectionController::*scroll)(bool);
+} scrollCommands[] = {{Command::ScrollTop, Command::ScrollBottom,
+ &nsISelectionController::CompleteScroll},
+ {Command::ScrollPageUp, Command::ScrollPageDown,
+ &nsISelectionController::ScrollPage},
+ {Command::ScrollLineUp, Command::ScrollLineDown,
+ &nsISelectionController::ScrollLine}};
+
+static const struct MoveCommand {
+ Command mReverseMove;
+ Command mForwardMove;
+ Command mReverseSelect;
+ Command mForwardSelect;
+ nsresult (NS_STDCALL nsISelectionController::*move)(bool, bool);
+} moveCommands[] = {
+ {Command::CharPrevious, Command::CharNext, Command::SelectCharPrevious,
+ Command::SelectCharNext, &nsISelectionController::CharacterMove},
+ {Command::LinePrevious, Command::LineNext, Command::SelectLinePrevious,
+ Command::SelectLineNext, &nsISelectionController::LineMove},
+ {Command::WordPrevious, Command::WordNext, Command::SelectWordPrevious,
+ Command::SelectWordNext, &nsISelectionController::WordMove},
+ {Command::BeginLine, Command::EndLine, Command::SelectBeginLine,
+ Command::SelectEndLine, &nsISelectionController::IntraLineMove},
+ {Command::MovePageUp, Command::MovePageDown, Command::SelectPageUp,
+ Command::SelectPageDown, &nsISelectionController::PageMove},
+ {Command::MoveTop, Command::MoveBottom, Command::SelectTop,
+ Command::SelectBottom, &nsISelectionController::CompleteMove}};
+
+static const struct PhysicalCommand {
+ Command mMove;
+ Command mSelect;
+ int16_t direction;
+ int16_t amount;
+} physicalCommands[] = {
+ {Command::MoveLeft, Command::SelectLeft, nsISelectionController::MOVE_LEFT,
+ 0},
+ {Command::MoveRight, Command::SelectRight,
+ nsISelectionController::MOVE_RIGHT, 0},
+ {Command::MoveUp, Command::SelectUp, nsISelectionController::MOVE_UP, 0},
+ {Command::MoveDown, Command::SelectDown, nsISelectionController::MOVE_DOWN,
+ 0},
+ {Command::MoveLeft2, Command::SelectLeft2,
+ nsISelectionController::MOVE_LEFT, 1},
+ {Command::MoveRight2, Command::SelectRight2,
+ nsISelectionController::MOVE_RIGHT, 1},
+ {Command::MoveUp2, Command::SelectUp2, nsISelectionController::MOVE_UP, 1},
+ {Command::MoveDown2, Command::SelectDown2,
+ nsISelectionController::MOVE_DOWN, 1}};
+
+nsresult SelectionMoveCommands::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ RefPtr<dom::Document> document = aEditorBase.GetDocument();
+ if (document) {
+ // Most of the commands below (possibly all of them) need layout to
+ // be up to date.
+ document->FlushPendingNotifications(FlushType::Layout);
+ }
+
+ nsCOMPtr<nsISelectionController> selectionController =
+ aEditorBase.GetSelectionController();
+ if (NS_WARN_IF(!selectionController)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // scroll commands
+ for (size_t i = 0; i < ArrayLength(scrollCommands); i++) {
+ const ScrollCommand& cmd = scrollCommands[i];
+ if (aCommand == cmd.mReverseScroll) {
+ return (selectionController->*(cmd.scroll))(false);
+ }
+ if (aCommand == cmd.mForwardScroll) {
+ return (selectionController->*(cmd.scroll))(true);
+ }
+ }
+
+ // caret movement/selection commands
+ for (size_t i = 0; i < ArrayLength(moveCommands); i++) {
+ const MoveCommand& cmd = moveCommands[i];
+ if (aCommand == cmd.mReverseMove) {
+ return (selectionController->*(cmd.move))(false, false);
+ }
+ if (aCommand == cmd.mForwardMove) {
+ return (selectionController->*(cmd.move))(true, false);
+ }
+ if (aCommand == cmd.mReverseSelect) {
+ return (selectionController->*(cmd.move))(false, true);
+ }
+ if (aCommand == cmd.mForwardSelect) {
+ return (selectionController->*(cmd.move))(true, true);
+ }
+ }
+
+ // physical-direction movement/selection
+ for (size_t i = 0; i < ArrayLength(physicalCommands); i++) {
+ const PhysicalCommand& cmd = physicalCommands[i];
+ if (aCommand == cmd.mMove) {
+ nsresult rv =
+ selectionController->PhysicalMove(cmd.direction, cmd.amount, false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "nsISelectionController::PhysicalMove() failed to move caret");
+ return rv;
+ }
+ if (aCommand == cmd.mSelect) {
+ nsresult rv =
+ selectionController->PhysicalMove(cmd.direction, cmd.amount, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "nsISelectionController::PhysicalMove() failed to select");
+ return rv;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult SelectionMoveCommands::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::InsertPlaintextCommand
+ ******************************************************************************/
+
+StaticRefPtr<InsertPlaintextCommand> InsertPlaintextCommand::sInstance;
+
+bool InsertPlaintextCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable();
+}
+
+nsresult InsertPlaintextCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ // XXX InsertTextAsAction() is not same as OnInputText(). However, other
+ // commands to insert line break or paragraph separator use OnInput*().
+ // According to the semantics of those methods, using *AsAction() is
+ // better, however, this may not cause two or more placeholder
+ // transactions to the top transaction since its name may not be
+ // nsGkAtoms::TypingTxnName.
+ DebugOnly<nsresult> rvIgnored =
+ aEditorBase.InsertTextAsAction(u""_ns, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::InsertTextAsAction() failed, but ignored");
+ return NS_OK;
+}
+
+nsresult InsertPlaintextCommand::DoCommandParam(
+ Command aCommand, const nsAString& aStringParam, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(aStringParam.IsVoid())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // XXX InsertTextAsAction() is not same as OnInputText(). However, other
+ // commands to insert line break or paragraph separator use OnInput*().
+ // According to the semantics of those methods, using *AsAction() is
+ // better, however, this may not cause two or more placeholder
+ // transactions to the top transaction since its name may not be
+ // nsGkAtoms::TypingTxnName.
+ DebugOnly<nsresult> rvIgnored =
+ aEditorBase.InsertTextAsAction(aStringParam, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::InsertTextAsAction() failed, but ignored");
+ return NS_OK;
+}
+
+nsresult InsertPlaintextCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::InsertParagraphCommand
+ ******************************************************************************/
+
+StaticRefPtr<InsertParagraphCommand> InsertParagraphCommand::sInstance;
+
+bool InsertParagraphCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase || aEditorBase->IsSingleLineEditor()) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable();
+}
+
+nsresult InsertParagraphCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ if (aEditorBase.IsSingleLineEditor()) {
+ return NS_ERROR_FAILURE;
+ }
+ if (aEditorBase.IsHTMLEditor()) {
+ nsresult rv = MOZ_KnownLive(aEditorBase.AsHTMLEditor())
+ ->InsertParagraphSeparatorAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertParagraphSeparatorAsAction() failed");
+ return rv;
+ }
+ nsresult rv = aEditorBase.InsertLineBreakAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertLineBreakAsAction() failed");
+ return rv;
+}
+
+nsresult InsertParagraphCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::InsertLineBreakCommand
+ ******************************************************************************/
+
+StaticRefPtr<InsertLineBreakCommand> InsertLineBreakCommand::sInstance;
+
+bool InsertLineBreakCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase || aEditorBase->IsSingleLineEditor()) {
+ return false;
+ }
+ return aEditorBase->IsSelectionEditable();
+}
+
+nsresult InsertLineBreakCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ if (aEditorBase.IsSingleLineEditor()) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = aEditorBase.InsertLineBreakAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertLineBreakAsAction() failed");
+ return rv;
+}
+
+nsresult InsertLineBreakCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/******************************************************************************
+ * mozilla::PasteQuotationCommand
+ ******************************************************************************/
+
+StaticRefPtr<PasteQuotationCommand> PasteQuotationCommand::sInstance;
+
+bool PasteQuotationCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ if (!aEditorBase) {
+ return false;
+ }
+ return !aEditorBase->IsSingleLineEditor() &&
+ aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard);
+}
+
+nsresult PasteQuotationCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ nsresult rv = aEditorBase.PasteAsQuotationAsAction(
+ nsIClipboard::kGlobalClipboard, EditorBase::DispatchPasteEvent::Yes,
+ aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::PasteAsQuotationAsAction(nsIClipboard::"
+ "kGlobalClipboard, DispatchPasteEvent::Yes) failed");
+ return rv;
+}
+
+nsresult PasteQuotationCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ if (!aEditorBase) {
+ return NS_OK;
+ }
+ aParams.SetBool(STATE_ENABLED,
+ aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard));
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorCommands.h b/editor/libeditor/EditorCommands.h
new file mode 100644
index 0000000000..031f9057e6
--- /dev/null
+++ b/editor/libeditor/EditorCommands.h
@@ -0,0 +1,931 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditorCommands_h
+#define mozilla_EditorCommands_h
+
+#include "mozilla/EditorForwards.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TypedEnumBits.h"
+#include "nsGkAtoms.h"
+#include "nsIControllerCommand.h"
+#include "nsIPrincipal.h"
+#include "nsISupportsImpl.h"
+#include "nsRefPtrHashtable.h"
+#include "nsStringFwd.h"
+
+class nsAtom;
+class nsCommandParams;
+class nsICommandParams;
+class nsIEditingSession;
+class nsITransferable;
+class nsStaticAtom;
+
+namespace mozilla {
+
+/**
+ * EditorCommandParamType tells you that EditorCommand subclasses refer
+ * which type in nsCommandParams (e.g., bool or nsString) or do not refer.
+ * If they refer some types, also set where is in nsCommandParams, e.g.,
+ * whether "state_attribute" or "state_data".
+ */
+enum class EditorCommandParamType : uint16_t {
+ // The command does not take params (even if specified, always ignored).
+ None = 0,
+ // The command refers nsCommandParams::GetBool() result.
+ Bool = 1 << 0,
+ // The command refers nsCommandParams::GetString() result.
+ // This may be specified with CString. In such case,
+ // nsCommandParams::GetCString() is preferred.
+ String = 1 << 1,
+ // The command refers nsCommandParams::GetCString() result.
+ CString = 1 << 2,
+ // The command refers nsCommandParams::GetISupports("transferable") result.
+ Transferable = 1 << 3,
+
+ // The command refres "state_attribute" of nsCommandParams when calling
+ // GetBool()/GetString()/GetCString(). This must not be set when the
+ // type is None or Transferable.
+ StateAttribute = 1 << 14,
+ // The command refers "state_data" of nsCommandParams when calling
+ // GetBool()/GetString()/GetCString(). This must not be set when the
+ // type is None or Transferable.
+ StateData = 1 << 15,
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(EditorCommandParamType)
+
+/**
+ * This is a base class for commands registered with the editor controller.
+ * Note that such commands are designed as singleton classes. So, MUST be
+ * stateless. Any state must be stored via the refCon (an nsIEditor).
+ */
+
+class EditorCommand : public nsIControllerCommand {
+ public:
+ NS_DECL_ISUPPORTS
+
+ static EditorCommandParamType GetParamType(Command aCommand) {
+ // Keep same order of registration in EditorController.cpp and
+ // HTMLEditorController.cpp.
+ switch (aCommand) {
+ // UndoCommand
+ case Command::HistoryUndo:
+ return EditorCommandParamType::None;
+ // RedoCommand
+ case Command::HistoryRedo:
+ return EditorCommandParamType::None;
+ // CutCommand
+ case Command::Cut:
+ return EditorCommandParamType::None;
+ // CutOrDeleteCommand
+ case Command::CutOrDelete:
+ return EditorCommandParamType::None;
+ // CopyCommand
+ case Command::Copy:
+ return EditorCommandParamType::None;
+ // CopyOrDeleteCommand
+ case Command::CopyOrDelete:
+ return EditorCommandParamType::None;
+ // SelectAllCommand
+ case Command::SelectAll:
+ return EditorCommandParamType::None;
+ // PasteCommand
+ case Command::Paste:
+ return EditorCommandParamType::None;
+ case Command::PasteTransferable:
+ return EditorCommandParamType::Transferable;
+ // SwitchTextDirectionCommand
+ case Command::FormatSetBlockTextDirection:
+ return EditorCommandParamType::None;
+ // DeleteCommand
+ case Command::Delete:
+ case Command::DeleteCharBackward:
+ case Command::DeleteCharForward:
+ case Command::DeleteWordBackward:
+ case Command::DeleteWordForward:
+ case Command::DeleteToBeginningOfLine:
+ case Command::DeleteToEndOfLine:
+ return EditorCommandParamType::None;
+ // InsertPlaintextCommand
+ case Command::InsertText:
+ return EditorCommandParamType::String |
+ EditorCommandParamType::StateData;
+ // InsertParagraphCommand
+ case Command::InsertParagraph:
+ return EditorCommandParamType::None;
+ // InsertLineBreakCommand
+ case Command::InsertLineBreak:
+ return EditorCommandParamType::None;
+ // PasteQuotationCommand
+ case Command::PasteAsQuotation:
+ return EditorCommandParamType::None;
+
+ // SelectionMoveCommand
+ case Command::ScrollTop:
+ case Command::ScrollBottom:
+ case Command::MoveTop:
+ case Command::MoveBottom:
+ case Command::SelectTop:
+ case Command::SelectBottom:
+ case Command::LineNext:
+ case Command::LinePrevious:
+ case Command::SelectLineNext:
+ case Command::SelectLinePrevious:
+ case Command::CharPrevious:
+ case Command::CharNext:
+ case Command::SelectCharPrevious:
+ case Command::SelectCharNext:
+ case Command::BeginLine:
+ case Command::EndLine:
+ case Command::SelectBeginLine:
+ case Command::SelectEndLine:
+ case Command::WordPrevious:
+ case Command::WordNext:
+ case Command::SelectWordPrevious:
+ case Command::SelectWordNext:
+ case Command::ScrollPageUp:
+ case Command::ScrollPageDown:
+ case Command::ScrollLineUp:
+ case Command::ScrollLineDown:
+ case Command::MovePageUp:
+ case Command::MovePageDown:
+ case Command::SelectPageUp:
+ case Command::SelectPageDown:
+ case Command::MoveLeft:
+ case Command::MoveRight:
+ case Command::MoveUp:
+ case Command::MoveDown:
+ case Command::MoveLeft2:
+ case Command::MoveRight2:
+ case Command::MoveUp2:
+ case Command::MoveDown2:
+ case Command::SelectLeft:
+ case Command::SelectRight:
+ case Command::SelectUp:
+ case Command::SelectDown:
+ case Command::SelectLeft2:
+ case Command::SelectRight2:
+ case Command::SelectUp2:
+ case Command::SelectDown2:
+ return EditorCommandParamType::None;
+ // PasteNoFormattingCommand
+ case Command::PasteWithoutFormat:
+ return EditorCommandParamType::None;
+
+ // DocumentStateCommand
+ case Command::EditorObserverDocumentCreated:
+ case Command::EditorObserverDocumentLocationChanged:
+ case Command::EditorObserverDocumentWillBeDestroyed:
+ return EditorCommandParamType::None;
+ // SetDocumentStateCommand
+ case Command::SetDocumentModified:
+ case Command::SetDocumentUseCSS:
+ case Command::SetDocumentReadOnly:
+ case Command::SetDocumentInsertBROnEnterKeyPress:
+ return EditorCommandParamType::Bool |
+ EditorCommandParamType::StateAttribute;
+ case Command::SetDocumentDefaultParagraphSeparator:
+ return EditorCommandParamType::CString |
+ EditorCommandParamType::StateAttribute;
+ case Command::ToggleObjectResizers:
+ case Command::ToggleInlineTableEditor:
+ case Command::ToggleAbsolutePositionEditor:
+ case Command::EnableCompatibleJoinSplitNodeDirection:
+ return EditorCommandParamType::Bool |
+ EditorCommandParamType::StateAttribute;
+
+ // IndentCommand
+ case Command::FormatIndent:
+ return EditorCommandParamType::None;
+ // OutdentCommand
+ case Command::FormatOutdent:
+ return EditorCommandParamType::None;
+ // StyleUpdatingCommand
+ case Command::FormatBold:
+ case Command::FormatItalic:
+ case Command::FormatUnderline:
+ case Command::FormatTeletypeText:
+ case Command::FormatStrikeThrough:
+ case Command::FormatSuperscript:
+ case Command::FormatSubscript:
+ case Command::FormatNoBreak:
+ case Command::FormatEmphasis:
+ case Command::FormatStrong:
+ case Command::FormatCitation:
+ case Command::FormatAbbreviation:
+ case Command::FormatAcronym:
+ case Command::FormatCode:
+ case Command::FormatSample:
+ case Command::FormatVariable:
+ case Command::FormatRemoveLink:
+ return EditorCommandParamType::None;
+ // ListCommand
+ case Command::InsertOrderedList:
+ case Command::InsertUnorderedList:
+ return EditorCommandParamType::None;
+ // ListItemCommand
+ case Command::InsertDefinitionTerm:
+ case Command::InsertDefinitionDetails:
+ return EditorCommandParamType::None;
+ // RemoveListCommand
+ case Command::FormatRemoveList:
+ return EditorCommandParamType::None;
+ // ParagraphStateCommand
+ case Command::FormatBlock:
+ case Command::ParagraphState:
+ return EditorCommandParamType::CString |
+ EditorCommandParamType::String |
+ EditorCommandParamType::StateAttribute;
+ // FontFaceStateCommand
+ case Command::FormatFontName:
+ return EditorCommandParamType::CString |
+ EditorCommandParamType::String |
+ EditorCommandParamType::StateAttribute;
+ // FontSizeStateCommand
+ case Command::FormatFontSize:
+ return EditorCommandParamType::CString |
+ EditorCommandParamType::String |
+ EditorCommandParamType::StateAttribute;
+ // FontColorStateCommand
+ case Command::FormatFontColor:
+ return EditorCommandParamType::CString |
+ EditorCommandParamType::String |
+ EditorCommandParamType::StateAttribute;
+ // BackgroundColorStateCommand
+ case Command::FormatDocumentBackgroundColor:
+ return EditorCommandParamType::CString |
+ EditorCommandParamType::String |
+ EditorCommandParamType::StateAttribute;
+ // HighlightColorStateCommand
+ case Command::FormatBackColor:
+ return EditorCommandParamType::CString |
+ EditorCommandParamType::String |
+ EditorCommandParamType::StateAttribute;
+ // AlignCommand:
+ case Command::FormatJustifyLeft:
+ case Command::FormatJustifyRight:
+ case Command::FormatJustifyCenter:
+ case Command::FormatJustifyFull:
+ case Command::FormatJustifyNone:
+ return EditorCommandParamType::CString |
+ EditorCommandParamType::String |
+ EditorCommandParamType::StateAttribute;
+ // RemoveStylesCommand
+ case Command::FormatRemove:
+ return EditorCommandParamType::None;
+ // IncreaseFontSizeCommand
+ case Command::FormatIncreaseFontSize:
+ return EditorCommandParamType::None;
+ // DecreaseFontSizeCommand
+ case Command::FormatDecreaseFontSize:
+ return EditorCommandParamType::None;
+ // InsertHTMLCommand
+ case Command::InsertHTML:
+ return EditorCommandParamType::String |
+ EditorCommandParamType::StateData;
+ // InsertTagCommand
+ case Command::InsertLink:
+ case Command::InsertImage:
+ return EditorCommandParamType::String |
+ EditorCommandParamType::StateAttribute;
+ case Command::InsertHorizontalRule:
+ return EditorCommandParamType::None;
+ // AbsolutePositioningCommand
+ case Command::FormatAbsolutePosition:
+ return EditorCommandParamType::None;
+ // DecreaseZIndexCommand
+ case Command::FormatDecreaseZIndex:
+ return EditorCommandParamType::None;
+ // IncreaseZIndexCommand
+ case Command::FormatIncreaseZIndex:
+ return EditorCommandParamType::None;
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown Command");
+ return EditorCommandParamType::None;
+ }
+ }
+
+ // nsIControllerCommand methods. Use EditorCommand specific methods instead
+ // for internal use.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD
+ IsCommandEnabled(const char* aCommandName, nsISupports* aCommandRefCon,
+ bool* aIsEnabled) final;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD DoCommand(const char* aCommandName,
+ nsISupports* aCommandRefCon) final;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD
+ DoCommandParams(const char* aCommandName, nsICommandParams* aParams,
+ nsISupports* aCommandRefCon) final;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD
+ GetCommandStateParams(const char* aCommandName, nsICommandParams* aParams,
+ nsISupports* aCommandRefCon) final;
+
+ MOZ_CAN_RUN_SCRIPT virtual bool IsCommandEnabled(
+ Command aCommand, EditorBase* aEditorBase) const = 0;
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommand(
+ Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const = 0;
+
+ /**
+ * @param aEditorBase If the context is an editor, should be set to
+ * it. Otherwise, nullptr.
+ * @param aEditingSession If the context is an editing session, should be
+ * set to it. This usually occurs if editor has
+ * not been created yet during initialization.
+ * Otherwise, nullptr.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const = 0;
+
+ /**
+ * Called only when the result of EditorCommand::GetParamType(aCommand) is
+ * EditorCommandParamType::None.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam(
+ Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT_UNREACHABLE("Wrong overload is called");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ /**
+ * Called only when the result of EditorCommand::GetParamType(aCommand)
+ * includes EditorCommandParamType::Bool. If aBoolParam is Nothing, it
+ * means that given param was nullptr.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam(
+ Command aCommand, const Maybe<bool>& aBoolParam, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT_UNREACHABLE("Wrong overload is called");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ /**
+ * Called only when the result of EditorCommand::GetParamType(aCommand)
+ * includes EditorCommandParamType::CString. If aCStringParam is void, it
+ * means that given param was nullptr.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam(
+ Command aCommand, const nsACString& aCStringParam,
+ EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT_UNREACHABLE("Wrong overload is called");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ /**
+ * Called only when the result of EditorCommand::GetParamType(aCommand)
+ * includes EditorCommandParamType::String. If aStringParam is void, it
+ * means that given param was nullptr.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam(
+ Command aCommand, const nsAString& aStringParam, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT_UNREACHABLE("Wrong overload is called");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ /**
+ * Called only when the result of EditorCommand::GetParamType(aCommand) is
+ * EditorCommandParamType::Transferable. If aTransferableParam may be
+ * nullptr.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam(
+ Command aCommand, nsITransferable* aTransferableParam,
+ EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT_UNREACHABLE("Wrong overload is called");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ protected:
+ EditorCommand() = default;
+ virtual ~EditorCommand() = default;
+};
+
+#define NS_DECL_EDITOR_COMMAND_COMMON_METHODS \
+ public: \
+ MOZ_CAN_RUN_SCRIPT virtual bool IsCommandEnabled( \
+ Command aCommand, EditorBase* aEditorBase) const final; \
+ using EditorCommand::IsCommandEnabled; \
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommand( \
+ Command aCommand, EditorBase& aEditorBase, nsIPrincipal* aPrincipal) \
+ const final; \
+ using EditorCommand::DoCommand; \
+ MOZ_CAN_RUN_SCRIPT virtual nsresult GetCommandStateParams( \
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase, \
+ nsIEditingSession* aEditingSession) const final; \
+ using EditorCommand::GetCommandStateParams; \
+ using EditorCommand::DoCommandParam;
+
+#define NS_DECL_DO_COMMAND_PARAM_DELEGATE_TO_DO_COMMAND \
+ public: \
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam( \
+ Command aCommand, EditorBase& aEditorBase, nsIPrincipal* aPrincipal) \
+ const final { \
+ return DoCommand(aCommand, aEditorBase, aPrincipal); \
+ }
+
+#define NS_DECL_DO_COMMAND_PARAM_FOR_BOOL_PARAM \
+ public: \
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam( \
+ Command aCommand, const Maybe<bool>& aBoolParam, \
+ EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const final;
+
+#define NS_DECL_DO_COMMAND_PARAM_FOR_CSTRING_PARAM \
+ public: \
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam( \
+ Command aCommand, const nsACString& aCStringParam, \
+ EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const final;
+
+#define NS_DECL_DO_COMMAND_PARAM_FOR_STRING_PARAM \
+ public: \
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam( \
+ Command aCommand, const nsAString& aStringParam, \
+ EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const final;
+
+#define NS_DECL_DO_COMMAND_PARAM_FOR_TRANSFERABLE_PARAM \
+ public: \
+ MOZ_CAN_RUN_SCRIPT virtual nsresult DoCommandParam( \
+ Command aCommand, nsITransferable* aTransferableParam, \
+ EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const final;
+
+#define NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(_cmd) \
+ public: \
+ static EditorCommand* GetInstance() { \
+ if (!sInstance) { \
+ sInstance = new _cmd(); \
+ } \
+ return sInstance; \
+ } \
+ \
+ static void Shutdown() { sInstance = nullptr; } \
+ \
+ private: \
+ static StaticRefPtr<_cmd> sInstance;
+
+#define NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(_cmd) \
+ class _cmd final : public EditorCommand { \
+ NS_DECL_EDITOR_COMMAND_COMMON_METHODS \
+ NS_DECL_DO_COMMAND_PARAM_DELEGATE_TO_DO_COMMAND \
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(_cmd) \
+ \
+ protected: \
+ _cmd() = default; \
+ virtual ~_cmd() = default; \
+ };
+
+#define NS_DECL_EDITOR_COMMAND_FOR_BOOL_PARAM(_cmd) \
+ class _cmd final : public EditorCommand { \
+ NS_DECL_EDITOR_COMMAND_COMMON_METHODS \
+ NS_DECL_DO_COMMAND_PARAM_FOR_BOOL_PARAM \
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(_cmd) \
+ \
+ protected: \
+ _cmd() = default; \
+ virtual ~_cmd() = default; \
+ };
+
+#define NS_DECL_EDITOR_COMMAND_FOR_CSTRING_PARAM(_cmd) \
+ class _cmd final : public EditorCommand { \
+ NS_DECL_EDITOR_COMMAND_COMMON_METHODS \
+ NS_DECL_DO_COMMAND_PARAM_FOR_CSTRING_PARAM \
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(_cmd) \
+ \
+ protected: \
+ _cmd() = default; \
+ virtual ~_cmd() = default; \
+ };
+
+#define NS_DECL_EDITOR_COMMAND_FOR_STRING_PARAM(_cmd) \
+ class _cmd final : public EditorCommand { \
+ NS_DECL_EDITOR_COMMAND_COMMON_METHODS \
+ NS_DECL_DO_COMMAND_PARAM_FOR_STRING_PARAM \
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(_cmd) \
+ \
+ protected: \
+ _cmd() = default; \
+ virtual ~_cmd() = default; \
+ };
+
+#define NS_DECL_EDITOR_COMMAND_FOR_TRANSFERABLE_PARAM(_cmd) \
+ class _cmd final : public EditorCommand { \
+ NS_DECL_EDITOR_COMMAND_COMMON_METHODS \
+ NS_DECL_DO_COMMAND_PARAM_FOR_TRANSFERABLE_PARAM \
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(_cmd) \
+ \
+ protected: \
+ _cmd() = default; \
+ virtual ~_cmd() = default; \
+ };
+
+// basic editor commands
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(UndoCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(RedoCommand)
+
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(CutCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(CutOrDeleteCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(CopyCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(CopyOrDeleteCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(PasteCommand)
+NS_DECL_EDITOR_COMMAND_FOR_TRANSFERABLE_PARAM(PasteTransferableCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(SwitchTextDirectionCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(DeleteCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(SelectAllCommand)
+
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(SelectionMoveCommands)
+
+// Insert content commands
+NS_DECL_EDITOR_COMMAND_FOR_STRING_PARAM(InsertPlaintextCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(InsertParagraphCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(InsertLineBreakCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(PasteQuotationCommand)
+
+/******************************************************************************
+ * Commands for HTML editor
+ ******************************************************************************/
+
+// virtual base class for commands that need to save and update Boolean state
+// (like styles etc)
+class StateUpdatingCommandBase : public EditorCommand {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(StateUpdatingCommandBase, EditorCommand)
+
+ NS_DECL_EDITOR_COMMAND_COMMON_METHODS
+ NS_DECL_DO_COMMAND_PARAM_DELEGATE_TO_DO_COMMAND
+
+ protected:
+ StateUpdatingCommandBase() = default;
+ virtual ~StateUpdatingCommandBase() = default;
+
+ // get the current state (on or off) for this style or block format
+ MOZ_CAN_RUN_SCRIPT virtual nsresult GetCurrentState(
+ nsStaticAtom& aTagName, HTMLEditor& aHTMLEditor,
+ nsCommandParams& aParams) const = 0;
+
+ // add/remove the style
+ MOZ_CAN_RUN_SCRIPT virtual nsresult ToggleState(
+ nsStaticAtom& aTagName, HTMLEditor& aHTMLEditor,
+ nsIPrincipal* aPrincipal) const = 0;
+
+ static nsStaticAtom* GetTagName(Command aCommand) {
+ switch (aCommand) {
+ case Command::FormatBold:
+ return nsGkAtoms::b;
+ case Command::FormatItalic:
+ return nsGkAtoms::i;
+ case Command::FormatUnderline:
+ return nsGkAtoms::u;
+ case Command::FormatTeletypeText:
+ return nsGkAtoms::tt;
+ case Command::FormatStrikeThrough:
+ return nsGkAtoms::strike;
+ case Command::FormatSuperscript:
+ return nsGkAtoms::sup;
+ case Command::FormatSubscript:
+ return nsGkAtoms::sub;
+ case Command::FormatNoBreak:
+ return nsGkAtoms::nobr;
+ case Command::FormatEmphasis:
+ return nsGkAtoms::em;
+ case Command::FormatStrong:
+ return nsGkAtoms::strong;
+ case Command::FormatCitation:
+ return nsGkAtoms::cite;
+ case Command::FormatAbbreviation:
+ return nsGkAtoms::abbr;
+ case Command::FormatAcronym:
+ return nsGkAtoms::acronym;
+ case Command::FormatCode:
+ return nsGkAtoms::code;
+ case Command::FormatSample:
+ return nsGkAtoms::samp;
+ case Command::FormatVariable:
+ return nsGkAtoms::var;
+ case Command::FormatRemoveLink:
+ return nsGkAtoms::href;
+ case Command::InsertOrderedList:
+ return nsGkAtoms::ol;
+ case Command::InsertUnorderedList:
+ return nsGkAtoms::ul;
+ case Command::InsertDefinitionTerm:
+ return nsGkAtoms::dt;
+ case Command::InsertDefinitionDetails:
+ return nsGkAtoms::dd;
+ case Command::FormatAbsolutePosition:
+ return nsGkAtoms::_empty;
+ default:
+ return nullptr;
+ }
+ }
+ friend class InsertTagCommand; // for allowing it to access GetTagName()
+};
+
+// Shared class for the various style updating commands like bold, italics etc.
+// Suitable for commands whose state is either 'on' or 'off'.
+class StyleUpdatingCommand final : public StateUpdatingCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(StyleUpdatingCommand)
+
+ protected:
+ StyleUpdatingCommand() = default;
+ virtual ~StyleUpdatingCommand() = default;
+
+ // get the current state (on or off) for this style or block format
+ MOZ_CAN_RUN_SCRIPT nsresult
+ GetCurrentState(nsStaticAtom& aTagName, HTMLEditor& aHTMLEditor,
+ nsCommandParams& aParams) const final;
+
+ // add/remove the style
+ MOZ_CAN_RUN_SCRIPT nsresult ToggleState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+class InsertTagCommand final : public EditorCommand {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(InsertTagCommand, EditorCommand)
+
+ NS_DECL_EDITOR_COMMAND_COMMON_METHODS
+ NS_DECL_DO_COMMAND_PARAM_DELEGATE_TO_DO_COMMAND
+ NS_DECL_DO_COMMAND_PARAM_FOR_STRING_PARAM
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(InsertTagCommand)
+
+ protected:
+ InsertTagCommand() = default;
+ virtual ~InsertTagCommand() = default;
+
+ static nsAtom* GetTagName(Command aCommand) {
+ switch (aCommand) {
+ case Command::InsertLink:
+ return nsGkAtoms::a;
+ case Command::InsertImage:
+ return nsGkAtoms::img;
+ case Command::InsertHorizontalRule:
+ return nsGkAtoms::hr;
+ default:
+ return StateUpdatingCommandBase::GetTagName(aCommand);
+ }
+ }
+};
+
+class ListCommand final : public StateUpdatingCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(ListCommand)
+
+ protected:
+ ListCommand() = default;
+ virtual ~ListCommand() = default;
+
+ // get the current state (on or off) for this style or block format
+ MOZ_CAN_RUN_SCRIPT nsresult
+ GetCurrentState(nsStaticAtom& aTagName, HTMLEditor& aHTMLEditor,
+ nsCommandParams& aParams) const final;
+
+ // add/remove the style
+ MOZ_CAN_RUN_SCRIPT nsresult ToggleState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+class ListItemCommand final : public StateUpdatingCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(ListItemCommand)
+
+ protected:
+ ListItemCommand() = default;
+ virtual ~ListItemCommand() = default;
+
+ // get the current state (on or off) for this style or block format
+ MOZ_CAN_RUN_SCRIPT nsresult
+ GetCurrentState(nsStaticAtom& aTagName, HTMLEditor& aHTMLEditor,
+ nsCommandParams& aParams) const final;
+
+ // add/remove the style
+ MOZ_CAN_RUN_SCRIPT nsresult ToggleState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+// Base class for commands whose state consists of a string (e.g. para format)
+class MultiStateCommandBase : public EditorCommand {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(MultiStateCommandBase, EditorCommand)
+
+ NS_DECL_EDITOR_COMMAND_COMMON_METHODS
+ NS_DECL_DO_COMMAND_PARAM_FOR_STRING_PARAM
+
+ protected:
+ MultiStateCommandBase() = default;
+ virtual ~MultiStateCommandBase() = default;
+
+ MOZ_CAN_RUN_SCRIPT virtual nsresult GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const = 0;
+ MOZ_CAN_RUN_SCRIPT virtual nsresult SetState(
+ HTMLEditor* aHTMLEditor, const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const = 0;
+};
+
+/**
+ * The command class for Document.execCommand("formatBlock"),
+ * Document.queryCommandValue("formatBlock") etc.
+ */
+class FormatBlockStateCommand final : public MultiStateCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(FormatBlockStateCommand)
+
+ protected:
+ FormatBlockStateCommand() = default;
+ virtual ~FormatBlockStateCommand() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+/**
+ * The command class for the legacy XUL edit command, cmd_paragraphState.
+ * This command treats only <p>, <pre>, <h1>, <h2>, <h3>, <h4>, <h5>, <h6>,
+ * <address> as a format node.
+ */
+class ParagraphStateCommand final : public MultiStateCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(ParagraphStateCommand)
+
+ protected:
+ ParagraphStateCommand() = default;
+ virtual ~ParagraphStateCommand() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+class FontFaceStateCommand final : public MultiStateCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(FontFaceStateCommand)
+
+ protected:
+ FontFaceStateCommand() = default;
+ virtual ~FontFaceStateCommand() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+class FontSizeStateCommand final : public MultiStateCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(FontSizeStateCommand)
+
+ protected:
+ FontSizeStateCommand() = default;
+ virtual ~FontSizeStateCommand() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+class HighlightColorStateCommand final : public MultiStateCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(HighlightColorStateCommand)
+
+ protected:
+ HighlightColorStateCommand() = default;
+ virtual ~HighlightColorStateCommand() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+class FontColorStateCommand final : public MultiStateCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(FontColorStateCommand)
+
+ protected:
+ FontColorStateCommand() = default;
+ virtual ~FontColorStateCommand() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+class AlignCommand final : public MultiStateCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(AlignCommand)
+
+ protected:
+ AlignCommand() = default;
+ virtual ~AlignCommand() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+class BackgroundColorStateCommand final : public MultiStateCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(BackgroundColorStateCommand)
+
+ protected:
+ BackgroundColorStateCommand() = default;
+ virtual ~BackgroundColorStateCommand() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+class AbsolutePositioningCommand final : public StateUpdatingCommandBase {
+ public:
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(AbsolutePositioningCommand)
+
+ protected:
+ AbsolutePositioningCommand() = default;
+ virtual ~AbsolutePositioningCommand() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ GetCurrentState(nsStaticAtom& aTagName, HTMLEditor& aHTMLEditor,
+ nsCommandParams& aParams) const final;
+ MOZ_CAN_RUN_SCRIPT nsresult ToggleState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsIPrincipal* aPrincipal) const final;
+};
+
+// composer commands
+
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(DocumentStateCommand)
+
+class SetDocumentStateCommand final : public EditorCommand {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(SetDocumentStateCommand, EditorCommand)
+
+ NS_DECL_EDITOR_COMMAND_COMMON_METHODS
+ NS_DECL_DO_COMMAND_PARAM_FOR_BOOL_PARAM
+ NS_DECL_DO_COMMAND_PARAM_FOR_CSTRING_PARAM
+ NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON(SetDocumentStateCommand)
+
+ private:
+ SetDocumentStateCommand() = default;
+ virtual ~SetDocumentStateCommand() = default;
+};
+
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(DecreaseZIndexCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(IncreaseZIndexCommand)
+
+// Generic commands
+
+// Edit menu
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(PasteNoFormattingCommand)
+
+// Block transformations
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(IndentCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(OutdentCommand)
+
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(RemoveListCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(RemoveStylesCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(IncreaseFontSizeCommand)
+NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE(DecreaseFontSizeCommand)
+
+// Insert content commands
+NS_DECL_EDITOR_COMMAND_FOR_STRING_PARAM(InsertHTMLCommand)
+
+#undef NS_DECL_EDITOR_COMMAND_FOR_NO_PARAM_WITH_DELEGATE
+#undef NS_DECL_EDITOR_COMMAND_FOR_BOOL_PARAM
+#undef NS_DECL_EDITOR_COMMAND_FOR_CSTRING_PARAM
+#undef NS_DECL_EDITOR_COMMAND_FOR_STRING_PARAM
+#undef NS_DECL_EDITOR_COMMAND_FOR_TRANSFERABLE_PARAM
+#undef NS_DECL_EDITOR_COMMAND_COMMON_METHODS
+#undef NS_DECL_DO_COMMAND_PARAM_DELEGATE_TO_DO_COMMAND
+#undef NS_DECL_DO_COMMAND_PARAM_FOR_BOOL_PARAM
+#undef NS_DECL_DO_COMMAND_PARAM_FOR_CSTRING_PARAM
+#undef NS_DECL_DO_COMMAND_PARAM_FOR_STRING_PARAM
+#undef NS_DECL_DO_COMMAND_PARAM_FOR_TRANSFERABLE_PARAM
+#undef NS_INLINE_DECL_EDITOR_COMMAND_MAKE_SINGLETON
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_EditorCommands_h
diff --git a/editor/libeditor/EditorController.cpp b/editor/libeditor/EditorController.cpp
new file mode 100644
index 0000000000..9bb440a615
--- /dev/null
+++ b/editor/libeditor/EditorController.cpp
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EditorController.h"
+
+#include "EditorCommands.h"
+#include "mozilla/mozalloc.h"
+#include "nsControllerCommandTable.h"
+#include "nsDebug.h"
+#include "nsError.h"
+
+class nsIControllerCommand;
+
+namespace mozilla {
+
+#define NS_REGISTER_COMMAND(_cmdClass, _cmdName) \
+ { \
+ aCommandTable->RegisterCommand( \
+ _cmdName, \
+ static_cast<nsIControllerCommand*>(_cmdClass::GetInstance())); \
+ }
+
+// static
+nsresult EditorController::RegisterEditingCommands(
+ nsControllerCommandTable* aCommandTable) {
+ // now register all our commands
+ // These are commands that will be used in text widgets, and in composer
+
+ NS_REGISTER_COMMAND(UndoCommand, "cmd_undo");
+ NS_REGISTER_COMMAND(RedoCommand, "cmd_redo");
+
+ NS_REGISTER_COMMAND(CutCommand, "cmd_cut");
+ NS_REGISTER_COMMAND(CutOrDeleteCommand, "cmd_cutOrDelete");
+ NS_REGISTER_COMMAND(CopyCommand, "cmd_copy");
+ NS_REGISTER_COMMAND(CopyOrDeleteCommand, "cmd_copyOrDelete");
+ NS_REGISTER_COMMAND(SelectAllCommand, "cmd_selectAll");
+
+ NS_REGISTER_COMMAND(PasteCommand, "cmd_paste");
+ NS_REGISTER_COMMAND(PasteTransferableCommand, "cmd_pasteTransferable");
+
+ NS_REGISTER_COMMAND(SwitchTextDirectionCommand, "cmd_switchTextDirection");
+
+ NS_REGISTER_COMMAND(DeleteCommand, "cmd_delete");
+ NS_REGISTER_COMMAND(DeleteCommand, "cmd_deleteCharBackward");
+ NS_REGISTER_COMMAND(DeleteCommand, "cmd_deleteCharForward");
+ NS_REGISTER_COMMAND(DeleteCommand, "cmd_deleteWordBackward");
+ NS_REGISTER_COMMAND(DeleteCommand, "cmd_deleteWordForward");
+ NS_REGISTER_COMMAND(DeleteCommand, "cmd_deleteToBeginningOfLine");
+ NS_REGISTER_COMMAND(DeleteCommand, "cmd_deleteToEndOfLine");
+
+ // Insert content
+ NS_REGISTER_COMMAND(InsertPlaintextCommand, "cmd_insertText");
+ NS_REGISTER_COMMAND(InsertParagraphCommand, "cmd_insertParagraph");
+ NS_REGISTER_COMMAND(InsertLineBreakCommand, "cmd_insertLineBreak");
+ NS_REGISTER_COMMAND(PasteQuotationCommand, "cmd_pasteQuote");
+
+ return NS_OK;
+}
+
+// static
+nsresult EditorController::RegisterEditorCommands(
+ nsControllerCommandTable* aCommandTable) {
+ // These are commands that will be used in text widgets only.
+
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_scrollTop");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_scrollBottom");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveTop");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveBottom");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectTop");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectBottom");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_lineNext");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_linePrevious");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectLineNext");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectLinePrevious");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_charPrevious");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_charNext");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectCharPrevious");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectCharNext");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_beginLine");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_endLine");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectBeginLine");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectEndLine");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_wordPrevious");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_wordNext");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectWordPrevious");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectWordNext");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_scrollPageUp");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_scrollPageDown");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_scrollLineUp");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_scrollLineDown");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_movePageUp");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_movePageDown");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectPageUp");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectPageDown");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveLeft");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveRight");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveUp");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveDown");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveLeft2");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveRight2");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveUp2");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_moveDown2");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectLeft");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectRight");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectUp");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectDown");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectLeft2");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectRight2");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectUp2");
+ NS_REGISTER_COMMAND(SelectionMoveCommands, "cmd_selectDown2");
+
+ return NS_OK;
+}
+
+// static
+void EditorController::Shutdown() {
+ // EditingCommands
+ UndoCommand::Shutdown();
+ RedoCommand::Shutdown();
+ CutCommand::Shutdown();
+ CutOrDeleteCommand::Shutdown();
+ CopyCommand::Shutdown();
+ CopyOrDeleteCommand::Shutdown();
+ PasteCommand::Shutdown();
+ PasteTransferableCommand::Shutdown();
+ SwitchTextDirectionCommand::Shutdown();
+ DeleteCommand::Shutdown();
+ SelectAllCommand::Shutdown();
+ InsertPlaintextCommand::Shutdown();
+ InsertParagraphCommand::Shutdown();
+ InsertLineBreakCommand::Shutdown();
+ PasteQuotationCommand::Shutdown();
+
+ // EditorCommands
+ SelectionMoveCommands::Shutdown();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorController.h b/editor/libeditor/EditorController.h
new file mode 100644
index 0000000000..f7286e1f55
--- /dev/null
+++ b/editor/libeditor/EditorController.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditorController_h
+#define mozilla_EditorController_h
+
+#include "nscore.h"
+
+class nsControllerCommandTable;
+
+namespace mozilla {
+
+// the editor controller is used for both text widgets, and basic text editing
+// commands in composer. The refCon that gets passed to its commands is an
+// nsIEditor.
+
+class EditorController final {
+ public:
+ static nsresult RegisterEditorCommands(
+ nsControllerCommandTable* aCommandTable);
+ static nsresult RegisterEditingCommands(
+ nsControllerCommandTable* aCommandTable);
+ static void Shutdown();
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_EditorController_h
diff --git a/editor/libeditor/EditorDOMPoint.h b/editor/libeditor/EditorDOMPoint.h
new file mode 100644
index 0000000000..907b02f059
--- /dev/null
+++ b/editor/libeditor/EditorDOMPoint.h
@@ -0,0 +1,1629 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditorDOMPoint_h
+#define mozilla_EditorDOMPoint_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RangeBoundary.h"
+#include "mozilla/ToString.h"
+#include "mozilla/dom/AbstractRange.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Selection.h" // for Selection::InterlinePosition
+#include "mozilla/dom/Text.h"
+#include "nsAtom.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsCRT.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsStyledElement.h"
+
+#include <type_traits>
+
+namespace mozilla {
+
+/**
+ * EditorDOMPoint and EditorRawDOMPoint are simple classes which refers
+ * a point in the DOM tree at creating the instance or initializing the
+ * instance with calling Set().
+ *
+ * EditorDOMPoint refers container node (and child node if it's already set)
+ * with nsCOMPtr. EditorRawDOMPoint refers them with raw pointer.
+ * So, EditorRawDOMPoint is useful when you access the nodes only before
+ * changing DOM tree since increasing refcount may appear in micro benchmark
+ * if it's in a hot path. On the other hand, if you need to refer them even
+ * after changing DOM tree, you must use EditorDOMPoint.
+ *
+ * When initializing an instance only with child node or offset, the instance
+ * starts to refer the child node or offset in the container. In this case,
+ * the other information hasn't been initialized due to performance reason.
+ * When you retrieve the other information with calling Offset() or
+ * GetChild(), the other information is computed with the current DOM tree.
+ * Therefore, e.g., in the following case, the other information may be
+ * different:
+ *
+ * EditorDOMPoint pointA(container1, childNode1);
+ * EditorDOMPoint pointB(container1, childNode1);
+ * Unused << pointA.Offset(); // The offset is computed now.
+ * container1->RemoveChild(childNode1->GetPreviousSibling());
+ * Unused << pointB.Offset(); // Now, pointB.Offset() equals pointA.Offset() - 1
+ *
+ * similarly:
+ *
+ * EditorDOMPoint pointA(container1, 5);
+ * EditorDOMPoint pointB(container1, 5);
+ * Unused << pointA.GetChild(); // The child is computed now.
+ * container1->RemoveChild(childNode1->GetFirstChild());
+ * Unused << pointB.GetChild(); // Now, pointB.GetChild() equals
+ * // pointA.GetChild()->GetPreviousSibling().
+ *
+ * So, when you initialize an instance only with one information, you need to
+ * be careful when you access the other information after changing the DOM tree.
+ * When you need to lock the child node or offset and recompute the other
+ * information with new DOM tree, you can use
+ * AutoEditorDOMPointOffsetInvalidator and AutoEditorDOMPointChildInvalidator.
+ */
+
+// FYI: Don't make the following instantiating macros end with `;` because
+// using them without `;`, VSCode may be confused and cause wrong red-
+// wavy underlines in the following code of the macro.
+#define NS_INSTANTIATE_EDITOR_DOM_POINT_METHOD(aResultType, aMethodName, ...) \
+ template aResultType EditorDOMPoint::aMethodName(__VA_ARGS__); \
+ template aResultType EditorRawDOMPoint::aMethodName(__VA_ARGS__); \
+ template aResultType EditorDOMPointInText::aMethodName(__VA_ARGS__); \
+ template aResultType EditorRawDOMPointInText::aMethodName(__VA_ARGS__)
+
+#define NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(aResultType, aMethodName, \
+ ...) \
+ template aResultType EditorDOMPoint::aMethodName(__VA_ARGS__) const; \
+ template aResultType EditorRawDOMPoint::aMethodName(__VA_ARGS__) const; \
+ template aResultType EditorDOMPointInText::aMethodName(__VA_ARGS__) const; \
+ template aResultType EditorRawDOMPointInText::aMethodName(__VA_ARGS__) const
+
+#define NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(aMethodName, ...) \
+ template EditorDOMPoint aMethodName(__VA_ARGS__); \
+ template EditorRawDOMPoint aMethodName(__VA_ARGS__); \
+ template EditorDOMPointInText aMethodName(__VA_ARGS__); \
+ template EditorRawDOMPointInText aMethodName(__VA_ARGS__)
+
+#define NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( \
+ aMethodName, ...) \
+ template EditorDOMPoint aMethodName(__VA_ARGS__) const; \
+ template EditorRawDOMPoint aMethodName(__VA_ARGS__) const; \
+ template EditorDOMPointInText aMethodName(__VA_ARGS__) const; \
+ template EditorRawDOMPointInText aMethodName(__VA_ARGS__) const
+
+template <typename ParentType, typename ChildType>
+class EditorDOMPointBase final {
+ using SelfType = EditorDOMPointBase<ParentType, ChildType>;
+
+ public:
+ using InterlinePosition = dom::Selection::InterlinePosition;
+
+ EditorDOMPointBase() = default;
+
+ template <typename ContainerType>
+ EditorDOMPointBase(
+ const ContainerType* aContainer, uint32_t aOffset,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined)
+ : mParent(const_cast<ContainerType*>(aContainer)),
+ mChild(nullptr),
+ mOffset(Some(aOffset)),
+ mInterlinePosition(aInterlinePosition) {
+ NS_WARNING_ASSERTION(
+ !mParent || mOffset.value() <= mParent->Length(),
+ "The offset is larger than the length of aContainer or negative");
+ if (!mParent) {
+ mOffset.reset();
+ }
+ }
+
+ template <typename ContainerType, template <typename> typename StrongPtr>
+ EditorDOMPointBase(
+ const StrongPtr<ContainerType>& aContainer, uint32_t aOffset,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined)
+ : EditorDOMPointBase(aContainer.get(), aOffset, aInterlinePosition) {}
+
+ template <typename ContainerType, template <typename> typename StrongPtr>
+ EditorDOMPointBase(
+ const StrongPtr<const ContainerType>& aContainer, uint32_t aOffset,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined)
+ : EditorDOMPointBase(aContainer.get(), aOffset, aInterlinePosition) {}
+
+ /**
+ * Different from RangeBoundary, aPointedNode should be a child node
+ * which you want to refer.
+ */
+ explicit EditorDOMPointBase(
+ const nsINode* aPointedNode,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined)
+ : mParent(aPointedNode && aPointedNode->IsContent()
+ ? aPointedNode->GetParentNode()
+ : nullptr),
+ mChild(aPointedNode && aPointedNode->IsContent()
+ ? const_cast<nsIContent*>(aPointedNode->AsContent())
+ : nullptr),
+ mInterlinePosition(aInterlinePosition) {
+ mIsChildInitialized = aPointedNode && mChild;
+ NS_WARNING_ASSERTION(IsSet(),
+ "The child is nullptr or doesn't have its parent");
+ NS_WARNING_ASSERTION(mChild && mChild->GetParentNode() == mParent,
+ "Initializing RangeBoundary with invalid value");
+ }
+
+ EditorDOMPointBase(
+ nsINode* aContainer, nsIContent* aPointedNode, uint32_t aOffset,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined)
+ : mParent(aContainer),
+ mChild(aPointedNode),
+ mOffset(mozilla::Some(aOffset)),
+ mInterlinePosition(aInterlinePosition),
+ mIsChildInitialized(true) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ aContainer, "This constructor shouldn't be used when pointing nowhere");
+ MOZ_ASSERT(mOffset.value() <= mParent->Length());
+ MOZ_ASSERT(mChild || mParent->Length() == mOffset.value() ||
+ !mParent->IsContainerNode());
+ MOZ_ASSERT(!mChild || mParent == mChild->GetParentNode());
+ MOZ_ASSERT(mParent->GetChildAt_Deprecated(mOffset.value()) == mChild);
+ }
+
+ template <typename PT, typename CT>
+ explicit EditorDOMPointBase(const RangeBoundaryBase<PT, CT>& aOther)
+ : mParent(aOther.mParent),
+ mChild(aOther.mRef ? aOther.mRef->GetNextSibling()
+ : (aOther.mParent ? aOther.mParent->GetFirstChild()
+ : nullptr)),
+ mOffset(aOther.mOffset),
+ mIsChildInitialized(aOther.mRef || (aOther.mOffset.isSome() &&
+ !aOther.mOffset.value())) {}
+
+ void SetInterlinePosition(InterlinePosition aInterlinePosition) {
+ MOZ_ASSERT(IsSet());
+ mInterlinePosition = aInterlinePosition;
+ }
+ InterlinePosition GetInterlinePosition() const {
+ return IsSet() ? mInterlinePosition : InterlinePosition::Undefined;
+ }
+
+ /**
+ * GetContainer() returns the container node at the point.
+ * GetContainerAs() returns the container node as specific type.
+ */
+ nsINode* GetContainer() const { return mParent; }
+ template <typename ContentNodeType>
+ ContentNodeType* GetContainerAs() const {
+ return ContentNodeType::FromNodeOrNull(mParent);
+ }
+
+ /**
+ * ContainerAs() returns the container node with just casting to the specific
+ * type. Therefore, callers need to guarantee that the result is not nullptr
+ * nor wrong cast.
+ */
+ template <typename ContentNodeType>
+ ContentNodeType* ContainerAs() const {
+ MOZ_ASSERT(mParent);
+ MOZ_DIAGNOSTIC_ASSERT(ContentNodeType::FromNode(mParent));
+ return static_cast<ContentNodeType*>(GetContainer());
+ }
+
+ /**
+ * GetContainerParent() returns parent of the container node at the point.
+ */
+ nsINode* GetContainerParent() const {
+ return mParent ? mParent->GetParent() : nullptr;
+ }
+ template <typename ContentNodeType>
+ ContentNodeType* GetContainerParentAs() const {
+ return ContentNodeType::FromNodeOrNull(GetContainerParent());
+ }
+ template <typename ContentNodeType>
+ ContentNodeType* ContainerParentAs() const {
+ MOZ_DIAGNOSTIC_ASSERT(GetContainerParentAs<ContentNodeType>());
+ return static_cast<ContentNodeType*>(GetContainerParent());
+ }
+
+ dom::Element* GetContainerOrContainerParentElement() const {
+ if (MOZ_UNLIKELY(!mParent)) {
+ return nullptr;
+ }
+ return mParent->IsElement() ? ContainerAs<dom::Element>()
+ : GetContainerParentAs<dom::Element>();
+ }
+
+ /**
+ * CanContainerHaveChildren() returns true if the container node can have
+ * child nodes. Otherwise, e.g., when the container is a text node, returns
+ * false.
+ */
+ bool CanContainerHaveChildren() const {
+ return mParent && mParent->IsContainerNode();
+ }
+
+ /**
+ * IsContainerEmpty() returns true if it has no children or its text is empty.
+ */
+ bool IsContainerEmpty() const { return mParent && !mParent->Length(); }
+
+ /**
+ * IsInContentNode() returns true if the container is a subclass of
+ * nsIContent.
+ */
+ bool IsInContentNode() const { return mParent && mParent->IsContent(); }
+
+ /**
+ * IsInDataNode() returns true if the container node is a data node including
+ * text node.
+ */
+ bool IsInDataNode() const { return mParent && mParent->IsCharacterData(); }
+
+ /**
+ * IsInTextNode() returns true if the container node is a text node.
+ */
+ bool IsInTextNode() const { return mParent && mParent->IsText(); }
+
+ /**
+ * IsInNativeAnonymousSubtree() returns true if the container is in
+ * native anonymous subtree.
+ */
+ bool IsInNativeAnonymousSubtree() const {
+ return mParent && mParent->IsInNativeAnonymousSubtree();
+ }
+
+ /**
+ * IsContainerHTMLElement() returns true if the container node is an HTML
+ * element node and its node name is aTag.
+ */
+ bool IsContainerHTMLElement(nsAtom* aTag) const {
+ return mParent && mParent->IsHTMLElement(aTag);
+ }
+
+ /**
+ * IsContainerAnyOfHTMLElements() returns true if the container node is an
+ * HTML element node and its node name is one of the arguments.
+ */
+ template <typename First, typename... Args>
+ bool IsContainerAnyOfHTMLElements(First aFirst, Args... aArgs) const {
+ return mParent && mParent->IsAnyOfHTMLElements(aFirst, aArgs...);
+ }
+
+ /**
+ * GetChild() returns a child node which is pointed by the instance.
+ * If mChild hasn't been initialized yet, this computes the child node
+ * from mParent and mOffset with *current* DOM tree.
+ */
+ nsIContent* GetChild() const {
+ if (!mParent || !mParent->IsContainerNode()) {
+ return nullptr;
+ }
+ if (mIsChildInitialized) {
+ return mChild;
+ }
+ // Fix child node now.
+ const_cast<SelfType*>(this)->EnsureChild();
+ return mChild;
+ }
+
+ template <typename ContentNodeType>
+ ContentNodeType* GetChildAs() const {
+ return ContentNodeType::FromNodeOrNull(GetChild());
+ }
+ template <typename ContentNodeType>
+ ContentNodeType* ChildAs() const {
+ MOZ_DIAGNOSTIC_ASSERT(GetChildAs<ContentNodeType>());
+ return static_cast<ContentNodeType*>(GetChild());
+ }
+
+ /**
+ * GetCurrentChildAtOffset() returns current child at mOffset.
+ * I.e., mOffset needs to be fixed before calling this.
+ */
+ nsIContent* GetCurrentChildAtOffset() const {
+ MOZ_ASSERT(mOffset.isSome());
+ if (mOffset.isNothing()) {
+ return GetChild();
+ }
+ return mParent ? mParent->GetChildAt_Deprecated(*mOffset) : nullptr;
+ }
+
+ /**
+ * GetChildOrContainerIfDataNode() returns the child content node,
+ * or container content node if the container is a data node.
+ */
+ nsIContent* GetChildOrContainerIfDataNode() const {
+ if (IsInDataNode()) {
+ return ContainerAs<nsIContent>();
+ }
+ return GetChild();
+ }
+
+ /**
+ * GetNextSiblingOfChild() returns next sibling of the child node.
+ * If this refers after the last child or the container cannot have children,
+ * this returns nullptr with warning.
+ * If mChild hasn't been initialized yet, this computes the child node
+ * from mParent and mOffset with *current* DOM tree.
+ */
+ nsIContent* GetNextSiblingOfChild() const {
+ if (NS_WARN_IF(!mParent) || !mParent->IsContainerNode()) {
+ return nullptr;
+ }
+ if (mIsChildInitialized) {
+ return mChild ? mChild->GetNextSibling() : nullptr;
+ }
+ MOZ_ASSERT(mOffset.isSome());
+ if (NS_WARN_IF(mOffset.value() > mParent->Length())) {
+ // If this has been set only offset and now the offset is invalid,
+ // let's just return nullptr.
+ return nullptr;
+ }
+ // Fix child node now.
+ const_cast<SelfType*>(this)->EnsureChild();
+ return mChild ? mChild->GetNextSibling() : nullptr;
+ }
+ template <typename ContentNodeType>
+ ContentNodeType* GetNextSiblingOfChildAs() const {
+ return ContentNodeType::FromNodeOrNull(GetNextSiblingOfChild());
+ }
+ template <typename ContentNodeType>
+ ContentNodeType* NextSiblingOfChildAs() const {
+ MOZ_ASSERT(IsSet());
+ MOZ_DIAGNOSTIC_ASSERT(GetNextSiblingOfChildAs<ContentNodeType>());
+ return static_cast<ContentNodeType*>(GetNextSiblingOfChild());
+ }
+
+ /**
+ * GetPreviousSiblingOfChild() returns previous sibling of a child
+ * at offset. If this refers the first child or the container cannot have
+ * children, this returns nullptr with warning.
+ * If mChild hasn't been initialized yet, this computes the child node
+ * from mParent and mOffset with *current* DOM tree.
+ */
+ nsIContent* GetPreviousSiblingOfChild() const {
+ if (NS_WARN_IF(!mParent) || !mParent->IsContainerNode()) {
+ return nullptr;
+ }
+ if (mIsChildInitialized) {
+ return mChild ? mChild->GetPreviousSibling() : mParent->GetLastChild();
+ }
+ MOZ_ASSERT(mOffset.isSome());
+ if (NS_WARN_IF(mOffset.value() > mParent->Length())) {
+ // If this has been set only offset and now the offset is invalid,
+ // let's just return nullptr.
+ return nullptr;
+ }
+ // Fix child node now.
+ const_cast<SelfType*>(this)->EnsureChild();
+ return mChild ? mChild->GetPreviousSibling() : mParent->GetLastChild();
+ }
+ template <typename ContentNodeType>
+ ContentNodeType* GetPreviousSiblingOfChildAs() const {
+ return ContentNodeType::FromNodeOrNull(GetPreviousSiblingOfChild());
+ }
+ template <typename ContentNodeType>
+ ContentNodeType* PreviousSiblingOfChildAs() const {
+ MOZ_ASSERT(IsSet());
+ MOZ_DIAGNOSTIC_ASSERT(GetPreviousSiblingOfChildAs<ContentNodeType>());
+ return static_cast<ContentNodeType*>(GetPreviousSiblingOfChild());
+ }
+
+ /**
+ * Simple accessors of the character in dom::Text so that when you call
+ * these methods, you need to guarantee that the container is a dom::Text.
+ */
+ MOZ_NEVER_INLINE_DEBUG char16_t Char() const {
+ MOZ_ASSERT(IsSetAndValid());
+ MOZ_ASSERT(!IsEndOfContainer());
+ return ContainerAs<dom::Text>()->TextFragment().CharAt(mOffset.value());
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsCharASCIISpace() const {
+ return nsCRT::IsAsciiSpace(Char());
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsCharNBSP() const { return Char() == 0x00A0; }
+ MOZ_NEVER_INLINE_DEBUG bool IsCharASCIISpaceOrNBSP() const {
+ char16_t ch = Char();
+ return nsCRT::IsAsciiSpace(ch) || ch == 0x00A0;
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsCharNewLine() const { return Char() == '\n'; }
+ MOZ_NEVER_INLINE_DEBUG bool IsCharPreformattedNewLine() const;
+ MOZ_NEVER_INLINE_DEBUG bool
+ IsCharPreformattedNewLineCollapsedWithWhiteSpaces() const;
+ /**
+ * IsCharCollapsibleASCIISpace(), IsCharCollapsibleNBSP() and
+ * IsCharCollapsibleASCIISpaceOrNBSP() checks whether the white-space is
+ * preformatted or collapsible with the style of the container text node
+ * without flushing pending notifications.
+ */
+ bool IsCharCollapsibleASCIISpace() const;
+ bool IsCharCollapsibleNBSP() const;
+ bool IsCharCollapsibleASCIISpaceOrNBSP() const;
+
+ MOZ_NEVER_INLINE_DEBUG bool IsCharHighSurrogateFollowedByLowSurrogate()
+ const {
+ MOZ_ASSERT(IsSetAndValid());
+ MOZ_ASSERT(!IsEndOfContainer());
+ return ContainerAs<dom::Text>()
+ ->TextFragment()
+ .IsHighSurrogateFollowedByLowSurrogateAt(mOffset.value());
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsCharLowSurrogateFollowingHighSurrogate() const {
+ MOZ_ASSERT(IsSetAndValid());
+ MOZ_ASSERT(!IsEndOfContainer());
+ return ContainerAs<dom::Text>()
+ ->TextFragment()
+ .IsLowSurrogateFollowingHighSurrogateAt(mOffset.value());
+ }
+
+ MOZ_NEVER_INLINE_DEBUG char16_t PreviousChar() const {
+ MOZ_ASSERT(IsSetAndValid());
+ MOZ_ASSERT(!IsStartOfContainer());
+ return ContainerAs<dom::Text>()->TextFragment().CharAt(mOffset.value() - 1);
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsPreviousCharASCIISpace() const {
+ return nsCRT::IsAsciiSpace(PreviousChar());
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsPreviousCharNBSP() const {
+ return PreviousChar() == 0x00A0;
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsPreviousCharASCIISpaceOrNBSP() const {
+ char16_t ch = PreviousChar();
+ return nsCRT::IsAsciiSpace(ch) || ch == 0x00A0;
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsPreviousCharNewLine() const {
+ return PreviousChar() == '\n';
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsPreviousCharPreformattedNewLine() const;
+ MOZ_NEVER_INLINE_DEBUG bool
+ IsPreviousCharPreformattedNewLineCollapsedWithWhiteSpaces() const;
+ /**
+ * IsPreviousCharCollapsibleASCIISpace(), IsPreviousCharCollapsibleNBSP() and
+ * IsPreviousCharCollapsibleASCIISpaceOrNBSP() checks whether the white-space
+ * is preformatted or collapsible with the style of the container text node
+ * without flushing pending notifications.
+ */
+ bool IsPreviousCharCollapsibleASCIISpace() const;
+ bool IsPreviousCharCollapsibleNBSP() const;
+ bool IsPreviousCharCollapsibleASCIISpaceOrNBSP() const;
+
+ MOZ_NEVER_INLINE_DEBUG char16_t NextChar() const {
+ MOZ_ASSERT(IsSetAndValid());
+ MOZ_ASSERT(!IsAtLastContent() && !IsEndOfContainer());
+ return ContainerAs<dom::Text>()->TextFragment().CharAt(mOffset.value() + 1);
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsNextCharASCIISpace() const {
+ return nsCRT::IsAsciiSpace(NextChar());
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsNextCharNBSP() const {
+ return NextChar() == 0x00A0;
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsNextCharASCIISpaceOrNBSP() const {
+ char16_t ch = NextChar();
+ return nsCRT::IsAsciiSpace(ch) || ch == 0x00A0;
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsNextCharNewLine() const {
+ return NextChar() == '\n';
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsNextCharPreformattedNewLine() const;
+ MOZ_NEVER_INLINE_DEBUG bool
+ IsNextCharPreformattedNewLineCollapsedWithWhiteSpaces() const;
+ /**
+ * IsNextCharCollapsibleASCIISpace(), IsNextCharCollapsibleNBSP() and
+ * IsNextCharCollapsibleASCIISpaceOrNBSP() checks whether the white-space is
+ * preformatted or collapsible with the style of the container text node
+ * without flushing pending notifications.
+ */
+ bool IsNextCharCollapsibleASCIISpace() const;
+ bool IsNextCharCollapsibleNBSP() const;
+ bool IsNextCharCollapsibleASCIISpaceOrNBSP() const;
+
+ [[nodiscard]] bool HasOffset() const { return mOffset.isSome(); }
+ uint32_t Offset() const {
+ if (mOffset.isSome()) {
+ MOZ_ASSERT(mOffset.isSome());
+ return mOffset.value();
+ }
+ if (MOZ_UNLIKELY(!mParent)) {
+ MOZ_ASSERT(!mChild);
+ return 0u;
+ }
+ MOZ_ASSERT(mParent->IsContainerNode(),
+ "If the container cannot have children, mOffset.isSome() should "
+ "be true");
+ if (!mChild) {
+ // We're referring after the last child. Fix offset now.
+ const_cast<SelfType*>(this)->mOffset = mozilla::Some(mParent->Length());
+ return mOffset.value();
+ }
+ MOZ_ASSERT(mChild->GetParentNode() == mParent);
+ // Fix offset now.
+ if (mChild == mParent->GetFirstChild()) {
+ const_cast<SelfType*>(this)->mOffset = mozilla::Some(0u);
+ return 0u;
+ }
+ const_cast<SelfType*>(this)->mOffset = mParent->ComputeIndexOf(mChild);
+ MOZ_DIAGNOSTIC_ASSERT(mOffset.isSome());
+ return mOffset.valueOr(0u); // Avoid crash in Release/Beta
+ }
+
+ /**
+ * Set() sets a point to aOffset or aChild.
+ * If it's set with aOffset, mChild is invalidated. If it's set with aChild,
+ * mOffset may be invalidated.
+ */
+ template <typename ContainerType>
+ void Set(ContainerType* aContainer, uint32_t aOffset) {
+ mParent = aContainer;
+ mChild = nullptr;
+ mOffset = mozilla::Some(aOffset);
+ mIsChildInitialized = false;
+ mInterlinePosition = InterlinePosition::Undefined;
+ NS_ASSERTION(!mParent || mOffset.value() <= mParent->Length(),
+ "The offset is out of bounds");
+ }
+ template <typename ContainerType, template <typename> typename StrongPtr>
+ void Set(const StrongPtr<ContainerType>& aContainer, uint32_t aOffset) {
+ Set(aContainer.get(), aOffset);
+ }
+ void Set(const nsINode* aChild) {
+ MOZ_ASSERT(aChild);
+ if (NS_WARN_IF(!aChild->IsContent())) {
+ Clear();
+ return;
+ }
+ mParent = aChild->GetParentNode();
+ mChild = const_cast<nsIContent*>(aChild->AsContent());
+ mOffset.reset();
+ mIsChildInitialized = true;
+ mInterlinePosition = InterlinePosition::Undefined;
+ }
+
+ /**
+ * SetToEndOf() sets this to the end of aContainer. Then, mChild is always
+ * nullptr but marked as initialized and mOffset is always set.
+ */
+ template <typename ContainerType>
+ MOZ_NEVER_INLINE_DEBUG void SetToEndOf(const ContainerType* aContainer) {
+ MOZ_ASSERT(aContainer);
+ mParent = const_cast<ContainerType*>(aContainer);
+ mChild = nullptr;
+ mOffset = mozilla::Some(mParent->Length());
+ mIsChildInitialized = true;
+ mInterlinePosition = InterlinePosition::Undefined;
+ }
+ template <typename ContainerType, template <typename> typename StrongPtr>
+ MOZ_NEVER_INLINE_DEBUG void SetToEndOf(
+ const StrongPtr<ContainerType>& aContainer) {
+ SetToEndOf(aContainer.get());
+ }
+ template <typename ContainerType>
+ MOZ_NEVER_INLINE_DEBUG static SelfType AtEndOf(
+ const ContainerType& aContainer,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined) {
+ SelfType point;
+ point.SetToEndOf(&aContainer);
+ point.mInterlinePosition = aInterlinePosition;
+ return point;
+ }
+ template <typename ContainerType, template <typename> typename StrongPtr>
+ MOZ_NEVER_INLINE_DEBUG static SelfType AtEndOf(
+ const StrongPtr<ContainerType>& aContainer,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined) {
+ MOZ_ASSERT(aContainer.get());
+ return AtEndOf(*aContainer.get(), aInterlinePosition);
+ }
+
+ /**
+ * SetAfter() sets mChild to next sibling of aChild.
+ */
+ void SetAfter(const nsINode* aChild) {
+ MOZ_ASSERT(aChild);
+ nsIContent* nextSibling = aChild->GetNextSibling();
+ if (nextSibling) {
+ Set(nextSibling);
+ return;
+ }
+ nsINode* parentNode = aChild->GetParentNode();
+ if (NS_WARN_IF(!parentNode)) {
+ Clear();
+ return;
+ }
+ SetToEndOf(parentNode);
+ }
+ template <typename ContainerType>
+ static SelfType After(
+ const ContainerType& aContainer,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined) {
+ SelfType point;
+ point.SetAfter(&aContainer);
+ point.mInterlinePosition = aInterlinePosition;
+ return point;
+ }
+ template <typename ContainerType, template <typename> typename StrongPtr>
+ MOZ_NEVER_INLINE_DEBUG static SelfType After(
+ const StrongPtr<ContainerType>& aContainer,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined) {
+ MOZ_ASSERT(aContainer.get());
+ return After(*aContainer.get(), aInterlinePosition);
+ }
+ template <typename PT, typename CT>
+ MOZ_NEVER_INLINE_DEBUG static SelfType After(
+ const EditorDOMPointBase<PT, CT>& aPoint,
+ InterlinePosition aInterlinePosition = InterlinePosition::Undefined) {
+ MOZ_ASSERT(aPoint.IsSet());
+ if (aPoint.mChild) {
+ return After(*aPoint.mChild, aInterlinePosition);
+ }
+ if (NS_WARN_IF(aPoint.IsEndOfContainer())) {
+ return SelfType();
+ }
+ auto point = aPoint.NextPoint().template To<SelfType>();
+ point.mInterlinePosition = aInterlinePosition;
+ return point;
+ }
+
+ /**
+ * ParentPoint() returns a point whose child is the container.
+ */
+ template <typename EditorDOMPointType = SelfType>
+ EditorDOMPointType ParentPoint() const {
+ MOZ_ASSERT(mParent);
+ if (MOZ_UNLIKELY(!mParent) || !mParent->IsContent()) {
+ return EditorDOMPointType();
+ }
+ return EditorDOMPointType(ContainerAs<nsIContent>());
+ }
+
+ /**
+ * NextPoint() and PreviousPoint() returns next/previous DOM point in
+ * the container.
+ */
+ template <typename EditorDOMPointType = SelfType>
+ EditorDOMPointType NextPoint() const {
+ NS_ASSERTION(!IsEndOfContainer(), "Should not be at end of the container");
+ auto result = this->template To<EditorDOMPointType>();
+ result.AdvanceOffset();
+ return result;
+ }
+ template <typename EditorDOMPointType = SelfType>
+ EditorDOMPointType PreviousPoint() const {
+ NS_ASSERTION(!IsStartOfContainer(),
+ "Should not be at start of the container");
+ EditorDOMPointType result = this->template To<EditorDOMPointType>();
+ result.RewindOffset();
+ return result;
+ }
+
+ /**
+ * Clear() makes the instance not point anywhere.
+ */
+ void Clear() {
+ mParent = nullptr;
+ mChild = nullptr;
+ mOffset.reset();
+ mIsChildInitialized = false;
+ mInterlinePosition = InterlinePosition::Undefined;
+ }
+
+ /**
+ * AdvanceOffset() tries to refer next sibling of mChild and/of next offset.
+ * If the container can have children and there is no next sibling or the
+ * offset reached the length of the container, this outputs warning and does
+ * nothing. So, callers need to check if there is next sibling which you
+ * need to refer.
+ *
+ * @return true if there is a next DOM point to refer.
+ */
+ bool AdvanceOffset() {
+ if (NS_WARN_IF(!mParent)) {
+ return false;
+ }
+ // If only mOffset is available, just compute the offset.
+ if ((mOffset.isSome() && !mIsChildInitialized) ||
+ !mParent->IsContainerNode()) {
+ MOZ_ASSERT(mOffset.isSome());
+ MOZ_ASSERT(!mChild);
+ if (NS_WARN_IF(mOffset.value() >= mParent->Length())) {
+ // We're already referring the start of the container.
+ return false;
+ }
+ mOffset = mozilla::Some(mOffset.value() + 1);
+ mInterlinePosition = InterlinePosition::Undefined;
+ return true;
+ }
+
+ MOZ_ASSERT(mIsChildInitialized);
+ MOZ_ASSERT(!mOffset.isSome() || mOffset.isSome());
+ if (NS_WARN_IF(!mParent->HasChildren()) || NS_WARN_IF(!mChild) ||
+ NS_WARN_IF(mOffset.isSome() && mOffset.value() >= mParent->Length())) {
+ // We're already referring the end of the container (or outside).
+ return false;
+ }
+
+ if (mOffset.isSome()) {
+ MOZ_ASSERT(mOffset.isSome());
+ mOffset = mozilla::Some(mOffset.value() + 1);
+ }
+ mChild = mChild->GetNextSibling();
+ mInterlinePosition = InterlinePosition::Undefined;
+ return true;
+ }
+
+ /**
+ * RewindOffset() tries to refer previous sibling of mChild and/or previous
+ * offset. If the container can have children and there is no next previous
+ * or the offset is 0, this outputs warning and does nothing. So, callers
+ * need to check if there is previous sibling which you need to refer.
+ *
+ * @return true if there is a previous DOM point to refer.
+ */
+ bool RewindOffset() {
+ if (NS_WARN_IF(!mParent)) {
+ return false;
+ }
+ // If only mOffset is available, just compute the offset.
+ if ((mOffset.isSome() && !mIsChildInitialized) ||
+ !mParent->IsContainerNode()) {
+ MOZ_ASSERT(mOffset.isSome());
+ MOZ_ASSERT(!mChild);
+ if (NS_WARN_IF(!mOffset.value()) ||
+ NS_WARN_IF(mOffset.value() > mParent->Length())) {
+ // We're already referring the start of the container or
+ // the offset is invalid since perhaps, the offset was set before
+ // the last DOM tree change.
+ NS_ASSERTION(false, "Failed to rewind offset");
+ return false;
+ }
+ mOffset = mozilla::Some(mOffset.value() - 1);
+ mInterlinePosition = InterlinePosition::Undefined;
+ return true;
+ }
+
+ MOZ_ASSERT(mIsChildInitialized);
+ MOZ_ASSERT(!mOffset.isSome() || mOffset.isSome());
+ if (NS_WARN_IF(!mParent->HasChildren()) ||
+ NS_WARN_IF(mChild && !mChild->GetPreviousSibling()) ||
+ NS_WARN_IF(mOffset.isSome() && !mOffset.value())) {
+ // We're already referring the start of the container (or the child has
+ // been moved from the container?).
+ return false;
+ }
+
+ nsIContent* previousSibling =
+ mChild ? mChild->GetPreviousSibling() : mParent->GetLastChild();
+ if (NS_WARN_IF(!previousSibling)) {
+ // We're already referring the first child of the container.
+ return false;
+ }
+
+ if (mOffset.isSome()) {
+ mOffset = mozilla::Some(mOffset.value() - 1);
+ }
+ mChild = previousSibling;
+ mInterlinePosition = InterlinePosition::Undefined;
+ return true;
+ }
+
+ /**
+ * GetNonAnonymousSubtreePoint() returns a DOM point which is NOT in
+ * native-anonymous subtree. If the instance isn't in native-anonymous
+ * subtree, this returns same point. Otherwise, climbs up until finding
+ * non-native-anonymous parent and returns the point of it. I.e.,
+ * container is parent of the found non-anonymous-native node.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType GetNonAnonymousSubtreePoint() const {
+ if (NS_WARN_IF(!IsSet())) {
+ return EditorDOMPointType();
+ }
+ if (!IsInNativeAnonymousSubtree()) {
+ return this->template To<EditorDOMPointType>();
+ }
+ nsINode* parent;
+ for (parent = mParent->GetParentNode();
+ parent && parent->IsInNativeAnonymousSubtree();
+ parent = parent->GetParentNode()) {
+ }
+ if (!parent) {
+ return EditorDOMPointType();
+ }
+ return EditorDOMPointType(parent);
+ }
+
+ [[nodiscard]] bool IsSet() const {
+ return mParent && (mIsChildInitialized || mOffset.isSome());
+ }
+
+ [[nodiscard]] bool IsSetAndValid() const {
+ if (!IsSet()) {
+ return false;
+ }
+
+ if (mChild &&
+ (mChild->GetParentNode() != mParent || mChild->IsBeingRemoved())) {
+ return false;
+ }
+ if (mOffset.isSome() && mOffset.value() > mParent->Length()) {
+ return false;
+ }
+ return true;
+ }
+
+ [[nodiscard]] bool IsInComposedDoc() const {
+ return IsSet() && mParent->IsInComposedDoc();
+ }
+
+ [[nodiscard]] bool IsSetAndValidInComposedDoc() const {
+ return IsInComposedDoc() && IsSetAndValid();
+ }
+
+ bool IsStartOfContainer() const {
+ // If we're referring the first point in the container:
+ // If mParent is not a container like a text node, mOffset is 0.
+ // If mChild is initialized and it's first child of mParent.
+ // If mChild isn't initialized and the offset is 0.
+ if (NS_WARN_IF(!mParent)) {
+ return false;
+ }
+ if (!mParent->IsContainerNode()) {
+ return !mOffset.value();
+ }
+ if (mIsChildInitialized) {
+ if (mParent->GetFirstChild() == mChild) {
+ NS_WARNING_ASSERTION(!mOffset.isSome() || !mOffset.value(),
+ "If mOffset was initialized, it should be 0");
+ return true;
+ }
+ NS_WARNING_ASSERTION(!mOffset.isSome() || mParent->GetChildAt_Deprecated(
+ mOffset.value()) == mChild,
+ "mOffset and mChild are mismatched");
+ return false;
+ }
+ MOZ_ASSERT(mOffset.isSome());
+ return !mOffset.value();
+ }
+
+ bool IsEndOfContainer() const {
+ // If we're referring after the last point of the container:
+ // If mParent is not a container like text node, mOffset is same as the
+ // length of the container.
+ // If mChild is initialized and it's nullptr.
+ // If mChild isn't initialized and mOffset is same as the length of the
+ // container.
+ if (NS_WARN_IF(!mParent)) {
+ return false;
+ }
+ if (!mParent->IsContainerNode()) {
+ return mOffset.value() == mParent->Length();
+ }
+ if (mIsChildInitialized) {
+ if (!mChild) {
+ NS_WARNING_ASSERTION(
+ !mOffset.isSome() || mOffset.value() == mParent->Length(),
+ "If mOffset was initialized, it should be length of the container");
+ return true;
+ }
+ NS_WARNING_ASSERTION(!mOffset.isSome() || mParent->GetChildAt_Deprecated(
+ mOffset.value()) == mChild,
+ "mOffset and mChild are mismatched");
+ return false;
+ }
+ MOZ_ASSERT(mOffset.isSome());
+ return mOffset.value() == mParent->Length();
+ }
+
+ /**
+ * IsAtLastContent() returns true when it refers last child of the container
+ * or last character offset of text node.
+ */
+ bool IsAtLastContent() const {
+ if (NS_WARN_IF(!mParent)) {
+ return false;
+ }
+ if (mParent->IsContainerNode() && mOffset.isSome()) {
+ return mOffset.value() == mParent->Length() - 1;
+ }
+ if (mIsChildInitialized) {
+ if (mChild && mChild == mParent->GetLastChild()) {
+ NS_WARNING_ASSERTION(
+ !mOffset.isSome() || mOffset.value() == mParent->Length() - 1,
+ "If mOffset was initialized, it should be length - 1 of the "
+ "container");
+ return true;
+ }
+ NS_WARNING_ASSERTION(!mOffset.isSome() || mParent->GetChildAt_Deprecated(
+ mOffset.value()) == mChild,
+ "mOffset and mChild are mismatched");
+ return false;
+ }
+ MOZ_ASSERT(mOffset.isSome());
+ return mOffset.value() == mParent->Length() - 1;
+ }
+
+ bool IsBRElementAtEndOfContainer() const {
+ if (NS_WARN_IF(!mParent)) {
+ return false;
+ }
+ if (!mParent->IsContainerNode()) {
+ return false;
+ }
+ const_cast<SelfType*>(this)->EnsureChild();
+ if (!mChild || mChild->GetNextSibling()) {
+ return false;
+ }
+ return mChild->IsHTMLElement(nsGkAtoms::br);
+ }
+
+ /**
+ * Return a point in text node if "this" points around a text node.
+ * EditorDOMPointType can always be EditorDOMPoint or EditorRawDOMPoint,
+ * but EditorDOMPointInText or EditorRawDOMPointInText is also available
+ * only when "this type" is one of them.
+ * If the point is in the anonymous <div> of a TextEditor, use
+ * TextEditor::FindBetterInsertionPoint() instead.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType GetPointInTextNodeIfPointingAroundTextNode() const {
+ if (NS_WARN_IF(!IsSet()) || !mParent->HasChildren()) {
+ return To<EditorDOMPointType>();
+ }
+ if (IsStartOfContainer()) {
+ if (auto* firstTextChild =
+ dom::Text::FromNode(mParent->GetFirstChild())) {
+ return EditorDOMPointType(firstTextChild, 0u);
+ }
+ return To<EditorDOMPointType>();
+ }
+ if (auto* previousSiblingChild = dom::Text::FromNodeOrNull(
+ GetPreviousSiblingOfChildAs<dom::Text>())) {
+ return EditorDOMPointType::AtEndOf(*previousSiblingChild);
+ }
+ if (auto* child = dom::Text::FromNodeOrNull(GetChildAs<dom::Text>())) {
+ return EditorDOMPointType(child, 0u);
+ }
+ return To<EditorDOMPointType>();
+ }
+
+ template <typename A, typename B>
+ EditorDOMPointBase& operator=(const RangeBoundaryBase<A, B>& aOther) {
+ mParent = aOther.mParent;
+ mChild = aOther.mRef ? aOther.mRef->GetNextSibling()
+ : (aOther.mParent && aOther.mParent->IsContainerNode()
+ ? aOther.mParent->GetFirstChild()
+ : nullptr);
+ mOffset = aOther.mOffset;
+ mIsChildInitialized =
+ aOther.mRef || (aOther.mParent && !aOther.mParent->IsContainerNode()) ||
+ (aOther.mOffset.isSome() && !aOther.mOffset.value());
+ mInterlinePosition = InterlinePosition::Undefined;
+ return *this;
+ }
+
+ template <typename EditorDOMPointType>
+ constexpr EditorDOMPointType To() const {
+ // XXX Cannot specialize this method due to implicit instantiatation caused
+ // by the inline CC functions below.
+ if (std::is_same<SelfType, EditorDOMPointType>::value) {
+ return reinterpret_cast<const EditorDOMPointType&>(*this);
+ }
+ EditorDOMPointType result;
+ result.mParent = mParent;
+ result.mChild = mChild;
+ result.mOffset = mOffset;
+ result.mIsChildInitialized = mIsChildInitialized;
+ result.mInterlinePosition = mInterlinePosition;
+ return result;
+ }
+
+ /**
+ * Don't compare mInterlinePosition. If it's required to check, perhaps,
+ * another compare operator like `===` should be created.
+ */
+ template <typename A, typename B>
+ bool operator==(const EditorDOMPointBase<A, B>& aOther) const {
+ if (mParent != aOther.mParent) {
+ return false;
+ }
+
+ if (mOffset.isSome() && aOther.mOffset.isSome()) {
+ // If both mOffset are set, we need to compare both mRef too because
+ // the relation of mRef and mOffset have already broken by DOM tree
+ // changes.
+ if (mOffset != aOther.mOffset) {
+ return false;
+ }
+ if (mChild == aOther.mChild) {
+ return true;
+ }
+ if (NS_WARN_IF(mIsChildInitialized && aOther.mIsChildInitialized)) {
+ // In this case, relation between mChild and mOffset of one of or both
+ // of them doesn't match with current DOM tree since the DOM tree might
+ // have been changed after computing mChild or mOffset.
+ return false;
+ }
+ // If one of mChild hasn't been computed yet, we should compare them only
+ // with mOffset. Perhaps, we shouldn't copy mChild from non-nullptr one
+ // to the other since if we copy it here, it may be unexpected behavior
+ // for some callers.
+ return true;
+ }
+
+ MOZ_ASSERT(mIsChildInitialized || aOther.mIsChildInitialized);
+
+ if (mOffset.isSome() && !mIsChildInitialized && !aOther.mOffset.isSome() &&
+ aOther.mIsChildInitialized) {
+ // If this has only mOffset and the other has only mChild, this needs to
+ // compute mChild now.
+ const_cast<SelfType*>(this)->EnsureChild();
+ return mChild == aOther.mChild;
+ }
+
+ if (!mOffset.isSome() && mIsChildInitialized && aOther.mOffset.isSome() &&
+ !aOther.mIsChildInitialized) {
+ // If this has only mChild and the other has only mOffset, the other needs
+ // to compute mChild now.
+ const_cast<EditorDOMPointBase<A, B>&>(aOther).EnsureChild();
+ return mChild == aOther.mChild;
+ }
+
+ // If mOffset of one of them hasn't been computed from mChild yet, we should
+ // compare only with mChild. Perhaps, we shouldn't copy mOffset from being
+ // some one to not being some one since if we copy it here, it may be
+ // unexpected behavior for some callers.
+ return mChild == aOther.mChild;
+ }
+
+ template <typename A, typename B>
+ bool operator==(const RangeBoundaryBase<A, B>& aOther) const {
+ // TODO: Optimize this with directly comparing with RangeBoundaryBase
+ // members.
+ return *this == SelfType(aOther);
+ }
+
+ template <typename A, typename B>
+ bool operator!=(const EditorDOMPointBase<A, B>& aOther) const {
+ return !(*this == aOther);
+ }
+
+ template <typename A, typename B>
+ bool operator!=(const RangeBoundaryBase<A, B>& aOther) const {
+ return !(*this == aOther);
+ }
+
+ /**
+ * This operator should be used if API of other modules take RawRangeBoundary,
+ * e.g., methods of Selection and nsRange.
+ */
+ operator const RawRangeBoundary() const { return ToRawRangeBoundary(); }
+ const RawRangeBoundary ToRawRangeBoundary() const {
+ if (!IsSet() || NS_WARN_IF(!mIsChildInitialized && !mOffset.isSome())) {
+ return RawRangeBoundary();
+ }
+ if (!mParent->IsContainerNode()) {
+ MOZ_ASSERT(mOffset.value() <= mParent->Length());
+ // If the container is a data node like a text node, we need to create
+ // RangeBoundaryBase instance only with mOffset because mChild is always
+ // nullptr.
+ return RawRangeBoundary(mParent, mOffset.value());
+ }
+ if (mIsChildInitialized && mOffset.isSome()) {
+ // If we've already set both child and offset, we should create
+ // RangeBoundary with offset after validation.
+#ifdef DEBUG
+ if (mChild) {
+ MOZ_ASSERT(mParent == mChild->GetParentNode());
+ MOZ_ASSERT(mParent->GetChildAt_Deprecated(mOffset.value()) == mChild);
+ } else {
+ MOZ_ASSERT(mParent->Length() == mOffset.value());
+ }
+#endif // #ifdef DEBUG
+ return RawRangeBoundary(mParent, mOffset.value());
+ }
+ // Otherwise, we should create RangeBoundaryBase only with available
+ // information.
+ if (mOffset.isSome()) {
+ return RawRangeBoundary(mParent, mOffset.value());
+ }
+ if (mChild) {
+ return RawRangeBoundary(mParent, mChild->GetPreviousSibling());
+ }
+ return RawRangeBoundary(mParent, mParent->GetLastChild());
+ }
+
+ already_AddRefed<nsRange> CreateCollapsedRange(ErrorResult& aRv) const {
+ const RawRangeBoundary boundary = ToRawRangeBoundary();
+ RefPtr<nsRange> range = nsRange::Create(boundary, boundary, aRv);
+ if (MOZ_UNLIKELY(aRv.Failed() || !range)) {
+ return nullptr;
+ }
+ return range.forget();
+ }
+
+ EditorDOMPointInText GetAsInText() const {
+ return IsInTextNode() ? EditorDOMPointInText(ContainerAs<dom::Text>(),
+ Offset(), mInterlinePosition)
+ : EditorDOMPointInText();
+ }
+ MOZ_NEVER_INLINE_DEBUG EditorDOMPointInText AsInText() const {
+ MOZ_ASSERT(IsInTextNode());
+ return EditorDOMPointInText(ContainerAs<dom::Text>(), Offset(),
+ mInterlinePosition);
+ }
+
+ template <typename A, typename B>
+ bool IsBefore(const EditorDOMPointBase<A, B>& aOther) const {
+ if (!IsSetAndValid() || !aOther.IsSetAndValid()) {
+ return false;
+ }
+ Maybe<int32_t> comp = nsContentUtils::ComparePoints(
+ ToRawRangeBoundary(), aOther.ToRawRangeBoundary());
+ return comp.isSome() && comp.value() == -1;
+ }
+
+ template <typename A, typename B>
+ bool EqualsOrIsBefore(const EditorDOMPointBase<A, B>& aOther) const {
+ if (!IsSetAndValid() || !aOther.IsSetAndValid()) {
+ return false;
+ }
+ Maybe<int32_t> comp = nsContentUtils::ComparePoints(
+ ToRawRangeBoundary(), aOther.ToRawRangeBoundary());
+ return comp.isSome() && comp.value() <= 0;
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const SelfType& aDOMPoint) {
+ aStream << "{ mParent=" << aDOMPoint.GetContainer();
+ if (aDOMPoint.mParent) {
+ aStream << " (" << *aDOMPoint.mParent
+ << ", Length()=" << aDOMPoint.mParent->Length() << ")";
+ }
+ aStream << ", mChild=" << static_cast<nsIContent*>(aDOMPoint.mChild);
+ if (aDOMPoint.mChild) {
+ aStream << " (" << *aDOMPoint.mChild << ")";
+ }
+ aStream << ", mOffset=" << aDOMPoint.mOffset << ", mIsChildInitialized="
+ << (aDOMPoint.mIsChildInitialized ? "true" : "false")
+ << ", mInterlinePosition=" << aDOMPoint.mInterlinePosition << " }";
+ return aStream;
+ }
+
+ private:
+ void EnsureChild() {
+ if (mIsChildInitialized) {
+ return;
+ }
+ if (!mParent) {
+ MOZ_ASSERT(!mOffset.isSome());
+ return;
+ }
+ MOZ_ASSERT(mOffset.isSome());
+ MOZ_ASSERT(mOffset.value() <= mParent->Length());
+ mIsChildInitialized = true;
+ if (!mParent->IsContainerNode()) {
+ return;
+ }
+ mChild = mParent->GetChildAt_Deprecated(mOffset.value());
+ MOZ_ASSERT(mChild || mOffset.value() == mParent->Length());
+ }
+
+ ParentType mParent = nullptr;
+ ChildType mChild = nullptr;
+
+ Maybe<uint32_t> mOffset;
+ InterlinePosition mInterlinePosition = InterlinePosition::Undefined;
+ bool mIsChildInitialized = false;
+
+ template <typename PT, typename CT>
+ friend class EditorDOMPointBase;
+
+ friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
+ EditorDOMPoint&, const char*,
+ uint32_t);
+ friend void ImplCycleCollectionUnlink(EditorDOMPoint&);
+};
+
+inline void ImplCycleCollectionUnlink(EditorDOMPoint& aField) {
+ ImplCycleCollectionUnlink(aField.mParent);
+ ImplCycleCollectionUnlink(aField.mChild);
+}
+
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback, EditorDOMPoint& aField,
+ const char* aName, uint32_t aFlags) {
+ ImplCycleCollectionTraverse(aCallback, aField.mParent, "mParent", 0);
+ ImplCycleCollectionTraverse(aCallback, aField.mChild, "mChild", 0);
+}
+
+/**
+ * EditorDOMRangeBase class stores a pair of same EditorDOMPointBase type.
+ * The instance must be created with valid DOM points and start must be
+ * before or same as end.
+ */
+#define NS_INSTANTIATE_EDITOR_DOM_RANGE_METHOD(aResultType, aMethodName, ...) \
+ template aResultType EditorDOMRange::aMethodName(__VA_ARGS__); \
+ template aResultType EditorRawDOMRange::aMethodName(__VA_ARGS__); \
+ template aResultType EditorDOMRangeInTexts::aMethodName(__VA_ARGS__); \
+ template aResultType EditorRawDOMRangeInTexts::aMethodName(__VA_ARGS__)
+
+#define NS_INSTANTIATE_EDITOR_DOM_RANGE_CONST_METHOD(aResultType, aMethodName, \
+ ...) \
+ template aResultType EditorDOMRange::aMethodName(__VA_ARGS__) const; \
+ template aResultType EditorRawDOMRange::aMethodName(__VA_ARGS__) const; \
+ template aResultType EditorDOMRangeInTexts::aMethodName(__VA_ARGS__) const; \
+ template aResultType EditorRawDOMRangeInTexts::aMethodName(__VA_ARGS__) const
+template <typename EditorDOMPointType>
+class EditorDOMRangeBase final {
+ using SelfType = EditorDOMRangeBase<EditorDOMPointType>;
+
+ public:
+ using PointType = EditorDOMPointType;
+
+ EditorDOMRangeBase() = default;
+ template <typename PT, typename CT>
+ explicit EditorDOMRangeBase(const EditorDOMPointBase<PT, CT>& aStart)
+ : mStart(aStart), mEnd(aStart) {
+ MOZ_ASSERT(!mStart.IsSet() || mStart.IsSetAndValid());
+ }
+ template <typename StartPointType, typename EndPointType>
+ explicit EditorDOMRangeBase(const StartPointType& aStart,
+ const EndPointType& aEnd)
+ : mStart(aStart.template To<PointType>()),
+ mEnd(aEnd.template To<PointType>()) {
+ MOZ_ASSERT_IF(mStart.IsSet(), mStart.IsSetAndValid());
+ MOZ_ASSERT_IF(mEnd.IsSet(), mEnd.IsSetAndValid());
+ MOZ_ASSERT_IF(mStart.IsSet() && mEnd.IsSet(),
+ mStart.EqualsOrIsBefore(mEnd));
+ }
+ explicit EditorDOMRangeBase(EditorDOMPointType&& aStart,
+ EditorDOMPointType&& aEnd)
+ : mStart(std::move(aStart)), mEnd(std::move(aEnd)) {
+ MOZ_ASSERT_IF(mStart.IsSet(), mStart.IsSetAndValid());
+ MOZ_ASSERT_IF(mEnd.IsSet(), mEnd.IsSetAndValid());
+ MOZ_ASSERT_IF(mStart.IsSet() && mEnd.IsSet(),
+ mStart.EqualsOrIsBefore(mEnd));
+ }
+ template <typename OtherPointType>
+ explicit EditorDOMRangeBase(const EditorDOMRangeBase<OtherPointType>& aOther)
+ : mStart(aOther.StartRef().template To<PointType>()),
+ mEnd(aOther.EndRef().template To<PointType>()) {
+ MOZ_ASSERT_IF(mStart.IsSet(), mStart.IsSetAndValid());
+ MOZ_ASSERT_IF(mEnd.IsSet(), mEnd.IsSetAndValid());
+ MOZ_ASSERT(mStart.IsSet() == mEnd.IsSet());
+ }
+ explicit EditorDOMRangeBase(const dom::AbstractRange& aRange)
+ : mStart(aRange.StartRef()), mEnd(aRange.EndRef()) {
+ MOZ_ASSERT_IF(mStart.IsSet(), mStart.IsSetAndValid());
+ MOZ_ASSERT_IF(mEnd.IsSet(), mEnd.IsSetAndValid());
+ MOZ_ASSERT_IF(mStart.IsSet() && mEnd.IsSet(),
+ mStart.EqualsOrIsBefore(mEnd));
+ }
+
+ template <typename MaybeOtherPointType>
+ void SetStart(const MaybeOtherPointType& aStart) {
+ mStart = aStart.template To<PointType>();
+ }
+ void SetStart(PointType&& aStart) { mStart = std::move(aStart); }
+ template <typename MaybeOtherPointType>
+ void SetEnd(const MaybeOtherPointType& aEnd) {
+ mEnd = aEnd.template To<PointType>();
+ }
+ void SetEnd(PointType&& aEnd) { mEnd = std::move(aEnd); }
+ template <typename StartPointType, typename EndPointType>
+ void SetStartAndEnd(const StartPointType& aStart, const EndPointType& aEnd) {
+ MOZ_ASSERT_IF(aStart.IsSet() && aEnd.IsSet(),
+ aStart.EqualsOrIsBefore(aEnd));
+ mStart = aStart.template To<PointType>();
+ mEnd = aEnd.template To<PointType>();
+ }
+ template <typename StartPointType>
+ void SetStartAndEnd(const StartPointType& aStart, PointType&& aEnd) {
+ MOZ_ASSERT_IF(aStart.IsSet() && aEnd.IsSet(),
+ aStart.EqualsOrIsBefore(aEnd));
+ mStart = aStart.template To<PointType>();
+ mEnd = std::move(aEnd);
+ }
+ template <typename EndPointType>
+ void SetStartAndEnd(PointType&& aStart, const EndPointType& aEnd) {
+ MOZ_ASSERT_IF(aStart.IsSet() && aEnd.IsSet(),
+ aStart.EqualsOrIsBefore(aEnd));
+ mStart = std::move(aStart);
+ mEnd = aEnd.template To<PointType>();
+ }
+ void SetStartAndEnd(PointType&& aStart, PointType&& aEnd) {
+ MOZ_ASSERT_IF(aStart.IsSet() && aEnd.IsSet(),
+ aStart.EqualsOrIsBefore(aEnd));
+ mStart = std::move(aStart);
+ mEnd = std::move(aEnd);
+ }
+ void Clear() {
+ mStart.Clear();
+ mEnd.Clear();
+ }
+
+ const PointType& StartRef() const { return mStart; }
+ const PointType& EndRef() const { return mEnd; }
+
+ bool Collapsed() const {
+ MOZ_ASSERT(IsPositioned());
+ return mStart == mEnd;
+ }
+ bool IsPositioned() const { return mStart.IsSet() && mEnd.IsSet(); }
+ bool IsPositionedAndValid() const {
+ return mStart.IsSetAndValid() && mEnd.IsSetAndValid() &&
+ mStart.EqualsOrIsBefore(mEnd);
+ }
+ template <typename OtherPointType>
+ MOZ_NEVER_INLINE_DEBUG bool Contains(const OtherPointType& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ return IsPositioned() && aPoint.IsSet() &&
+ mStart.EqualsOrIsBefore(aPoint) && aPoint.IsBefore(mEnd);
+ }
+ [[nodiscard]] nsINode* GetClosestCommonInclusiveAncestor() const;
+ bool InSameContainer() const {
+ MOZ_ASSERT(IsPositioned());
+ return IsPositioned() && mStart.GetContainer() == mEnd.GetContainer();
+ }
+ bool InAdjacentSiblings() const {
+ MOZ_ASSERT(IsPositioned());
+ return IsPositioned() &&
+ mStart.GetContainer()->GetNextSibling() == mEnd.GetContainer();
+ }
+ bool IsInContentNodes() const {
+ MOZ_ASSERT(IsPositioned());
+ return IsPositioned() && mStart.IsInContentNode() && mEnd.IsInContentNode();
+ }
+ bool IsInTextNodes() const {
+ MOZ_ASSERT(IsPositioned());
+ return IsPositioned() && mStart.IsInTextNode() && mEnd.IsInTextNode();
+ }
+ template <typename OtherRangeType>
+ bool operator==(const OtherRangeType& aOther) const {
+ return (!IsPositioned() && !aOther.IsPositioned()) ||
+ (mStart == aOther.mStart && mEnd == aOther.mEnd);
+ }
+ template <typename OtherRangeType>
+ bool operator!=(const OtherRangeType& aOther) const {
+ return !(*this == aOther);
+ }
+
+ EditorDOMRangeInTexts GetAsInTexts() const {
+ return IsInTextNodes()
+ ? EditorDOMRangeInTexts(mStart.AsInText(), mEnd.AsInText())
+ : EditorDOMRangeInTexts();
+ }
+ MOZ_NEVER_INLINE_DEBUG EditorDOMRangeInTexts AsInTexts() const {
+ MOZ_ASSERT(IsInTextNodes());
+ return EditorDOMRangeInTexts(mStart.AsInText(), mEnd.AsInText());
+ }
+
+ bool EnsureNotInNativeAnonymousSubtree() {
+ if (mStart.IsInNativeAnonymousSubtree()) {
+ nsIContent* parent = nullptr;
+ for (parent = mStart.template ContainerAs<nsIContent>()
+ ->GetClosestNativeAnonymousSubtreeRootParentOrHost();
+ parent && parent->IsInNativeAnonymousSubtree();
+ parent =
+ parent->GetClosestNativeAnonymousSubtreeRootParentOrHost()) {
+ }
+ if (MOZ_UNLIKELY(!parent)) {
+ return false;
+ }
+ mStart.Set(parent);
+ }
+ if (mEnd.IsInNativeAnonymousSubtree()) {
+ nsIContent* parent = nullptr;
+ for (parent = mEnd.template ContainerAs<nsIContent>()
+ ->GetClosestNativeAnonymousSubtreeRootParentOrHost();
+ parent && parent->IsInNativeAnonymousSubtree();
+ parent =
+ parent->GetClosestNativeAnonymousSubtreeRootParentOrHost()) {
+ }
+ if (MOZ_UNLIKELY(!parent)) {
+ return false;
+ }
+ mEnd.SetAfter(parent);
+ }
+ return true;
+ }
+
+ already_AddRefed<nsRange> CreateRange(ErrorResult& aRv) const {
+ RefPtr<nsRange> range = nsRange::Create(mStart.ToRawRangeBoundary(),
+ mEnd.ToRawRangeBoundary(), aRv);
+ if (MOZ_UNLIKELY(aRv.Failed() || !range)) {
+ return nullptr;
+ }
+ return range.forget();
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const SelfType& aRange) {
+ if (aRange.Collapsed()) {
+ aStream << "{ mStart=mEnd=" << aRange.mStart << " }";
+ } else {
+ aStream << "{ mStart=" << aRange.mStart << ", mEnd=" << aRange.mEnd
+ << " }";
+ }
+ return aStream;
+ }
+
+ private:
+ EditorDOMPointType mStart;
+ EditorDOMPointType mEnd;
+
+ friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
+ EditorDOMRange&, const char*,
+ uint32_t);
+ friend void ImplCycleCollectionUnlink(EditorDOMRange&);
+};
+
+inline void ImplCycleCollectionUnlink(EditorDOMRange& aField) {
+ ImplCycleCollectionUnlink(aField.mStart);
+ ImplCycleCollectionUnlink(aField.mEnd);
+}
+
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback, EditorDOMRange& aField,
+ const char* aName, uint32_t aFlags) {
+ ImplCycleCollectionTraverse(aCallback, aField.mStart, "mStart", 0);
+ ImplCycleCollectionTraverse(aCallback, aField.mEnd, "mEnd", 0);
+}
+
+/**
+ * AutoEditorDOMPointOffsetInvalidator is useful if DOM tree will be changed
+ * when EditorDOMPoint instance is available and keeps referring same child
+ * node.
+ *
+ * This class automatically guarantees that given EditorDOMPoint instance
+ * stores the child node and invalidates its offset when the instance is
+ * destroyed. Additionally, users of this class can invalidate the offset
+ * manually when they need.
+ */
+class MOZ_STACK_CLASS AutoEditorDOMPointOffsetInvalidator final {
+ public:
+ AutoEditorDOMPointOffsetInvalidator() = delete;
+ AutoEditorDOMPointOffsetInvalidator(
+ const AutoEditorDOMPointOffsetInvalidator&) = delete;
+ AutoEditorDOMPointOffsetInvalidator(AutoEditorDOMPointOffsetInvalidator&&) =
+ delete;
+ const AutoEditorDOMPointOffsetInvalidator& operator=(
+ const AutoEditorDOMPointOffsetInvalidator&) = delete;
+ explicit AutoEditorDOMPointOffsetInvalidator(EditorDOMPoint& aPoint)
+ : mPoint(aPoint), mCanceled(false) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ MOZ_ASSERT(mPoint.CanContainerHaveChildren());
+ mChild = mPoint.GetChild();
+ }
+
+ ~AutoEditorDOMPointOffsetInvalidator() {
+ if (!mCanceled) {
+ InvalidateOffset();
+ }
+ }
+
+ /**
+ * Manually, invalidate offset of the given point.
+ */
+ void InvalidateOffset() {
+ if (mChild) {
+ mPoint.Set(mChild);
+ } else {
+ // If the point referred after the last child, let's keep referring
+ // after current last node of the old container.
+ mPoint.SetToEndOf(mPoint.GetContainer());
+ }
+ }
+
+ /**
+ * After calling Cancel(), mPoint won't be modified by the destructor.
+ */
+ void Cancel() { mCanceled = true; }
+
+ private:
+ EditorDOMPoint& mPoint;
+ // Needs to store child node by ourselves because EditorDOMPoint stores
+ // child node with mRef which is previous sibling of current child node.
+ // Therefore, we cannot keep referring it if it's first child.
+ nsCOMPtr<nsIContent> mChild;
+
+ bool mCanceled;
+};
+
+class MOZ_STACK_CLASS AutoEditorDOMRangeOffsetsInvalidator final {
+ public:
+ explicit AutoEditorDOMRangeOffsetsInvalidator(EditorDOMRange& aRange)
+ : mStartInvalidator(const_cast<EditorDOMPoint&>(aRange.StartRef())),
+ mEndInvalidator(const_cast<EditorDOMPoint&>(aRange.EndRef())) {}
+
+ void InvalidateOffsets() {
+ mStartInvalidator.InvalidateOffset();
+ mEndInvalidator.InvalidateOffset();
+ }
+
+ void Cancel() {
+ mStartInvalidator.Cancel();
+ mEndInvalidator.Cancel();
+ }
+
+ private:
+ AutoEditorDOMPointOffsetInvalidator mStartInvalidator;
+ AutoEditorDOMPointOffsetInvalidator mEndInvalidator;
+};
+
+/**
+ * AutoEditorDOMPointChildInvalidator is useful if DOM tree will be changed
+ * when EditorDOMPoint instance is available and keeps referring same container
+ * and offset in it.
+ *
+ * This class automatically guarantees that given EditorDOMPoint instance
+ * stores offset and invalidates its child node when the instance is destroyed.
+ * Additionally, users of this class can invalidate the child manually when
+ * they need.
+ */
+class MOZ_STACK_CLASS AutoEditorDOMPointChildInvalidator final {
+ public:
+ AutoEditorDOMPointChildInvalidator() = delete;
+ AutoEditorDOMPointChildInvalidator(
+ const AutoEditorDOMPointChildInvalidator&) = delete;
+ AutoEditorDOMPointChildInvalidator(AutoEditorDOMPointChildInvalidator&&) =
+ delete;
+ const AutoEditorDOMPointChildInvalidator& operator=(
+ const AutoEditorDOMPointChildInvalidator&) = delete;
+ explicit AutoEditorDOMPointChildInvalidator(EditorDOMPoint& aPoint)
+ : mPoint(aPoint), mCanceled(false) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ Unused << mPoint.Offset();
+ }
+
+ ~AutoEditorDOMPointChildInvalidator() {
+ if (!mCanceled) {
+ InvalidateChild();
+ }
+ }
+
+ /**
+ * Manually, invalidate child of the given point.
+ */
+ void InvalidateChild() { mPoint.Set(mPoint.GetContainer(), mPoint.Offset()); }
+
+ /**
+ * After calling Cancel(), mPoint won't be modified by the destructor.
+ */
+ void Cancel() { mCanceled = true; }
+
+ private:
+ EditorDOMPoint& mPoint;
+
+ bool mCanceled;
+};
+
+class MOZ_STACK_CLASS AutoEditorDOMRangeChildrenInvalidator final {
+ public:
+ explicit AutoEditorDOMRangeChildrenInvalidator(EditorDOMRange& aRange)
+ : mStartInvalidator(const_cast<EditorDOMPoint&>(aRange.StartRef())),
+ mEndInvalidator(const_cast<EditorDOMPoint&>(aRange.EndRef())) {}
+
+ void InvalidateChildren() {
+ mStartInvalidator.InvalidateChild();
+ mEndInvalidator.InvalidateChild();
+ }
+
+ void Cancel() {
+ mStartInvalidator.Cancel();
+ mEndInvalidator.Cancel();
+ }
+
+ private:
+ AutoEditorDOMPointChildInvalidator mStartInvalidator;
+ AutoEditorDOMPointChildInvalidator mEndInvalidator;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_EditorDOMPoint_h
diff --git a/editor/libeditor/EditorEventListener.cpp b/editor/libeditor/EditorEventListener.cpp
new file mode 100644
index 0000000000..299057d17f
--- /dev/null
+++ b/editor/libeditor/EditorEventListener.cpp
@@ -0,0 +1,1237 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=4 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditorEventListener.h"
+
+#include "EditorBase.h" // for EditorBase, etc.
+#include "EditorUtils.h" // for EditorUtils
+#include "HTMLEditor.h" // for HTMLEditor
+#include "TextEditor.h" // for TextEditor
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
+#include "mozilla/AutoRestore.h"
+#include "mozilla/ContentEvents.h" // for InternalFocusEvent
+#include "mozilla/EventListenerManager.h" // for EventListenerManager
+#include "mozilla/EventStateManager.h" // for EventStateManager
+#include "mozilla/IMEStateManager.h" // for IMEStateManager
+#include "mozilla/LookAndFeel.h" // for LookAndFeel
+#include "mozilla/NativeKeyBindingsType.h" // for NativeKeyBindingsType
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/PresShell.h" // for PresShell
+#include "mozilla/TextEvents.h" // for WidgetCompositionEvent
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/Document.h" // for Document
+#include "mozilla/dom/DOMStringList.h"
+#include "mozilla/dom/DragEvent.h"
+#include "mozilla/dom/Element.h" // for Element
+#include "mozilla/dom/Event.h" // for Event
+#include "mozilla/dom/EventTarget.h" // for EventTarget
+#include "mozilla/dom/HTMLTextAreaElement.h"
+#include "mozilla/dom/MouseEvent.h" // for MouseEvent
+#include "mozilla/dom/Selection.h"
+
+#include "nsAString.h"
+#include "nsCaret.h" // for nsCaret
+#include "nsDebug.h" // for NS_WARNING, etc.
+#include "nsFocusManager.h" // for nsFocusManager
+#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::input
+#include "nsIContent.h" // for nsIContent
+#include "nsIContentInlines.h" // for nsINode::IsInDesignMode()
+#include "nsIController.h" // for nsIController
+#include "nsID.h"
+#include "nsIFormControl.h" // for nsIFormControl, etc.
+#include "nsINode.h" // for nsINode, etc.
+#include "nsIWidget.h" // for nsIWidget
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsPIWindowRoot.h" // for nsPIWindowRoot
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "nsRange.h"
+#include "nsServiceManagerUtils.h" // for do_GetService
+#include "nsString.h" // for nsAutoString
+#include "nsQueryObject.h" // for do_QueryObject
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+# include "nsContentUtils.h" // for nsContentUtils, etc.
+# include "nsIBidiKeyboard.h" // for nsIBidiKeyboard
+#endif
+
+#include "mozilla/dom/BrowserParent.h"
+
+class nsPresContext;
+
+namespace mozilla {
+
+using namespace dom;
+
+MOZ_CAN_RUN_SCRIPT static void DoCommandCallback(Command aCommand,
+ void* aData) {
+ Document* doc = static_cast<Document*>(aData);
+ nsPIDOMWindowOuter* win = doc->GetWindow();
+ if (!win) {
+ return;
+ }
+ nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot();
+ if (!root) {
+ return;
+ }
+
+ const char* commandStr = WidgetKeyboardEvent::GetCommandStr(aCommand);
+
+ nsCOMPtr<nsIController> controller;
+ root->GetControllerForCommand(commandStr, false /* for any window */,
+ getter_AddRefs(controller));
+ if (!controller) {
+ return;
+ }
+
+ bool commandEnabled;
+ if (NS_WARN_IF(NS_FAILED(
+ controller->IsCommandEnabled(commandStr, &commandEnabled)))) {
+ return;
+ }
+ if (commandEnabled) {
+ controller->DoCommand(commandStr);
+ }
+}
+
+EditorEventListener::EditorEventListener()
+ : mEditorBase(nullptr),
+ mCommitText(false),
+ mInTransaction(false),
+ mMouseDownOrUpConsumedByIME(false)
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ ,
+ mHaveBidiKeyboards(false),
+ mShouldSwitchTextDirection(false),
+ mSwitchToRTL(false)
+#endif
+{
+}
+
+EditorEventListener::~EditorEventListener() {
+ if (mEditorBase) {
+ NS_WARNING("We've not been uninstalled yet");
+ Disconnect();
+ }
+}
+
+nsresult EditorEventListener::Connect(EditorBase* aEditorBase) {
+ if (NS_WARN_IF(!aEditorBase)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard();
+ if (bidiKeyboard) {
+ bool haveBidiKeyboards = false;
+ bidiKeyboard->GetHaveBidiKeyboards(&haveBidiKeyboards);
+ mHaveBidiKeyboards = haveBidiKeyboards;
+ }
+#endif
+
+ mEditorBase = aEditorBase;
+
+ nsresult rv = InstallToEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorEventListener::InstallToEditor() failed");
+ Disconnect();
+ }
+ return rv;
+}
+
+nsresult EditorEventListener::InstallToEditor() {
+ MOZ_ASSERT(mEditorBase, "The caller must set mEditorBase");
+
+ EventTarget* eventTarget = mEditorBase->GetDOMEventTarget();
+ if (NS_WARN_IF(!eventTarget)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // register the event listeners with the listener manager
+ EventListenerManager* eventListenerManager =
+ eventTarget->GetOrCreateListenerManager();
+ if (NS_WARN_IF(!eventListenerManager)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // For non-html editor, ie.TextEditor, we want to preserve
+ // the event handling order to ensure listeners that are
+ // added to <input> and <texarea> still working as expected.
+ EventListenerFlags flags = mEditorBase->IsHTMLEditor()
+ ? TrustedEventsAtSystemGroupCapture()
+ : TrustedEventsAtSystemGroupBubble();
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ eventListenerManager->AddEventListenerByType(this, u"keydown"_ns, flags);
+ eventListenerManager->AddEventListenerByType(this, u"keyup"_ns, flags);
+#endif
+
+ eventListenerManager->AddEventListenerByType(this, u"keypress"_ns, flags);
+ eventListenerManager->AddEventListenerByType(this, u"dragover"_ns, flags);
+ eventListenerManager->AddEventListenerByType(this, u"dragleave"_ns, flags);
+ eventListenerManager->AddEventListenerByType(this, u"drop"_ns, flags);
+ // XXX We should add the mouse event listeners as system event group.
+ // E.g., web applications cannot prevent middle mouse paste by
+ // preventDefault() of click event at bubble phase.
+ // However, if we do so, all click handlers in any frames and frontend
+ // code need to check if it's editable. It makes easier create new bugs.
+ eventListenerManager->AddEventListenerByType(this, u"mousedown"_ns,
+ TrustedEventsAtCapture());
+ eventListenerManager->AddEventListenerByType(this, u"mouseup"_ns,
+ TrustedEventsAtCapture());
+ eventListenerManager->AddEventListenerByType(this, u"click"_ns,
+ TrustedEventsAtCapture());
+ eventListenerManager->AddEventListenerByType(
+ this, u"auxclick"_ns, TrustedEventsAtSystemGroupCapture());
+ // Focus event doesn't bubble so adding the listener to capturing phase as
+ // system event group.
+ eventListenerManager->AddEventListenerByType(
+ this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
+ eventListenerManager->AddEventListenerByType(
+ this, u"focus"_ns, TrustedEventsAtSystemGroupCapture());
+ eventListenerManager->AddEventListenerByType(
+ this, u"text"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->AddEventListenerByType(
+ this, u"compositionstart"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->AddEventListenerByType(
+ this, u"compositionend"_ns, TrustedEventsAtSystemGroupBubble());
+
+ return NS_OK;
+}
+
+void EditorEventListener::Disconnect() {
+ if (DetachedFromEditor()) {
+ return;
+ }
+ UninstallFromEditor();
+
+ const OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ mEditorBase = nullptr;
+
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm) {
+ nsIContent* focusedContent = fm->GetFocusedElement();
+ mozilla::dom::Element* root = editorBase->GetRoot();
+ if (focusedContent && root &&
+ focusedContent->IsInclusiveDescendantOf(root)) {
+ // Reset the Selection ancestor limiter and SelectionController state
+ // that EditorBase::InitializeSelection set up.
+ DebugOnly<nsresult> rvIgnored = editorBase->FinalizeSelection();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::FinalizeSelection() failed, but ignored");
+ }
+ }
+}
+
+void EditorEventListener::UninstallFromEditor() {
+ CleanupDragDropCaret();
+
+ EventTarget* eventTarget = mEditorBase->GetDOMEventTarget();
+ if (NS_WARN_IF(!eventTarget)) {
+ return;
+ }
+
+ EventListenerManager* eventListenerManager =
+ eventTarget->GetOrCreateListenerManager();
+ if (NS_WARN_IF(!eventListenerManager)) {
+ return;
+ }
+
+ EventListenerFlags flags = mEditorBase->IsHTMLEditor()
+ ? TrustedEventsAtSystemGroupCapture()
+ : TrustedEventsAtSystemGroupBubble();
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ eventListenerManager->RemoveEventListenerByType(this, u"keydown"_ns, flags);
+ eventListenerManager->RemoveEventListenerByType(this, u"keyup"_ns, flags);
+#endif
+ eventListenerManager->RemoveEventListenerByType(this, u"keypress"_ns, flags);
+ eventListenerManager->RemoveEventListenerByType(this, u"dragover"_ns, flags);
+ eventListenerManager->RemoveEventListenerByType(this, u"dragleave"_ns, flags);
+ eventListenerManager->RemoveEventListenerByType(this, u"drop"_ns, flags);
+ eventListenerManager->RemoveEventListenerByType(this, u"mousedown"_ns,
+ TrustedEventsAtCapture());
+ eventListenerManager->RemoveEventListenerByType(this, u"mouseup"_ns,
+ TrustedEventsAtCapture());
+ eventListenerManager->RemoveEventListenerByType(this, u"click"_ns,
+ TrustedEventsAtCapture());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"auxclick"_ns, TrustedEventsAtSystemGroupCapture());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"blur"_ns, TrustedEventsAtSystemGroupCapture());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"focus"_ns, TrustedEventsAtSystemGroupCapture());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"text"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"compositionstart"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"compositionend"_ns, TrustedEventsAtSystemGroupBubble());
+}
+
+PresShell* EditorEventListener::GetPresShell() const {
+ MOZ_ASSERT(!DetachedFromEditor());
+ return mEditorBase->GetPresShell();
+}
+
+nsPresContext* EditorEventListener::GetPresContext() const {
+ PresShell* presShell = GetPresShell();
+ return presShell ? presShell->GetPresContext() : nullptr;
+}
+
+bool EditorEventListener::EditorHasFocus() {
+ MOZ_ASSERT(!DetachedFromEditor());
+ const Element* focusedElement = mEditorBase->GetFocusedElement();
+ return focusedElement && focusedElement->IsInComposedDoc();
+}
+
+NS_IMPL_ISUPPORTS(EditorEventListener, nsIDOMEventListener)
+
+bool EditorEventListener::DetachedFromEditor() const { return !mEditorBase; }
+
+bool EditorEventListener::DetachedFromEditorOrDefaultPrevented(
+ WidgetEvent* aWidgetEvent) const {
+ return NS_WARN_IF(!aWidgetEvent) || DetachedFromEditor() ||
+ aWidgetEvent->DefaultPrevented();
+}
+
+bool EditorEventListener::EnsureCommitComposition() {
+ MOZ_ASSERT(!DetachedFromEditor());
+ RefPtr<EditorBase> editorBase(mEditorBase);
+ editorBase->CommitComposition();
+ return !DetachedFromEditor();
+}
+
+NS_IMETHODIMP EditorEventListener::HandleEvent(Event* aEvent) {
+ // Let's handle each event with the message of the internal event of the
+ // coming event. If the DOM event was created with improper interface,
+ // e.g., keydown event is created with |new MouseEvent("keydown", {});|,
+ // its message is always 0. Therefore, we can ban such strange event easy.
+ // However, we need to handle strange "focus" and "blur" event. See the
+ // following code of this switch statement.
+ // NOTE: Each event handler may require specific event interface. Before
+ // calling it, this queries the specific interface. If it would fail,
+ // each event handler would just ignore the event. So, in this method,
+ // you don't need to check if the QI succeeded before each call.
+ WidgetEvent* internalEvent = aEvent->WidgetEventPtr();
+
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ // For nested documents with multiple HTMLEditor registered on different
+ // nsWindowRoot, make sure the HTMLEditor for the original event target
+ // handles the events.
+ if (mEditorBase->IsHTMLEditor()) {
+ nsCOMPtr<nsINode> originalEventTargetNode =
+ nsINode::FromEventTargetOrNull(aEvent->GetOriginalTarget());
+
+ if (originalEventTargetNode &&
+ mEditorBase != originalEventTargetNode->OwnerDoc()->GetHTMLEditor()) {
+ return NS_OK;
+ }
+ if (!originalEventTargetNode && internalEvent->mMessage == eFocus &&
+ aEvent->GetCurrentTarget()->IsRootWindow()) {
+ return NS_OK;
+ }
+ }
+
+ switch (internalEvent->mMessage) {
+ // dragover and drop
+ case eDragOver:
+ case eDrop: {
+ // The editor which is registered on nsWindowRoot shouldn't handle
+ // drop events when it can be handled by Input or TextArea element on
+ // the chain.
+ if (aEvent->GetCurrentTarget()->IsRootWindow() &&
+ TextControlElement::FromEventTargetOrNull(
+ internalEvent->GetDOMEventTarget())) {
+ return NS_OK;
+ }
+ // aEvent should be grabbed by the caller since this is
+ // nsIDOMEventListener method. However, our clang plugin cannot check it
+ // if we use Event::As*Event(). So, we need to grab it by ourselves.
+ RefPtr<DragEvent> dragEvent = aEvent->AsDragEvent();
+ nsresult rv = DragOverOrDrop(dragEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::DragOverOrDrop() failed");
+ return rv;
+ }
+ // DragLeave
+ case eDragLeave: {
+ RefPtr<DragEvent> dragEvent = aEvent->AsDragEvent();
+ nsresult rv = DragLeave(dragEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::DragLeave() failed");
+ return rv;
+ }
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ // keydown
+ case eKeyDown: {
+ nsresult rv = KeyDown(internalEvent->AsKeyboardEvent());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::KeyDown() failed");
+ return rv;
+ }
+ // keyup
+ case eKeyUp: {
+ nsresult rv = KeyUp(internalEvent->AsKeyboardEvent());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::KeyUp() failed");
+ return rv;
+ }
+#endif // #ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ // keypress
+ case eKeyPress: {
+ nsresult rv = KeyPress(internalEvent->AsKeyboardEvent());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::KeyPress() failed");
+ return rv;
+ }
+ // mousedown
+ case eMouseDown: {
+ // EditorEventListener may receive (1) all mousedown, mouseup and click
+ // events, (2) only mousedown event or (3) only mouseup event.
+ // mMouseDownOrUpConsumedByIME is used only for ignoring click event if
+ // preceding mousedown and/or mouseup event is consumed by IME.
+ // Therefore, even if case #2 or case #3 occurs,
+ // mMouseDownOrUpConsumedByIME is true here. Therefore, we should always
+ // overwrite it here.
+ mMouseDownOrUpConsumedByIME =
+ NotifyIMEOfMouseButtonEvent(internalEvent->AsMouseEvent());
+ if (mMouseDownOrUpConsumedByIME) {
+ return NS_OK;
+ }
+ RefPtr<MouseEvent> mouseEvent = aEvent->AsMouseEvent();
+ if (NS_WARN_IF(!mouseEvent)) {
+ return NS_OK;
+ }
+ nsresult rv = MouseDown(mouseEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::MouseDown() failed");
+ return rv;
+ }
+ // mouseup
+ case eMouseUp: {
+ // See above comment in the eMouseDown case, first.
+ // This code assumes that case #1 is occuring. However, if case #3 may
+ // occurs after case #2 and the mousedown is consumed,
+ // mMouseDownOrUpConsumedByIME is true even though EditorEventListener
+ // has not received the preceding mousedown event of this mouseup event.
+ // So, mMouseDownOrUpConsumedByIME may be invalid here. However,
+ // this is not a matter because mMouseDownOrUpConsumedByIME is referred
+ // only by eMouseClick case but click event is fired only in case #1.
+ // So, before a click event is fired, mMouseDownOrUpConsumedByIME is
+ // always initialized in the eMouseDown case if it's referred.
+ if (NotifyIMEOfMouseButtonEvent(internalEvent->AsMouseEvent())) {
+ mMouseDownOrUpConsumedByIME = true;
+ }
+ if (mMouseDownOrUpConsumedByIME) {
+ return NS_OK;
+ }
+ RefPtr<MouseEvent> mouseEvent = aEvent->AsMouseEvent();
+ if (NS_WARN_IF(!mouseEvent)) {
+ return NS_OK;
+ }
+ nsresult rv = MouseUp(mouseEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::MouseUp() failed");
+ return rv;
+ }
+ // click
+ case eMouseClick: {
+ WidgetMouseEvent* widgetMouseEvent = internalEvent->AsMouseEvent();
+ // Don't handle non-primary click events
+ if (widgetMouseEvent->mButton != MouseButton::ePrimary) {
+ return NS_OK;
+ }
+ [[fallthrough]];
+ }
+ // auxclick
+ case eMouseAuxClick: {
+ WidgetMouseEvent* widgetMouseEvent = internalEvent->AsMouseEvent();
+ if (NS_WARN_IF(!widgetMouseEvent)) {
+ return NS_OK;
+ }
+ // If the preceding mousedown event or mouseup event was consumed,
+ // editor shouldn't handle this click event.
+ if (mMouseDownOrUpConsumedByIME) {
+ mMouseDownOrUpConsumedByIME = false;
+ widgetMouseEvent->PreventDefault();
+ return NS_OK;
+ }
+ nsresult rv = MouseClick(widgetMouseEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::MouseClick() failed");
+ return rv;
+ }
+ // focus
+ case eFocus: {
+ const InternalFocusEvent* focusEvent = internalEvent->AsFocusEvent();
+ if (NS_WARN_IF(!focusEvent)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = Focus(*focusEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::Focus() failed");
+ return rv;
+ }
+ // blur
+ case eBlur: {
+ const InternalFocusEvent* blurEvent = internalEvent->AsFocusEvent();
+ if (NS_WARN_IF(!blurEvent)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = Blur(*blurEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::Blur() failed");
+ return rv;
+ }
+ // text
+ case eCompositionChange: {
+ nsresult rv =
+ HandleChangeComposition(internalEvent->AsCompositionEvent());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorEventListener::HandleChangeComposition() failed");
+ return rv;
+ }
+ // compositionstart
+ case eCompositionStart: {
+ nsresult rv = HandleStartComposition(internalEvent->AsCompositionEvent());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorEventListener::HandleStartComposition() failed");
+ return rv;
+ }
+ // compositionend
+ case eCompositionEnd: {
+ HandleEndComposition(internalEvent->AsCompositionEvent());
+ return NS_OK;
+ }
+ default:
+ break;
+ }
+
+#ifdef DEBUG
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+ nsPrintfCString assertMessage(
+ "Editor doesn't handle \"%s\" event "
+ "because its internal event doesn't have proper message",
+ NS_ConvertUTF16toUTF8(eventType).get());
+ NS_ASSERTION(false, assertMessage.get());
+#endif
+
+ return NS_OK;
+}
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+
+// This function is borrowed from Chromium's ImeInput::IsCtrlShiftPressed
+bool IsCtrlShiftPressed(const WidgetKeyboardEvent* aKeyboardEvent,
+ bool& isRTL) {
+ MOZ_ASSERT(aKeyboardEvent);
+ // To check if a user is pressing only a control key and a right-shift key
+ // (or a left-shift key), we use the steps below:
+ // 1. Check if a user is pressing a control key and a right-shift key (or
+ // a left-shift key).
+ // 2. If the condition 1 is true, we should check if there are any other
+ // keys pressed at the same time.
+ // To ignore the keys checked in 1, we set their status to 0 before
+ // checking the key status.
+
+ if (!aKeyboardEvent->IsControl()) {
+ return false;
+ }
+
+ switch (aKeyboardEvent->mLocation) {
+ case eKeyLocationRight:
+ isRTL = true;
+ break;
+ case eKeyLocationLeft:
+ isRTL = false;
+ break;
+ default:
+ return false;
+ }
+
+ // Scan the key status to find pressed keys. We should abandon changing the
+ // text direction when there are other pressed keys.
+ return !aKeyboardEvent->IsAlt() && !aKeyboardEvent->IsMeta();
+}
+
+// This logic is mostly borrowed from Chromium's
+// RenderWidgetHostViewWin::OnKeyEvent.
+
+nsresult EditorEventListener::KeyUp(const WidgetKeyboardEvent* aKeyboardEvent) {
+ if (NS_WARN_IF(!aKeyboardEvent) || DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ if (!mHaveBidiKeyboards) {
+ return NS_OK;
+ }
+
+ // XXX Why doesn't this method check if it's consumed?
+ RefPtr<EditorBase> editorBase(mEditorBase);
+ if ((aKeyboardEvent->mKeyCode == NS_VK_SHIFT ||
+ aKeyboardEvent->mKeyCode == NS_VK_CONTROL) &&
+ mShouldSwitchTextDirection &&
+ (editorBase->IsTextEditor() ||
+ editorBase->AsHTMLEditor()->IsPlaintextMailComposer())) {
+ editorBase->SwitchTextDirectionTo(mSwitchToRTL
+ ? EditorBase::TextDirection::eRTL
+ : EditorBase::TextDirection::eLTR);
+ mShouldSwitchTextDirection = false;
+ }
+ return NS_OK;
+}
+
+nsresult EditorEventListener::KeyDown(
+ const WidgetKeyboardEvent* aKeyboardEvent) {
+ if (NS_WARN_IF(!aKeyboardEvent) || DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ if (!mHaveBidiKeyboards) {
+ return NS_OK;
+ }
+
+ // XXX Why isn't this method check if it's consumed?
+ if (aKeyboardEvent->mKeyCode == NS_VK_SHIFT) {
+ bool switchToRTL;
+ if (IsCtrlShiftPressed(aKeyboardEvent, switchToRTL)) {
+ mShouldSwitchTextDirection = true;
+ mSwitchToRTL = switchToRTL;
+ }
+ } else if (aKeyboardEvent->mKeyCode != NS_VK_CONTROL) {
+ // In case the user presses any other key besides Ctrl and Shift
+ mShouldSwitchTextDirection = false;
+ }
+ return NS_OK;
+}
+
+#endif // #ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+
+nsresult EditorEventListener::KeyPress(WidgetKeyboardEvent* aKeyboardEvent) {
+ if (NS_WARN_IF(!aKeyboardEvent)) {
+ return NS_OK;
+ }
+
+ RefPtr<EditorBase> editorBase(mEditorBase);
+ if (!editorBase->IsAcceptableInputEvent(aKeyboardEvent) ||
+ DetachedFromEditorOrDefaultPrevented(aKeyboardEvent)) {
+ return NS_OK;
+ }
+
+ // The exposed root of our editor may have been hidden or destroyed by a
+ // preceding event listener. However, the destruction has not occurred yet if
+ // pending notifications have not been flushed yet. Therefore, before
+ // handling user input, we need to get the latest state and if it's now
+ // destroyed with the flushing, we should just ignore this event instead of
+ // returning error since this is just a event listener.
+ RefPtr<Document> document = editorBase->GetDocument();
+ if (!document) {
+ return NS_OK;
+ }
+ document->FlushPendingNotifications(FlushType::Layout);
+ if (editorBase->Destroyed() || DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ nsresult rv = editorBase->HandleKeyPressEvent(aKeyboardEvent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::HandleKeyPressEvent() failed");
+ return rv;
+ }
+
+ auto GetWidget = [&]() -> nsIWidget* {
+ if (aKeyboardEvent->mWidget) {
+ return aKeyboardEvent->mWidget;
+ }
+ // If the event is created by chrome script, the widget is always nullptr.
+ return IMEStateManager::GetWidgetForTextInputHandling();
+ };
+
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ if (LookAndFeel::GetInt(LookAndFeel::IntID::HideCursorWhileTyping)) {
+ if (nsPresContext* pc = GetPresContext()) {
+ if (nsIWidget* widget = GetWidget()) {
+ pc->EventStateManager()->StartHidingCursorWhileTyping(widget);
+ }
+ }
+ }
+
+ if (aKeyboardEvent->DefaultPrevented()) {
+ return NS_OK;
+ }
+
+ if (!ShouldHandleNativeKeyBindings(aKeyboardEvent)) {
+ return NS_OK;
+ }
+
+ // Now, ask the native key bindings to handle the event.
+
+ nsIWidget* widget = GetWidget();
+ if (NS_WARN_IF(!widget)) {
+ return NS_OK;
+ }
+
+ RefPtr<Document> doc = editorBase->GetDocument();
+
+ // WidgetKeyboardEvent::ExecuteEditCommands() requires non-nullptr mWidget.
+ // If the event is created by chrome script, it is nullptr but we need to
+ // execute native key bindings. Therefore, we need to set widget to
+ // WidgetEvent::mWidget temporarily.
+ AutoRestore<nsCOMPtr<nsIWidget>> saveWidget(aKeyboardEvent->mWidget);
+ aKeyboardEvent->mWidget = widget;
+ if (aKeyboardEvent->ExecuteEditCommands(NativeKeyBindingsType::RichTextEditor,
+ DoCommandCallback, doc)) {
+ aKeyboardEvent->PreventDefault();
+ }
+ return NS_OK;
+}
+
+nsresult EditorEventListener::MouseClick(WidgetMouseEvent* aMouseClickEvent) {
+ if (NS_WARN_IF(!aMouseClickEvent) || DetachedFromEditor()) {
+ return NS_OK;
+ }
+ // nothing to do if editor isn't editable or clicked on out of the editor.
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ if (editorBase->IsReadonly() ||
+ !editorBase->IsAcceptableInputEvent(aMouseClickEvent)) {
+ return NS_OK;
+ }
+
+ // Notifies clicking on editor to IMEStateManager even when the event was
+ // consumed.
+ if (EditorHasFocus()) {
+ if (RefPtr<nsPresContext> presContext = GetPresContext()) {
+ RefPtr<Element> focusedElement = mEditorBase->GetFocusedElement();
+ IMEStateManager::OnClickInEditor(*presContext, focusedElement,
+ *aMouseClickEvent);
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+ }
+ }
+
+ if (DetachedFromEditorOrDefaultPrevented(aMouseClickEvent)) {
+ // We're done if 'preventdefault' is true (see for example bug 70698).
+ return NS_OK;
+ }
+
+ // If we got a mouse down inside the editing area, we should force the
+ // IME to commit before we change the cursor position.
+ if (!EnsureCommitComposition()) {
+ return NS_OK;
+ }
+
+ // XXX The following code is hack for our buggy "click" and "auxclick"
+ // implementation. "auxclick" event was added recently, however,
+ // any non-primary button click event handlers in our UI still keep
+ // listening to "click" events. Additionally, "auxclick" event is
+ // fired after "click" events and even if we do this in the system event
+ // group, middle click opens new tab before us. Therefore, we need to
+ // handle middle click at capturing phase of the default group even
+ // though this makes web apps cannot prevent middle click paste with
+ // calling preventDefault() of "click" nor "auxclick".
+
+ if (aMouseClickEvent->mButton != MouseButton::eMiddle ||
+ !WidgetMouseEvent::IsMiddleClickPasteEnabled()) {
+ return NS_OK;
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return NS_OK;
+ }
+ nsPresContext* presContext = GetPresContext();
+ if (NS_WARN_IF(!presContext)) {
+ return NS_OK;
+ }
+ MOZ_ASSERT(!aMouseClickEvent->DefaultPrevented());
+ nsEventStatus status = nsEventStatus_eIgnore;
+ RefPtr<EventStateManager> esm = presContext->EventStateManager();
+ DebugOnly<nsresult> rvIgnored = esm->HandleMiddleClickPaste(
+ presShell, aMouseClickEvent, &status, editorBase);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EventStateManager::HandleMiddleClickPaste() failed, but ignored");
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ // We no longer need to StopImmediatePropagation here since
+ // ClickHandlerChild.sys.mjs checks for and ignores editables, so won't
+ // re-handle the event
+ aMouseClickEvent->PreventDefault();
+ }
+ return NS_OK;
+}
+
+bool EditorEventListener::NotifyIMEOfMouseButtonEvent(
+ WidgetMouseEvent* aMouseEvent) {
+ MOZ_ASSERT(aMouseEvent);
+
+ if (!EditorHasFocus()) {
+ return false;
+ }
+
+ RefPtr<nsPresContext> presContext = GetPresContext();
+ if (NS_WARN_IF(!presContext)) {
+ return false;
+ }
+ RefPtr<Element> focusedElement = mEditorBase->GetFocusedElement();
+ return IMEStateManager::OnMouseButtonEventInEditor(
+ *presContext, focusedElement, *aMouseEvent);
+}
+
+nsresult EditorEventListener::MouseDown(MouseEvent* aMouseEvent) {
+ // FYI: We don't need to check if it's already consumed here because
+ // we need to commit composition at mouse button operation.
+ // FYI: This may be called by HTMLEditorEventListener::MouseDown() even
+ // when the event is not acceptable for committing composition.
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+ Unused << EnsureCommitComposition();
+ return NS_OK;
+}
+
+/**
+ * Drag event implementation
+ */
+
+void EditorEventListener::RefuseToDropAndHideCaret(DragEvent* aDragEvent) {
+ MOZ_ASSERT(aDragEvent->WidgetEventPtr()->mFlags.mInSystemGroup);
+
+ aDragEvent->PreventDefault();
+ aDragEvent->StopImmediatePropagation();
+ DataTransfer* dataTransfer = aDragEvent->GetDataTransfer();
+ if (dataTransfer) {
+ dataTransfer->SetDropEffectInt(nsIDragService::DRAGDROP_ACTION_NONE);
+ }
+ if (mCaret) {
+ mCaret->SetVisible(false);
+ }
+}
+
+nsresult EditorEventListener::DragOverOrDrop(DragEvent* aDragEvent) {
+ MOZ_ASSERT(aDragEvent);
+ MOZ_ASSERT(aDragEvent->WidgetEventPtr()->mMessage == eDrop ||
+ aDragEvent->WidgetEventPtr()->mMessage == eDragOver);
+
+ if (aDragEvent->WidgetEventPtr()->mMessage == eDrop) {
+ CleanupDragDropCaret();
+ MOZ_ASSERT(!mCaret);
+ } else {
+ InitializeDragDropCaret();
+ MOZ_ASSERT(mCaret);
+ }
+
+ if (DetachedFromEditorOrDefaultPrevented(aDragEvent->WidgetEventPtr())) {
+ return NS_OK;
+ }
+
+ int32_t dropOffset = -1;
+ nsCOMPtr<nsIContent> dropParentContent =
+ aDragEvent->GetRangeParentContentAndOffset(&dropOffset);
+ if (NS_WARN_IF(!dropParentContent) || NS_WARN_IF(dropOffset < 0)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (DetachedFromEditor()) {
+ RefuseToDropAndHideCaret(aDragEvent);
+ return NS_OK;
+ }
+
+ bool notEditable =
+ !dropParentContent->IsEditable() || mEditorBase->IsReadonly();
+
+ // First of all, hide caret if we won't insert the drop data into the editor
+ // obviously.
+ if (mCaret && (IsFileControlTextBox() || notEditable)) {
+ mCaret->SetVisible(false);
+ }
+
+ // If we're a native anonymous <input> element in <input type="file">,
+ // we don't need to handle the drop.
+ if (IsFileControlTextBox()) {
+ return NS_OK;
+ }
+
+ // If the drop target isn't ediable, the drop should be handled by the
+ // element.
+ if (notEditable) {
+ // If we're a text control element which is readonly or disabled,
+ // we should refuse to drop.
+ if (mEditorBase->IsTextEditor()) {
+ RefuseToDropAndHideCaret(aDragEvent);
+ return NS_OK;
+ }
+ // Otherwise, we shouldn't handle the drop.
+ return NS_OK;
+ }
+
+ // If the drag event does not have any data which we can handle, we should
+ // refuse to drop even if some parents can handle it because user may be
+ // trying to drop it on us, not our parent. For example, users don't want
+ // to drop HTML data to parent contenteditable element if they drop it on
+ // a child <input> element.
+ if (!DragEventHasSupportingData(aDragEvent)) {
+ RefuseToDropAndHideCaret(aDragEvent);
+ return NS_OK;
+ }
+
+ // If we don't accept the data drop at the point, for example, while dragging
+ // selection, it's not allowed dropping on its selection ranges. In this
+ // case, any parents shouldn't handle the drop instead of us, for example,
+ // dropping text shouldn't be treated as URL and load new page.
+ if (!CanInsertAtDropPosition(aDragEvent)) {
+ RefuseToDropAndHideCaret(aDragEvent);
+ return NS_OK;
+ }
+
+ WidgetDragEvent* asWidgetEvent = aDragEvent->WidgetEventPtr()->AsDragEvent();
+ AutoRestore<bool> inHTMLEditorEventListener(
+ asWidgetEvent->mInHTMLEditorEventListener);
+ if (mEditorBase->IsHTMLEditor()) {
+ asWidgetEvent->mInHTMLEditorEventListener = true;
+ }
+ aDragEvent->PreventDefault();
+
+ aDragEvent->StopImmediatePropagation();
+
+ if (asWidgetEvent->mMessage == eDrop) {
+ RefPtr<EditorBase> editorBase = mEditorBase;
+ nsresult rv = editorBase->HandleDropEvent(aDragEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::HandleDropEvent() failed");
+ return rv;
+ }
+
+ MOZ_ASSERT(asWidgetEvent->mMessage == eDragOver);
+
+ // If we handle the dragged item, we need to adjust drop effect here
+ // because once DataTransfer is retrieved, DragEvent has initialized it
+ // with nsContentUtils::SetDataTransferInEvent() but it does not check
+ // whether the content is movable or not.
+ DataTransfer* dataTransfer = aDragEvent->GetDataTransfer();
+ if (dataTransfer &&
+ dataTransfer->DropEffectInt() == nsIDragService::DRAGDROP_ACTION_MOVE) {
+ nsCOMPtr<nsINode> dragSource = dataTransfer->GetMozSourceNode();
+ if (dragSource && !dragSource->IsEditable()) {
+ // In this case, we shouldn't allow "move" because the drag source
+ // isn't editable.
+ dataTransfer->SetDropEffectInt(
+ nsContentUtils::FilterDropEffect(nsIDragService::DRAGDROP_ACTION_COPY,
+ dataTransfer->EffectAllowedInt()));
+ }
+ }
+
+ if (!mCaret) {
+ return NS_OK;
+ }
+
+ mCaret->SetVisible(true);
+ mCaret->SetCaretPosition(dropParentContent, dropOffset);
+
+ return NS_OK;
+}
+
+void EditorEventListener::InitializeDragDropCaret() {
+ if (mCaret) {
+ return;
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return;
+ }
+
+ mCaret = new nsCaret();
+ DebugOnly<nsresult> rvIgnored = mCaret->Init(presShell);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsCaret::Init() failed, but ignored");
+ mCaret->SetCaretReadOnly(true);
+ // This is to avoid the requirement that the Selection is Collapsed which
+ // it can't be when dragging a selection in the same shell.
+ // See nsCaret::IsVisible().
+ mCaret->SetVisibilityDuringSelection(true);
+
+ presShell->SetCaret(mCaret);
+}
+
+void EditorEventListener::CleanupDragDropCaret() {
+ if (!mCaret) {
+ return;
+ }
+
+ mCaret->SetVisible(false); // hide it, so that it turns off its timer
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (presShell) {
+ presShell->RestoreCaret();
+ }
+
+ mCaret->Terminate();
+ mCaret = nullptr;
+}
+
+nsresult EditorEventListener::DragLeave(DragEvent* aDragEvent) {
+ // XXX If aDragEvent was created by chrome script, its defaultPrevented
+ // may be true, though. We shouldn't handle such event but we don't
+ // have a way to distinguish if coming event is created by chrome script.
+ NS_WARNING_ASSERTION(!aDragEvent->WidgetEventPtr()->DefaultPrevented(),
+ "eDragLeave shouldn't be cancelable");
+ if (NS_WARN_IF(!aDragEvent) || DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ CleanupDragDropCaret();
+
+ return NS_OK;
+}
+
+bool EditorEventListener::DragEventHasSupportingData(
+ DragEvent* aDragEvent) const {
+ MOZ_ASSERT(
+ !DetachedFromEditorOrDefaultPrevented(aDragEvent->WidgetEventPtr()));
+ MOZ_ASSERT(aDragEvent->GetDataTransfer());
+
+ // Plaintext editors only support dropping text. Otherwise, HTML and files
+ // can be dropped as well.
+ DataTransfer* dataTransfer = aDragEvent->GetDataTransfer();
+ if (!dataTransfer) {
+ NS_WARNING("No data transfer returned");
+ return false;
+ }
+ return dataTransfer->HasType(NS_LITERAL_STRING_FROM_CSTRING(kTextMime)) ||
+ dataTransfer->HasType(
+ NS_LITERAL_STRING_FROM_CSTRING(kMozTextInternal)) ||
+ (mEditorBase->IsHTMLEditor() &&
+ !mEditorBase->AsHTMLEditor()->IsPlaintextMailComposer() &&
+ (dataTransfer->HasType(NS_LITERAL_STRING_FROM_CSTRING(kHTMLMime)) ||
+ dataTransfer->HasType(NS_LITERAL_STRING_FROM_CSTRING(kFileMime))));
+}
+
+bool EditorEventListener::CanInsertAtDropPosition(DragEvent* aDragEvent) {
+ MOZ_ASSERT(
+ !DetachedFromEditorOrDefaultPrevented(aDragEvent->WidgetEventPtr()));
+ MOZ_ASSERT(!mEditorBase->IsReadonly());
+ MOZ_ASSERT(DragEventHasSupportingData(aDragEvent));
+
+ DataTransfer* dataTransfer = aDragEvent->GetDataTransfer();
+ if (NS_WARN_IF(!dataTransfer)) {
+ return false;
+ }
+
+ // If there is no source node, this is probably an external drag and the
+ // drop is allowed. The later checks rely on checking if the drag target
+ // is the same as the drag source.
+ nsCOMPtr<nsINode> sourceNode = dataTransfer->GetMozSourceNode();
+ if (!sourceNode) {
+ return true;
+ }
+
+ // There is a source node, so compare the source documents and this document.
+ // Disallow drops on the same document.
+
+ RefPtr<Document> targetDocument = mEditorBase->GetDocument();
+ if (NS_WARN_IF(!targetDocument)) {
+ return false;
+ }
+
+ RefPtr<Document> sourceDocument = sourceNode->OwnerDoc();
+
+ // If the source and the dest are not same document, allow to drop it always.
+ if (targetDocument != sourceDocument) {
+ return true;
+ }
+
+ // If the source node is a remote browser, treat this as coming from a
+ // different document and allow the drop.
+ if (BrowserParent::GetFrom(nsIContent::FromNode(sourceNode))) {
+ return true;
+ }
+
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ if (!selection) {
+ return false;
+ }
+
+ // If selection is collapsed, allow to drop it always.
+ if (selection->IsCollapsed()) {
+ return true;
+ }
+
+ int32_t dropOffset = -1;
+ nsCOMPtr<nsIContent> dropParentContent =
+ aDragEvent->GetRangeParentContentAndOffset(&dropOffset);
+ if (!dropParentContent || NS_WARN_IF(dropOffset < 0) ||
+ NS_WARN_IF(DetachedFromEditor())) {
+ return false;
+ }
+
+ return !nsContentUtils::IsPointInSelection(*selection, *dropParentContent,
+ dropOffset);
+}
+
+nsresult EditorEventListener::HandleStartComposition(
+ WidgetCompositionEvent* aCompositionStartEvent) {
+ if (NS_WARN_IF(!aCompositionStartEvent)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+ RefPtr<EditorBase> editorBase(mEditorBase);
+ if (!editorBase->IsAcceptableInputEvent(aCompositionStartEvent)) {
+ return NS_OK;
+ }
+ // Although, "compositionstart" should be cancelable, but currently,
+ // eCompositionStart event coming from widget is not cancelable.
+ MOZ_ASSERT(!aCompositionStartEvent->DefaultPrevented(),
+ "eCompositionStart shouldn't be cancelable");
+ nsresult rv = editorBase->OnCompositionStart(*aCompositionStartEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::OnCompositionStart() failed");
+ return rv;
+}
+
+nsresult EditorEventListener::HandleChangeComposition(
+ WidgetCompositionEvent* aCompositionChangeEvent) {
+ if (NS_WARN_IF(!aCompositionChangeEvent)) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(!aCompositionChangeEvent->DefaultPrevented(),
+ "eCompositionChange event shouldn't be cancelable");
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+ RefPtr<EditorBase> editorBase(mEditorBase);
+ if (!editorBase->IsAcceptableInputEvent(aCompositionChangeEvent)) {
+ return NS_OK;
+ }
+
+ // if we are readonly, then do nothing.
+ if (editorBase->IsReadonly()) {
+ return NS_OK;
+ }
+
+ nsresult rv = editorBase->OnCompositionChange(*aCompositionChangeEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::OnCompositionChange() failed");
+ return rv;
+}
+
+void EditorEventListener::HandleEndComposition(
+ WidgetCompositionEvent* aCompositionEndEvent) {
+ if (NS_WARN_IF(!aCompositionEndEvent) || DetachedFromEditor()) {
+ return;
+ }
+ RefPtr<EditorBase> editorBase(mEditorBase);
+ if (!editorBase->IsAcceptableInputEvent(aCompositionEndEvent)) {
+ return;
+ }
+ MOZ_ASSERT(!aCompositionEndEvent->DefaultPrevented(),
+ "eCompositionEnd shouldn't be cancelable");
+
+ editorBase->OnCompositionEnd(*aCompositionEndEvent);
+}
+
+nsresult EditorEventListener::Focus(const InternalFocusEvent& aFocusEvent) {
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsINode> originalEventTargetNode =
+ nsINode::FromEventTargetOrNull(aFocusEvent.GetOriginalDOMEventTarget());
+ if (NS_WARN_IF(!originalEventTargetNode)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // If the target is a document node but it's not editable, we should
+ // ignore it because actual focused element's event is going to come.
+ if (originalEventTargetNode->IsDocument()) {
+ if (!originalEventTargetNode->IsInDesignMode()) {
+ return NS_OK;
+ }
+ }
+ // We should not receive focus events whose target is not a content node
+ // unless the node is a document node.
+ else if (NS_WARN_IF(!originalEventTargetNode->IsContent())) {
+ return NS_OK;
+ }
+
+ const OwningNonNull<EditorBase> editorBase(*mEditorBase);
+ DebugOnly<nsresult> rvIgnored = editorBase->OnFocus(*originalEventTargetNode);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "EditorBase::OnFocus() failed");
+ return NS_OK; // Don't return error code to the event listener manager.
+}
+
+nsresult EditorEventListener::Blur(const InternalFocusEvent& aBlurEvent) {
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ DebugOnly<nsresult> rvIgnored = mEditorBase->OnBlur(aBlurEvent.mTarget);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "EditorBase::OnBlur() failed");
+ return NS_OK; // Don't return error code to the event listener manager.
+}
+
+bool EditorEventListener::IsFileControlTextBox() {
+ MOZ_ASSERT(!DetachedFromEditor());
+
+ RefPtr<EditorBase> editorBase(mEditorBase);
+ Element* rootElement = editorBase->GetRoot();
+ if (!rootElement || !rootElement->ChromeOnlyAccess()) {
+ return false;
+ }
+ nsIContent* parent = rootElement->FindFirstNonChromeOnlyAccessContent();
+ if (!parent || !parent->IsHTMLElement(nsGkAtoms::input)) {
+ return false;
+ }
+ nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(parent);
+ return formControl->ControlType() == FormControlType::InputFile;
+}
+
+bool EditorEventListener::ShouldHandleNativeKeyBindings(
+ WidgetKeyboardEvent* aKeyboardEvent) {
+ MOZ_ASSERT(!DetachedFromEditor());
+
+ // Only return true if the target of the event is a desendant of the active
+ // editing host in order to match the similar decision made in
+ // nsXBLWindowKeyHandler.
+ // Note that IsAcceptableInputEvent doesn't check for the active editing
+ // host for keyboard events, otherwise this check would have been
+ // unnecessary. IsAcceptableInputEvent currently makes a similar check for
+ // mouse events.
+
+ nsCOMPtr<nsIContent> targetContent = nsIContent::FromEventTargetOrNull(
+ aKeyboardEvent->GetOriginalDOMEventTarget());
+ if (NS_WARN_IF(!targetContent)) {
+ return false;
+ }
+
+ RefPtr<HTMLEditor> htmlEditor = HTMLEditor::GetFrom(mEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+
+ if (htmlEditor->IsInDesignMode()) {
+ // Don't need to perform any checks in designMode documents.
+ return true;
+ }
+
+ nsIContent* editingHost = htmlEditor->ComputeEditingHost();
+ if (!editingHost) {
+ return false;
+ }
+
+ return targetContent->IsInclusiveDescendantOf(editingHost);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorEventListener.h b/editor/libeditor/EditorEventListener.h
new file mode 100644
index 0000000000..33af96f4ba
--- /dev/null
+++ b/editor/libeditor/EditorEventListener.h
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EditorEventListener_h
+#define EditorEventListener_h
+
+#include "EditorForwards.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "nsIDOMEventListener.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+
+class nsCaret;
+class nsIContent;
+class nsPresContext;
+
+// X.h defines KeyPress
+#ifdef KeyPress
+# undef KeyPress
+#endif
+
+#ifdef XP_WIN
+// On Windows, we support switching the text direction by pressing Ctrl+Shift
+# define HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+#endif
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class DragEvent;
+class MouseEvent;
+} // namespace dom
+
+class EditorEventListener : public nsIDOMEventListener {
+ public:
+ EditorEventListener();
+
+ virtual nsresult Connect(EditorBase* aEditorBase);
+
+ virtual void Disconnect();
+
+ /**
+ * DetachedFromEditor() returns true if editor was detached.
+ * Otherwise, false.
+ */
+ [[nodiscard]] bool DetachedFromEditor() const;
+
+ NS_DECL_ISUPPORTS
+
+ // nsIDOMEventListener
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(dom::Event* aEvent) override;
+
+ protected:
+ virtual ~EditorEventListener();
+
+ nsresult InstallToEditor();
+ void UninstallFromEditor();
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ nsresult KeyDown(const WidgetKeyboardEvent* aKeyboardEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult KeyUp(const WidgetKeyboardEvent* aKeyboardEvent);
+#endif
+ MOZ_CAN_RUN_SCRIPT nsresult KeyPress(WidgetKeyboardEvent* aKeyboardEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult
+ HandleChangeComposition(WidgetCompositionEvent* aCompositionEvent);
+ nsresult HandleStartComposition(WidgetCompositionEvent* aCompositionEvent);
+ MOZ_CAN_RUN_SCRIPT void HandleEndComposition(
+ WidgetCompositionEvent* aCompositionEvent);
+ MOZ_CAN_RUN_SCRIPT virtual nsresult MouseDown(dom::MouseEvent* aMouseEvent);
+ MOZ_CAN_RUN_SCRIPT virtual nsresult MouseUp(dom::MouseEvent* aMouseEvent) {
+ return NS_OK;
+ }
+ MOZ_CAN_RUN_SCRIPT virtual nsresult MouseClick(
+ WidgetMouseEvent* aMouseClickEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult Focus(const InternalFocusEvent& aFocusEvent);
+ nsresult Blur(const InternalFocusEvent& aBlurEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult DragOverOrDrop(dom::DragEvent* aDragEvent);
+ nsresult DragLeave(dom::DragEvent* aDragEvent);
+
+ void RefuseToDropAndHideCaret(dom::DragEvent* aDragEvent);
+ bool DragEventHasSupportingData(dom::DragEvent* aDragEvent) const;
+ MOZ_CAN_RUN_SCRIPT bool CanInsertAtDropPosition(dom::DragEvent* aDragEvent);
+ void InitializeDragDropCaret();
+ void CleanupDragDropCaret();
+ PresShell* GetPresShell() const;
+ nsPresContext* GetPresContext() const;
+ // Returns true if IME consumes the mouse event.
+ MOZ_CAN_RUN_SCRIPT bool NotifyIMEOfMouseButtonEvent(
+ WidgetMouseEvent* aMouseEvent);
+ bool EditorHasFocus();
+ bool IsFileControlTextBox();
+ bool ShouldHandleNativeKeyBindings(WidgetKeyboardEvent* aKeyboardEvent);
+
+ /**
+ * DetachedFromEditorOrDefaultPrevented() returns true if editor was detached
+ * and/or the event was consumed. Otherwise, i.e., attached editor can
+ * handle the event, returns true.
+ */
+ bool DetachedFromEditorOrDefaultPrevented(WidgetEvent* aEvent) const;
+
+ /**
+ * EnsureCommitComposition() requests to commit composition if there is.
+ * Returns false if the editor is detached from the listener, i.e.,
+ * impossible to continue to handle the event. Otherwise, true.
+ */
+ [[nodiscard]] bool EnsureCommitComposition();
+
+ EditorBase* mEditorBase; // weak
+ RefPtr<nsCaret> mCaret;
+ bool mCommitText;
+ bool mInTransaction;
+ bool mMouseDownOrUpConsumedByIME;
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ bool mHaveBidiKeyboards;
+ bool mShouldSwitchTextDirection;
+ bool mSwitchToRTL;
+#endif
+};
+
+} // namespace mozilla
+
+#endif // #ifndef EditorEventListener_h
diff --git a/editor/libeditor/EditorForwards.h b/editor/libeditor/EditorForwards.h
new file mode 100644
index 0000000000..80223a6db5
--- /dev/null
+++ b/editor/libeditor/EditorForwards.h
@@ -0,0 +1,168 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditorHelperForwards_h
+#define mozilla_EditorHelperForwards_h
+
+#include "mozilla/EnumSet.h"
+
+#include <cstdint>
+
+/******************************************************************************
+ * Forward declarations of other modules'
+ ******************************************************************************/
+class nsIContent;
+class nsINode;
+
+template <typename T>
+class nsCOMPtr;
+
+template <typename T>
+class RefPtr;
+
+namespace mozilla {
+
+template <typename V, typename E>
+class Result;
+
+template <typename T>
+class OwningNonNull;
+
+namespace dom {
+class Element;
+class Text;
+} // namespace dom
+
+/******************************************************************************
+ * enum classes
+ ******************************************************************************/
+
+enum class BlockInlineCheck : uint8_t; // HTMLEditHelpers.h
+enum class CollectChildrenOption; // HTMLEditUtils.h
+enum class EditAction; // mozilla/EditAction.h
+enum class EditorCommandParamType : uint16_t; // mozilla/EditorCommands.h
+enum class EditSubAction : int32_t; // mozilla/EditAction.h
+enum class ParagraphSeparator; // mozilla/HTMLEditor.h
+enum class SpecifiedStyle : uint8_t; // mozilla/PendingStyles.h
+enum class SuggestCaret; // EditorUtils.h
+enum class WithTransaction; // HTMLEditHelpers.h
+
+/******************************************************************************
+ * enum sets
+ ******************************************************************************/
+
+using CollectChildrenOptions = EnumSet<CollectChildrenOption>;
+using SuggestCaretOptions = EnumSet<SuggestCaret>;
+
+/******************************************************************************
+ * classes or structs which are required for template classes/structs
+ ******************************************************************************/
+
+template <typename PT, typename CT>
+class EditorDOMPointBase; // mozilla/EditorDOMPoint.h
+
+using EditorDOMPoint =
+ EditorDOMPointBase<nsCOMPtr<nsINode>, nsCOMPtr<nsIContent>>;
+using EditorRawDOMPoint = EditorDOMPointBase<nsINode*, nsIContent*>;
+using EditorDOMPointInText = EditorDOMPointBase<RefPtr<dom::Text>, nsIContent*>;
+using EditorRawDOMPointInText = EditorDOMPointBase<dom::Text*, nsIContent*>;
+
+/******************************************************************************
+ * classes
+ ******************************************************************************/
+
+class AutoPendingStyleCacheArray; // mozilla/PendingStyles.h
+class EditTransactionBase; // mozilla/EditTransactionBase.h
+class EditorBase; // mozilla/EditorBase.h
+class HTMLEditor; // mozilla/HTMLEditor.h
+class ManualNACPtr; // mozilla/ManualNAC.h
+class PendingStyle; // mozilla/PendingStyles.h
+class PendingStyleCache; // mozilla/PendingStyles.h
+class PendingStyles; // mozilla/PendingStyles.h
+class RangeUpdater; // mozilla/SelectionState.h
+class SelectionState; // mozilla/SelectionState.h
+class TextEditor; // mozilla/TextEditor.h
+
+class AutoRangeArray; // AutoRangeArray.h
+class AutoSelectionRangeArray; // EditorUtils.h
+class CaretPoint; // EditorUtils.h
+class ChangeAttributeTransaction; // ChangeAttributeTransaction.h
+class ChangeStyleTransaction; // ChangeStyleTransaction.h
+class CompositionTransaction; // CompositionTransaction.h
+class CSSEditUtils; // CSSEditUtils.h
+class DeleteContentTransactionBase; // DeleteContentTransactionBase.h
+class DeleteMultipleRangesTransaction; // DeleteMultipleRangesTransaction.h
+class DeleteNodeTransaction; // DeleteNodeTransaction.h
+class DeleteRangeTransaction; // DeleteRangeTransaction.h
+class DeleteTextTransaction; // DeleteTextTransaction.h
+class EditActionResult; // EditorUtils.h
+class EditAggregateTransaction; // EditAggregateTransaction.h
+class EditorEventListener; // EditorEventListener.h
+class EditResult; // HTMLEditHelpers.h
+class HTMLEditorEventListener; // HTMLEditorEventListener.h
+class InsertNodeTransaction; // InsertNodeTransaction.h
+class InsertTextResult; // EditorUtils.h
+class InsertTextTransaction; // InsertTextTransaction.h
+class InterCiter; // InterCiter.h
+class JoinNodesResult; // HTMLEditHelpers.h
+class JoinNodesTransaction; // JoinNodesTransaction.h
+class MoveNodeResult; // HTMLEditHelpers.h
+class MoveNodeTransaction; // MoveNodeTransaction.h
+class PlaceholderTransaction; // PlaceholderTransaction.h
+class ReplaceTextTransaction; // ReplaceTextTransaction.h
+class SplitNodeResult; // HTMLEditHelpers.h
+class SplitNodeTransaction; // SplitNodeTransaction.h
+class SplitRangeOffFromNodeResult; // HTMLEditHelpers.h
+class SplitRangeOffResult; // HTMLEditHelpers.h
+class WhiteSpaceVisibilityKeeper; // WSRunObject.h
+class WSRunScanner; // WSRunObject.h
+class WSScanResult; // WSRunObject.h
+
+/******************************************************************************
+ * structs
+ ******************************************************************************/
+
+class EditorElementStyle; // HTMLEditHelpers.h
+struct EditorInlineStyle; // HTMLEditHelpers.h
+struct EditorInlineStyleAndValue; // HTMLEditHelpers.h
+struct RangeItem; // mozilla/SelectionState.h
+
+/******************************************************************************
+ * template classes
+ ******************************************************************************/
+
+template <typename EditorDOMPointType>
+class EditorDOMRangeBase; // mozilla/EditorDOMPoint.h
+
+template <typename NodeType>
+class CreateNodeResultBase; // EditorUtils.h
+
+template <typename EditorDOMPointType>
+class ReplaceRangeDataBase; // EditorUtils.h
+
+/******************************************************************************
+ * aliases
+ ******************************************************************************/
+
+using CreateContentResult = CreateNodeResultBase<nsIContent>;
+using CreateElementResult = CreateNodeResultBase<dom::Element>;
+using CreateTextResult = CreateNodeResultBase<dom::Text>;
+
+// InsertParagraphResult is an alias of CreateElementResult because it returns
+// new paragraph from point of view of users (i.e., right paragraph if split)
+// instead of newly created paragraph element.
+using InsertParagraphResult = CreateElementResult;
+
+using EditorDOMRange = EditorDOMRangeBase<EditorDOMPoint>;
+using EditorRawDOMRange = EditorDOMRangeBase<EditorRawDOMPoint>;
+using EditorDOMRangeInTexts = EditorDOMRangeBase<EditorDOMPointInText>;
+using EditorRawDOMRangeInTexts = EditorDOMRangeBase<EditorRawDOMPointInText>;
+
+using ReplaceRangeData = ReplaceRangeDataBase<EditorDOMPoint>;
+using ReplaceRangeInTextsData = ReplaceRangeDataBase<EditorDOMPointInText>;
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_EditorHelperForwards_h
diff --git a/editor/libeditor/EditorUtils.cpp b/editor/libeditor/EditorUtils.cpp
new file mode 100644
index 0000000000..d9df9338e0
--- /dev/null
+++ b/editor/libeditor/EditorUtils.cpp
@@ -0,0 +1,445 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditorUtils.h"
+
+#include "EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc
+#include "HTMLEditHelpers.h" // for MoveNodeResult
+#include "HTMLEditUtils.h" // for HTMLEditUtils
+#include "TextEditor.h" // for TextEditor
+
+#include "mozilla/ComputedStyle.h" // for ComputedStyle
+#include "mozilla/IntegerRange.h" // for IntegerRange
+#include "mozilla/dom/Document.h" // for dom::Document
+#include "mozilla/dom/Selection.h" // for dom::Selection
+#include "mozilla/dom/Text.h" // for dom::Text
+
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle
+#include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
+#include "nsFrameSelection.h" // for nsFrameSelection
+#include "nsIContent.h" // for nsIContent
+#include "nsINode.h" // for nsINode
+#include "nsITransferable.h" // for nsITransferable
+#include "nsRange.h" // for nsRange
+#include "nsStyleConsts.h" // for StyleWhiteSpace
+#include "nsStyleStruct.h" // for nsStyleText, etc
+
+namespace mozilla {
+
+using namespace dom;
+
+/******************************************************************************
+ * mozilla::EditActionResult
+ *****************************************************************************/
+
+EditActionResult& EditActionResult::operator|=(
+ const MoveNodeResult& aMoveNodeResult) {
+ mHandled |= aMoveNodeResult.Handled();
+ return *this;
+}
+
+/******************************************************************************
+ * some general purpose editor utils
+ *****************************************************************************/
+
+bool EditorUtils::IsDescendantOf(const nsINode& aNode, const nsINode& aParent,
+ EditorRawDOMPoint* aOutPoint /* = nullptr */) {
+ if (aOutPoint) {
+ aOutPoint->Clear();
+ }
+
+ if (&aNode == &aParent) {
+ return false;
+ }
+
+ for (const nsINode* node = &aNode; node; node = node->GetParentNode()) {
+ if (node->GetParentNode() == &aParent) {
+ if (aOutPoint) {
+ MOZ_ASSERT(node->IsContent());
+ aOutPoint->Set(node->AsContent());
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool EditorUtils::IsDescendantOf(const nsINode& aNode, const nsINode& aParent,
+ EditorDOMPoint* aOutPoint) {
+ MOZ_ASSERT(aOutPoint);
+ aOutPoint->Clear();
+ if (&aNode == &aParent) {
+ return false;
+ }
+
+ for (const nsINode* node = &aNode; node; node = node->GetParentNode()) {
+ if (node->GetParentNode() == &aParent) {
+ MOZ_ASSERT(node->IsContent());
+ aOutPoint->Set(node->AsContent());
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// static
+Maybe<std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode>>
+EditorUtils::GetComputedWhiteSpaceStyles(const nsIContent& aContent) {
+ if (MOZ_UNLIKELY(!aContent.IsElement() && !aContent.GetParentElement())) {
+ return Nothing();
+ }
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(
+ aContent.IsElement() ? aContent.AsElement()
+ : aContent.GetParentElement());
+ if (NS_WARN_IF(!elementStyle)) {
+ return Nothing();
+ }
+ const auto* styleText = elementStyle->StyleText();
+ return Some(
+ std::pair(styleText->mWhiteSpaceCollapse, styleText->mTextWrapMode));
+}
+
+// static
+bool EditorUtils::IsWhiteSpacePreformatted(const nsIContent& aContent) {
+ // Look at the node (and its parent if it's not an element), and grab its
+ // ComputedStyle.
+ Element* element = aContent.GetAsElementOrParentElement();
+ if (!element) {
+ return false;
+ }
+
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(element);
+ if (!elementStyle) {
+ // Consider nodes without a ComputedStyle to be NOT preformatted:
+ // For instance, this is true of JS tags inside the body (which show
+ // up as #text nodes but have no ComputedStyle).
+ return false;
+ }
+
+ return elementStyle->StyleText()->WhiteSpaceIsSignificant();
+}
+
+// static
+bool EditorUtils::IsNewLinePreformatted(const nsIContent& aContent) {
+ // Look at the node (and its parent if it's not an element), and grab its
+ // ComputedStyle.
+ Element* element = aContent.GetAsElementOrParentElement();
+ if (!element) {
+ return false;
+ }
+
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(element);
+ if (!elementStyle) {
+ // Consider nodes without a ComputedStyle to be NOT preformatted:
+ // For instance, this is true of JS tags inside the body (which show
+ // up as #text nodes but have no ComputedStyle).
+ return false;
+ }
+
+ return elementStyle->StyleText()->NewlineIsSignificantStyle();
+}
+
+// static
+bool EditorUtils::IsOnlyNewLinePreformatted(const nsIContent& aContent) {
+ // Look at the node (and its parent if it's not an element), and grab its
+ // ComputedStyle.
+ Element* element = aContent.GetAsElementOrParentElement();
+ if (!element) {
+ return false;
+ }
+
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(element);
+ if (!elementStyle) {
+ // Consider nodes without a ComputedStyle to be NOT preformatted:
+ // For instance, this is true of JS tags inside the body (which show
+ // up as #text nodes but have no ComputedStyle).
+ return false;
+ }
+
+ return elementStyle->StyleText()->mWhiteSpaceCollapse ==
+ StyleWhiteSpaceCollapse::PreserveBreaks;
+}
+
+// static
+Result<nsCOMPtr<nsITransferable>, nsresult>
+EditorUtils::CreateTransferableForPlainText(const Document& aDocument) {
+ // Create generic Transferable for getting the data
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("do_CreateInstance() failed to create nsITransferable instance");
+ return Err(rv);
+ }
+
+ if (!transferable) {
+ NS_WARNING("do_CreateInstance() returned nullptr, but ignored");
+ return nsCOMPtr<nsITransferable>();
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ transferable->Init(aDocument.GetLoadContext());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::Init() failed, but ignored");
+
+ rvIgnored = transferable->AddDataFlavor(kTextMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kTextMime) failed, but ignored");
+ rvIgnored = transferable->AddDataFlavor(kMozTextInternal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kMozTextInternal) failed, but ignored");
+ return transferable;
+}
+
+/******************************************************************************
+ * mozilla::EditorDOMPointBase
+ *****************************************************************************/
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsCharCollapsibleASCIISpace);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsCharCollapsibleASCIISpace() const {
+ if (IsCharNewLine()) {
+ return !EditorUtils::IsNewLinePreformatted(*ContainerAs<Text>());
+ }
+ return IsCharASCIISpace() &&
+ !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsCharCollapsibleNBSP);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsCharCollapsibleNBSP() const {
+ // TODO: Perhaps, we should return false if neither previous char nor
+ // next char is collapsible white-space or NBSP.
+ return IsCharNBSP() &&
+ !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool,
+ IsCharCollapsibleASCIISpaceOrNBSP);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsCharCollapsibleASCIISpaceOrNBSP() const {
+ if (IsCharNewLine()) {
+ return !EditorUtils::IsNewLinePreformatted(*ContainerAs<Text>());
+ }
+ return IsCharASCIISpaceOrNBSP() &&
+ !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(
+ bool, IsPreviousCharCollapsibleASCIISpace);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsPreviousCharCollapsibleASCIISpace() const {
+ if (IsPreviousCharNewLine()) {
+ return !EditorUtils::IsNewLinePreformatted(*ContainerAs<Text>());
+ }
+ return IsPreviousCharASCIISpace() &&
+ !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool,
+ IsPreviousCharCollapsibleNBSP);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsPreviousCharCollapsibleNBSP() const {
+ return IsPreviousCharNBSP() &&
+ !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(
+ bool, IsPreviousCharCollapsibleASCIISpaceOrNBSP);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsPreviousCharCollapsibleASCIISpaceOrNBSP()
+ const {
+ if (IsPreviousCharNewLine()) {
+ return !EditorUtils::IsNewLinePreformatted(*ContainerAs<Text>());
+ }
+ return IsPreviousCharASCIISpaceOrNBSP() &&
+ !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool,
+ IsNextCharCollapsibleASCIISpace);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsNextCharCollapsibleASCIISpace() const {
+ if (IsNextCharNewLine()) {
+ return !EditorUtils::IsNewLinePreformatted(*ContainerAs<Text>());
+ }
+ return IsNextCharASCIISpace() &&
+ !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsNextCharCollapsibleNBSP);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsNextCharCollapsibleNBSP() const {
+ return IsNextCharNBSP() &&
+ !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(
+ bool, IsNextCharCollapsibleASCIISpaceOrNBSP);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsNextCharCollapsibleASCIISpaceOrNBSP() const {
+ if (IsNextCharNewLine()) {
+ return !EditorUtils::IsNewLinePreformatted(*ContainerAs<Text>());
+ }
+ return IsNextCharASCIISpaceOrNBSP() &&
+ !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsCharPreformattedNewLine);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsCharPreformattedNewLine() const {
+ return IsCharNewLine() &&
+ EditorUtils::IsNewLinePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(
+ bool, IsCharPreformattedNewLineCollapsedWithWhiteSpaces);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<
+ PT, CT>::IsCharPreformattedNewLineCollapsedWithWhiteSpaces() const {
+ return IsCharNewLine() &&
+ EditorUtils::IsOnlyNewLinePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool,
+ IsPreviousCharPreformattedNewLine);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsPreviousCharPreformattedNewLine() const {
+ return IsPreviousCharNewLine() &&
+ EditorUtils::IsNewLinePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(
+ bool, IsPreviousCharPreformattedNewLineCollapsedWithWhiteSpaces);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<
+ PT, CT>::IsPreviousCharPreformattedNewLineCollapsedWithWhiteSpaces() const {
+ return IsPreviousCharNewLine() &&
+ EditorUtils::IsOnlyNewLinePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool,
+ IsNextCharPreformattedNewLine);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<PT, CT>::IsNextCharPreformattedNewLine() const {
+ return IsNextCharNewLine() &&
+ EditorUtils::IsNewLinePreformatted(*ContainerAs<Text>());
+}
+
+NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(
+ bool, IsNextCharPreformattedNewLineCollapsedWithWhiteSpaces);
+
+template <typename PT, typename CT>
+bool EditorDOMPointBase<
+ PT, CT>::IsNextCharPreformattedNewLineCollapsedWithWhiteSpaces() const {
+ return IsNextCharNewLine() &&
+ EditorUtils::IsOnlyNewLinePreformatted(*ContainerAs<Text>());
+}
+
+/******************************************************************************
+ * mozilla::EditorDOMRangeBase
+ *****************************************************************************/
+
+NS_INSTANTIATE_EDITOR_DOM_RANGE_CONST_METHOD(nsINode*,
+ GetClosestCommonInclusiveAncestor);
+
+template <typename EditorDOMPointType>
+nsINode* EditorDOMRangeBase<
+ EditorDOMPointType>::GetClosestCommonInclusiveAncestor() const {
+ if (NS_WARN_IF(!IsPositioned())) {
+ return nullptr;
+ }
+ return nsContentUtils::GetClosestCommonInclusiveAncestor(
+ mStart.GetContainer(), mEnd.GetContainer());
+}
+
+/******************************************************************************
+ * mozilla::CaretPoint
+ *****************************************************************************/
+
+nsresult CaretPoint::SuggestCaretPointTo(
+ EditorBase& aEditorBase, const SuggestCaretOptions& aOptions) const {
+ mHandledCaretPoint = true;
+ if (!mCaretPoint.IsSet()) {
+ if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion)) {
+ return NS_OK;
+ }
+ NS_WARNING("There was no suggestion to put caret");
+ return NS_ERROR_FAILURE;
+ }
+ if (aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt) &&
+ !aEditorBase.AllowsTransactionsToChangeSelection()) {
+ return NS_OK;
+ }
+ nsresult rv = aEditorBase.CollapseSelectionTo(mCaretPoint);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ return aOptions.contains(SuggestCaret::AndIgnoreTrivialError) && NS_FAILED(rv)
+ ? NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR
+ : rv;
+}
+
+bool CaretPoint::CopyCaretPointTo(EditorDOMPoint& aPointToPutCaret,
+ const EditorBase& aEditorBase,
+ const SuggestCaretOptions& aOptions) const {
+ MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError));
+ mHandledCaretPoint = true;
+ if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) &&
+ !mCaretPoint.IsSet()) {
+ return false;
+ }
+ if (aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt) &&
+ !aEditorBase.AllowsTransactionsToChangeSelection()) {
+ return false;
+ }
+ aPointToPutCaret = mCaretPoint;
+ return true;
+}
+
+bool CaretPoint::MoveCaretPointTo(EditorDOMPoint& aPointToPutCaret,
+ const EditorBase& aEditorBase,
+ const SuggestCaretOptions& aOptions) {
+ MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError));
+ mHandledCaretPoint = true;
+ if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) &&
+ !mCaretPoint.IsSet()) {
+ return false;
+ }
+ if (aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt) &&
+ !aEditorBase.AllowsTransactionsToChangeSelection()) {
+ return false;
+ }
+ aPointToPutCaret = UnwrapCaretPoint();
+ return true;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/EditorUtils.h b/editor/libeditor/EditorUtils.h
new file mode 100644
index 0000000000..c6b08952dd
--- /dev/null
+++ b/editor/libeditor/EditorUtils.h
@@ -0,0 +1,477 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_EditorUtils_h
+#define mozilla_EditorUtils_h
+
+#include "mozilla/EditorBase.h" // for EditorBase
+#include "mozilla/EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc
+#include "mozilla/EditorForwards.h"
+#include "mozilla/IntegerRange.h" // for IntegerRange
+#include "mozilla/Maybe.h" // for Maybe
+#include "mozilla/Result.h" // for Result<>
+#include "mozilla/dom/Element.h" // for dom::Element
+#include "mozilla/dom/HTMLBRElement.h" // for dom::HTMLBRElement
+#include "mozilla/dom/Selection.h" // for dom::Selection
+#include "mozilla/dom/Text.h" // for dom::Text
+
+#include "nsAtom.h" // for nsStaticAtom
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsDebug.h" // for NS_WARNING, etc
+#include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
+#include "nsRange.h" // for nsRange
+#include "nsString.h" // for nsAString, nsString, etc
+
+class nsITransferable;
+
+namespace mozilla {
+
+enum class StyleWhiteSpace : uint8_t;
+
+enum class SuggestCaret {
+ // If specified, the method returns NS_OK when there is no recommended caret
+ // position.
+ OnlyIfHasSuggestion,
+ // If specified and if EditorBase::AllowsTransactionsToChangeSelection
+ // returns false, the method does nothing and returns NS_OK.
+ OnlyIfTransactionsAllowedToDoIt,
+ // If specified, the method returns
+ // NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR even if
+ // EditorBase::CollapseSelectionTo returns an error except when
+ // NS_ERROR_EDITOR_DESTROYED.
+ AndIgnoreTrivialError,
+};
+
+/******************************************************************************
+ * CaretPoint is a wrapper of EditorDOMPoint and provides a helper method to
+ * collapse Selection there, or move it to a local variable. This is typically
+ * used as the ok type of Result or a base class of DoSomethingResult classes.
+ ******************************************************************************/
+class MOZ_STACK_CLASS CaretPoint {
+ public:
+ explicit CaretPoint(const EditorDOMPoint& aPointToPutCaret)
+ : mCaretPoint(aPointToPutCaret) {}
+ explicit CaretPoint(EditorDOMPoint&& aPointToPutCaret)
+ : mCaretPoint(std::move(aPointToPutCaret)) {}
+
+ CaretPoint(const CaretPoint&) = delete;
+ CaretPoint& operator=(const CaretPoint&) = delete;
+ CaretPoint(CaretPoint&&) = default;
+ CaretPoint& operator=(CaretPoint&&) = default;
+
+ /**
+ * Suggest caret position to aEditorBase.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SuggestCaretPointTo(
+ EditorBase& aEditorBase, const SuggestCaretOptions& aOptions) const;
+
+ /**
+ * IgnoreCaretPointSuggestion() should be called if the method does not want
+ * to use caret position recommended by this instance.
+ */
+ void IgnoreCaretPointSuggestion() const { mHandledCaretPoint = true; }
+
+ /**
+ * When propagating the result, it may not want to the caller modify
+ * selection. In such case, this can clear the caret point. Use
+ * IgnoreCaretPointSuggestion() in the caller side instead.
+ */
+ void ForgetCaretPointSuggestion() { mCaretPoint.Clear(); }
+
+ bool HasCaretPointSuggestion() const { return mCaretPoint.IsSet(); }
+ constexpr const EditorDOMPoint& CaretPointRef() const { return mCaretPoint; }
+ constexpr EditorDOMPoint&& UnwrapCaretPoint() {
+ mHandledCaretPoint = true;
+ return std::move(mCaretPoint);
+ }
+ bool CopyCaretPointTo(EditorDOMPoint& aPointToPutCaret,
+ const SuggestCaretOptions& aOptions) const {
+ MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError));
+ MOZ_ASSERT(
+ !aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt));
+ mHandledCaretPoint = true;
+ if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) &&
+ !mCaretPoint.IsSet()) {
+ return false;
+ }
+ aPointToPutCaret = mCaretPoint;
+ return true;
+ }
+ bool MoveCaretPointTo(EditorDOMPoint& aPointToPutCaret,
+ const SuggestCaretOptions& aOptions) {
+ MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError));
+ MOZ_ASSERT(
+ !aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt));
+ if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) &&
+ !mCaretPoint.IsSet()) {
+ return false;
+ }
+ aPointToPutCaret = UnwrapCaretPoint();
+ return true;
+ }
+ bool CopyCaretPointTo(EditorDOMPoint& aPointToPutCaret,
+ const EditorBase& aEditorBase,
+ const SuggestCaretOptions& aOptions) const;
+ bool MoveCaretPointTo(EditorDOMPoint& aPointToPutCaret,
+ const EditorBase& aEditorBase,
+ const SuggestCaretOptions& aOptions);
+
+ protected:
+ constexpr bool CaretPointHandled() const { return mHandledCaretPoint; }
+
+ void SetCaretPoint(const EditorDOMPoint& aCaretPoint) {
+ mHandledCaretPoint = false;
+ mCaretPoint = aCaretPoint;
+ }
+ void SetCaretPoint(EditorDOMPoint&& aCaretPoint) {
+ mHandledCaretPoint = false;
+ mCaretPoint = std::move(aCaretPoint);
+ }
+
+ void UnmarkAsHandledCaretPoint() { mHandledCaretPoint = true; }
+
+ CaretPoint() = default;
+
+ private:
+ EditorDOMPoint mCaretPoint;
+ bool mutable mHandledCaretPoint = false;
+};
+
+/***************************************************************************
+ * EditActionResult is useful to return the handling state of edit sub actions
+ * without out params.
+ */
+class MOZ_STACK_CLASS EditActionResult final {
+ public:
+ bool Canceled() const { return mCanceled; }
+ bool Handled() const { return mHandled; }
+ bool Ignored() const { return !mCanceled && !mHandled; }
+
+ void MarkAsCanceled() { mCanceled = true; }
+ void MarkAsHandled() { mHandled = true; }
+
+ EditActionResult& operator|=(const EditActionResult& aOther) {
+ mCanceled |= aOther.mCanceled;
+ mHandled |= aOther.mHandled;
+ return *this;
+ }
+
+ EditActionResult& operator|=(const MoveNodeResult& aMoveNodeResult);
+
+ static EditActionResult IgnoredResult() {
+ return EditActionResult(false, false);
+ }
+ static EditActionResult HandledResult() {
+ return EditActionResult(false, true);
+ }
+ static EditActionResult CanceledResult() {
+ return EditActionResult(true, true);
+ }
+
+ EditActionResult(const EditActionResult&) = delete;
+ EditActionResult& operator=(const EditActionResult&) = delete;
+ EditActionResult(EditActionResult&&) = default;
+ EditActionResult& operator=(EditActionResult&&) = default;
+
+ private:
+ bool mCanceled = false;
+ bool mHandled = false;
+
+ EditActionResult(bool aCanceled, bool aHandled)
+ : mCanceled(aCanceled), mHandled(aHandled) {}
+
+ EditActionResult() : mCanceled(false), mHandled(false) {}
+};
+
+/***************************************************************************
+ * CreateNodeResultBase is a simple class for CreateSomething() methods
+ * which want to return new node.
+ */
+template <typename NodeType>
+class MOZ_STACK_CLASS CreateNodeResultBase final : public CaretPoint {
+ using SelfType = CreateNodeResultBase<NodeType>;
+
+ public:
+ bool Handled() const { return mNode; }
+ NodeType* GetNewNode() const { return mNode; }
+ RefPtr<NodeType> UnwrapNewNode() { return std::move(mNode); }
+
+ CreateNodeResultBase() = delete;
+ explicit CreateNodeResultBase(NodeType& aNode) : mNode(&aNode) {}
+ explicit CreateNodeResultBase(NodeType& aNode,
+ const EditorDOMPoint& aCandidateCaretPoint)
+ : CaretPoint(aCandidateCaretPoint), mNode(&aNode) {}
+ explicit CreateNodeResultBase(NodeType& aNode,
+ EditorDOMPoint&& aCandidateCaretPoint)
+ : CaretPoint(std::move(aCandidateCaretPoint)), mNode(&aNode) {}
+
+ explicit CreateNodeResultBase(RefPtr<NodeType>&& aNode)
+ : mNode(std::move(aNode)) {}
+ explicit CreateNodeResultBase(RefPtr<NodeType>&& aNode,
+ const EditorDOMPoint& aCandidateCaretPoint)
+ : CaretPoint(aCandidateCaretPoint), mNode(std::move(aNode)) {
+ MOZ_ASSERT(mNode);
+ }
+ explicit CreateNodeResultBase(RefPtr<NodeType>&& aNode,
+ EditorDOMPoint&& aCandidateCaretPoint)
+ : CaretPoint(std::move(aCandidateCaretPoint)), mNode(std::move(aNode)) {
+ MOZ_ASSERT(mNode);
+ }
+
+ [[nodiscard]] static SelfType NotHandled() {
+ return SelfType(EditorDOMPoint());
+ }
+ [[nodiscard]] static SelfType NotHandled(
+ const EditorDOMPoint& aPointToPutCaret) {
+ SelfType result(aPointToPutCaret);
+ return result;
+ }
+ [[nodiscard]] static SelfType NotHandled(EditorDOMPoint&& aPointToPutCaret) {
+ SelfType result(std::move(aPointToPutCaret));
+ return result;
+ }
+
+#ifdef DEBUG
+ ~CreateNodeResultBase() {
+ MOZ_ASSERT(!HasCaretPointSuggestion() || CaretPointHandled());
+ }
+#endif
+
+ CreateNodeResultBase(const SelfType& aOther) = delete;
+ SelfType& operator=(const SelfType& aOther) = delete;
+ CreateNodeResultBase(SelfType&& aOther) = default;
+ SelfType& operator=(SelfType&& aOther) = default;
+
+ private:
+ explicit CreateNodeResultBase(const EditorDOMPoint& aCandidateCaretPoint)
+ : CaretPoint(aCandidateCaretPoint) {}
+ explicit CreateNodeResultBase(EditorDOMPoint&& aCandidateCaretPoint)
+ : CaretPoint(std::move(aCandidateCaretPoint)) {}
+
+ RefPtr<NodeType> mNode;
+};
+
+/**
+ * This is a result of inserting text. If the text inserted as a part of
+ * composition, this does not return CaretPoint. Otherwise, must return
+ * CaretPoint which is typically same as end of inserted text.
+ */
+class MOZ_STACK_CLASS InsertTextResult final : public CaretPoint {
+ public:
+ InsertTextResult() : CaretPoint(EditorDOMPoint()) {}
+ template <typename EditorDOMPointType>
+ explicit InsertTextResult(const EditorDOMPointType& aEndOfInsertedText)
+ : CaretPoint(EditorDOMPoint()),
+ mEndOfInsertedText(aEndOfInsertedText.template To<EditorDOMPoint>()) {}
+ explicit InsertTextResult(EditorDOMPointInText&& aEndOfInsertedText)
+ : CaretPoint(EditorDOMPoint()),
+ mEndOfInsertedText(std::move(aEndOfInsertedText)) {}
+ template <typename PT, typename CT>
+ InsertTextResult(EditorDOMPointInText&& aEndOfInsertedText,
+ const EditorDOMPointBase<PT, CT>& aCaretPoint)
+ : CaretPoint(aCaretPoint.template To<EditorDOMPoint>()),
+ mEndOfInsertedText(std::move(aEndOfInsertedText)) {}
+ InsertTextResult(EditorDOMPointInText&& aEndOfInsertedText,
+ CaretPoint&& aCaretPoint)
+ : CaretPoint(std::move(aCaretPoint)),
+ mEndOfInsertedText(std::move(aEndOfInsertedText)) {
+ UnmarkAsHandledCaretPoint();
+ }
+ InsertTextResult(InsertTextResult&& aOther, EditorDOMPoint&& aCaretPoint)
+ : CaretPoint(std::move(aCaretPoint)),
+ mEndOfInsertedText(std::move(aOther.mEndOfInsertedText)) {}
+
+ [[nodiscard]] bool Handled() const { return mEndOfInsertedText.IsSet(); }
+ const EditorDOMPointInText& EndOfInsertedTextRef() const {
+ return mEndOfInsertedText;
+ }
+
+ private:
+ EditorDOMPointInText mEndOfInsertedText;
+};
+
+/***************************************************************************
+ * stack based helper class for calling EditorBase::EndTransaction() after
+ * EditorBase::BeginTransaction(). This shouldn't be used in editor classes
+ * or helper classes while an edit action is being handled. Use
+ * AutoTransactionBatch in such cases since it uses non-virtual internal
+ * methods.
+ ***************************************************************************/
+class MOZ_RAII AutoTransactionBatchExternal final {
+ public:
+ MOZ_CAN_RUN_SCRIPT explicit AutoTransactionBatchExternal(
+ EditorBase& aEditorBase)
+ : mEditorBase(aEditorBase) {
+ MOZ_KnownLive(mEditorBase).BeginTransaction();
+ }
+
+ MOZ_CAN_RUN_SCRIPT ~AutoTransactionBatchExternal() {
+ MOZ_KnownLive(mEditorBase).EndTransaction();
+ }
+
+ private:
+ EditorBase& mEditorBase;
+};
+
+/******************************************************************************
+ * AutoSelectionRangeArray stores all ranges in `aSelection`.
+ * Note that modifying the ranges means modifing the selection ranges.
+ *****************************************************************************/
+class MOZ_STACK_CLASS AutoSelectionRangeArray final {
+ public:
+ explicit AutoSelectionRangeArray(dom::Selection& aSelection) {
+ for (const uint32_t i : IntegerRange(aSelection.RangeCount())) {
+ MOZ_ASSERT(aSelection.GetRangeAt(i));
+ mRanges.AppendElement(*aSelection.GetRangeAt(i));
+ }
+ }
+
+ AutoTArray<mozilla::OwningNonNull<nsRange>, 8> mRanges;
+};
+
+class EditorUtils final {
+ public:
+ using EditorType = EditorBase::EditorType;
+ using Selection = dom::Selection;
+
+ /**
+ * IsDescendantOf() checks if aNode is a child or a descendant of aParent.
+ * aOutPoint is set to the child of aParent.
+ *
+ * @return true if aNode is a child or a descendant of aParent.
+ */
+ static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent,
+ EditorRawDOMPoint* aOutPoint = nullptr);
+ static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent,
+ EditorDOMPoint* aOutPoint);
+
+ /**
+ * Returns true if aContent is a <br> element and it's marked as padding for
+ * empty editor.
+ */
+ static bool IsPaddingBRElementForEmptyEditor(const nsIContent& aContent) {
+ const dom::HTMLBRElement* brElement =
+ dom::HTMLBRElement::FromNode(&aContent);
+ return brElement && brElement->IsPaddingForEmptyEditor();
+ }
+
+ /**
+ * Returns true if aContent is a <br> element and it's marked as padding for
+ * empty last line.
+ */
+ static bool IsPaddingBRElementForEmptyLastLine(const nsIContent& aContent) {
+ const dom::HTMLBRElement* brElement =
+ dom::HTMLBRElement::FromNode(&aContent);
+ return brElement && brElement->IsPaddingForEmptyLastLine();
+ }
+
+ /**
+ * IsEditableContent() returns true if aContent's data or children is ediable
+ * for the given editor type. Be aware, returning true does NOT mean the
+ * node can be removed from its parent node, and returning false does NOT
+ * mean the node cannot be removed from the parent node.
+ * XXX May be the anonymous nodes in TextEditor not editable? If it's not
+ * so, we can get rid of aEditorType.
+ */
+ static bool IsEditableContent(const nsIContent& aContent,
+ EditorType aEditorType) {
+ if (aEditorType == EditorType::HTML &&
+ (!aContent.IsEditable() || !aContent.IsInComposedDoc())) {
+ // FIXME(emilio): Why only for HTML editors? All content from the root
+ // content in text editors is also editable, so afaict we can remove the
+ // special-case.
+ return false;
+ }
+ return IsElementOrText(aContent);
+ }
+
+ /**
+ * Returns true if aContent is a usual element node (not padding <br> element
+ * for empty editor) or a text node. In other words, returns true if
+ * aContent is a usual element node or visible data node.
+ */
+ static bool IsElementOrText(const nsIContent& aContent) {
+ if (aContent.IsText()) {
+ return true;
+ }
+ return aContent.IsElement() && !IsPaddingBRElementForEmptyEditor(aContent);
+ }
+
+ /**
+ * Get the two longhands that make up computed white-space style of aContent.
+ */
+ static Maybe<std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode>>
+ GetComputedWhiteSpaceStyles(const nsIContent& aContent);
+
+ /**
+ * IsWhiteSpacePreformatted() checks the style info for the node for the
+ * preformatted text style. This does NOT flush layout.
+ */
+ static bool IsWhiteSpacePreformatted(const nsIContent& aContent);
+
+ /**
+ * IsNewLinePreformatted() checks whether the linefeed characters are
+ * preformatted or collapsible white-spaces. This does NOT flush layout.
+ */
+ static bool IsNewLinePreformatted(const nsIContent& aContent);
+
+ /**
+ * IsOnlyNewLinePreformatted() checks whether the linefeed characters are
+ * preformated but white-spaces are collapsed, or otherwise. I.e., this
+ * returns true only when `white-space-collapse:pre-line`.
+ */
+ static bool IsOnlyNewLinePreformatted(const nsIContent& aContent);
+
+ static nsStaticAtom* GetTagNameAtom(const nsAString& aTagName) {
+ if (aTagName.IsEmpty()) {
+ return nullptr;
+ }
+ nsAutoString lowerTagName;
+ nsContentUtils::ASCIIToLower(aTagName, lowerTagName);
+ return NS_GetStaticAtom(lowerTagName);
+ }
+
+ static nsStaticAtom* GetAttributeAtom(const nsAString& aAttribute) {
+ if (aAttribute.IsEmpty()) {
+ return nullptr; // Don't use nsGkAtoms::_empty for attribute.
+ }
+ return NS_GetStaticAtom(aAttribute);
+ }
+
+ /**
+ * Helper method for deletion. When this returns true, Selection will be
+ * computed with nsFrameSelection that also requires flushed layout
+ * information.
+ */
+ template <typename SelectionOrAutoRangeArray>
+ static bool IsFrameSelectionRequiredToExtendSelection(
+ nsIEditor::EDirection aDirectionAndAmount,
+ SelectionOrAutoRangeArray& aSelectionOrAutoRangeArray) {
+ switch (aDirectionAndAmount) {
+ case nsIEditor::eNextWord:
+ case nsIEditor::ePreviousWord:
+ case nsIEditor::eToBeginningOfLine:
+ case nsIEditor::eToEndOfLine:
+ return true;
+ case nsIEditor::ePrevious:
+ case nsIEditor::eNext:
+ return aSelectionOrAutoRangeArray.IsCollapsed();
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Create an nsITransferable instance which has kTextMime and
+ * kMozTextInternal flavors.
+ */
+ static Result<nsCOMPtr<nsITransferable>, nsresult>
+ CreateTransferableForPlainText(const dom::Document& aDocument);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_EditorUtils_h
diff --git a/editor/libeditor/HTMLAbsPositionEditor.cpp b/editor/libeditor/HTMLAbsPositionEditor.cpp
new file mode 100644
index 0000000000..2378d243dd
--- /dev/null
+++ b/editor/libeditor/HTMLAbsPositionEditor.cpp
@@ -0,0 +1,993 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditor.h"
+
+#include <math.h>
+
+#include "CSSEditUtils.h"
+#include "EditAction.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditorEventListener.h"
+#include "HTMLEditUtils.h"
+
+#include "mozilla/EventListenerManager.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/EventTarget.h"
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsCOMPtr.h"
+#include "nsComputedDOMStyle.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsROCSSPrimitiveValue.h"
+#include "nsINode.h"
+#include "nsIPrincipal.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsUtils.h"
+#include "nsLiteralString.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStyledElement.h"
+#include "nscore.h"
+#include <algorithm>
+
+namespace mozilla {
+
+using namespace dom;
+
+nsresult HTMLEditor::SetSelectionToAbsoluteOrStaticAsAction(
+ bool aEnabled, nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eSetPositionToAbsoluteOrStatic, aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return rv;
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ if (aEnabled) {
+ Result<EditActionResult, nsresult> result =
+ SetSelectionToAbsoluteAsSubAction(*editingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::SetSelectionToAbsoluteAsSubAction() failed");
+ return result.unwrapErr();
+ }
+ return NS_OK;
+ }
+ Result<EditActionResult, nsresult> result = SetSelectionToStaticAsSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::SetSelectionToStaticAsSubAction() failed");
+ return result.unwrapErr();
+ }
+ return NS_OK;
+}
+
+already_AddRefed<Element>
+HTMLEditor::GetAbsolutelyPositionedSelectionContainer() const {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return nullptr;
+ }
+
+ Element* selectionContainerElement = GetSelectionContainerElement();
+ if (NS_WARN_IF(!selectionContainerElement)) {
+ return nullptr;
+ }
+
+ AutoTArray<RefPtr<Element>, 24> arrayOfParentElements;
+ for (Element* element :
+ selectionContainerElement->InclusiveAncestorsOfType<Element>()) {
+ arrayOfParentElements.AppendElement(element);
+ }
+
+ nsAutoString positionValue;
+ for (RefPtr<Element> element = selectionContainerElement; element;
+ element = element->GetParentElement()) {
+ if (element->IsHTMLElement(nsGkAtoms::html)) {
+ NS_WARNING(
+ "HTMLEditor::GetAbsolutelyPositionedSelectionContainer() reached "
+ "<html> element");
+ return nullptr;
+ }
+ nsCOMPtr<nsINode> parentNode = element->GetParentNode();
+ nsresult rv = CSSEditUtils::GetComputedProperty(
+ MOZ_KnownLive(*element), *nsGkAtoms::position, positionValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::position) failed");
+ return nullptr;
+ }
+ if (NS_WARN_IF(Destroyed()) ||
+ NS_WARN_IF(parentNode != element->GetParentNode())) {
+ return nullptr;
+ }
+ if (positionValue.EqualsLiteral("absolute")) {
+ return element.forget();
+ }
+ }
+ return nullptr;
+}
+
+NS_IMETHODIMP HTMLEditor::GetAbsolutePositioningEnabled(bool* aIsEnabled) {
+ *aIsEnabled = IsAbsolutePositionEditorEnabled();
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::SetAbsolutePositioningEnabled(bool aIsEnabled) {
+ EnableAbsolutePositionEditor(aIsEnabled);
+ return NS_OK;
+}
+
+Result<int32_t, nsresult> HTMLEditor::AddZIndexWithTransaction(
+ nsStyledElement& aStyledElement, int32_t aChange) {
+ if (!aChange) {
+ return 0; // XXX Why don't we return current z-index value in this case?
+ }
+
+ int32_t zIndex = GetZIndex(aStyledElement);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ zIndex = std::max(zIndex + aChange, 0);
+ nsresult rv = SetZIndexWithTransaction(aStyledElement, zIndex);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::SetZIndexWithTransaction() destroyed the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::SetZIndexWithTransaction() failed, but ignored");
+ return zIndex;
+}
+
+nsresult HTMLEditor::SetZIndexWithTransaction(nsStyledElement& aStyledElement,
+ int32_t aZIndex) {
+ nsAutoString zIndexValue;
+ zIndexValue.AppendInt(aZIndex);
+
+ nsresult rv = CSSEditUtils::SetCSSPropertyWithTransaction(
+ *this, aStyledElement, *nsGkAtoms::z_index, zIndexValue);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyWithTransaction(nsGkAtoms::z_index) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyWithTransaction(nsGkAtoms::"
+ "z_index) failed, but ignored");
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AddZIndexAsAction(int32_t aChange,
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eIncreaseOrDecreaseZIndex, aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ Result<EditActionResult, nsresult> result = AddZIndexAsSubAction(aChange);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::AddZIndexAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ return NS_OK;
+}
+
+int32_t HTMLEditor::GetZIndex(Element& aElement) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return 0;
+ }
+
+ nsAutoString zIndexValue;
+
+ nsresult rv = CSSEditUtils::GetSpecifiedProperty(
+ aElement, *nsGkAtoms::z_index, zIndexValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::GetSpecifiedProperty(nsGkAtoms::z_index) failed");
+ return 0;
+ }
+ if (zIndexValue.EqualsLiteral("auto")) {
+ if (!aElement.GetParentElement()) {
+ NS_WARNING("aElement was an orphan node or the root node");
+ return 0;
+ }
+ // we have to look at the positioned ancestors
+ // cf. CSS 2 spec section 9.9.1
+ nsAutoString positionValue;
+ for (RefPtr<Element> element = aElement.GetParentElement(); element;
+ element = element->GetParentElement()) {
+ if (element->IsHTMLElement(nsGkAtoms::body)) {
+ return 0;
+ }
+ nsCOMPtr<nsINode> parentNode = element->GetParentElement();
+ nsresult rv = CSSEditUtils::GetComputedProperty(
+ *element, *nsGkAtoms::position, positionValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::position) failed");
+ return 0;
+ }
+ if (NS_WARN_IF(Destroyed()) ||
+ NS_WARN_IF(parentNode != element->GetParentNode())) {
+ return 0;
+ }
+ if (!positionValue.EqualsLiteral("absolute")) {
+ continue;
+ }
+ // ah, we found one, what's its z-index ? If its z-index is auto,
+ // we have to continue climbing the document's tree
+ rv = CSSEditUtils::GetComputedProperty(*element, *nsGkAtoms::z_index,
+ zIndexValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::z_index) failed");
+ return 0;
+ }
+ if (NS_WARN_IF(Destroyed()) ||
+ NS_WARN_IF(parentNode != element->GetParentNode())) {
+ return 0;
+ }
+ if (!zIndexValue.EqualsLiteral("auto")) {
+ break;
+ }
+ }
+ }
+
+ if (zIndexValue.EqualsLiteral("auto")) {
+ return 0;
+ }
+
+ nsresult rvIgnored;
+ int32_t result = zIndexValue.ToInteger(&rvIgnored);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsAString::ToInteger() failed, but ignored");
+ return result;
+}
+
+bool HTMLEditor::CreateGrabberInternal(nsIContent& aParentContent) {
+ if (NS_WARN_IF(mGrabber)) {
+ return false;
+ }
+
+ mGrabber = CreateAnonymousElement(nsGkAtoms::span, aParentContent,
+ u"mozGrabber"_ns, false);
+
+ // mGrabber may be destroyed during creation due to there may be
+ // mutation event listener.
+ if (!mGrabber) {
+ NS_WARNING(
+ "HTMLEditor::CreateAnonymousElement(nsGkAtoms::span, mozGrabber) "
+ "failed");
+ return false;
+ }
+
+ EventListenerManager* eventListenerManager =
+ mGrabber->GetOrCreateListenerManager();
+ eventListenerManager->AddEventListenerByType(
+ mEventListener, u"mousedown"_ns, TrustedEventsAtSystemGroupBubble());
+ MOZ_ASSERT(mGrabber);
+ return true;
+}
+
+nsresult HTMLEditor::RefreshGrabberInternal() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!mAbsolutelyPositionedObject) {
+ return NS_OK;
+ }
+
+ OwningNonNull<Element> absolutelyPositionedObject =
+ *mAbsolutelyPositionedObject;
+ nsresult rv = GetPositionAndDimensions(
+ absolutelyPositionedObject, mPositionedObjectX, mPositionedObjectY,
+ mPositionedObjectWidth, mPositionedObjectHeight,
+ mPositionedObjectBorderLeft, mPositionedObjectBorderTop,
+ mPositionedObjectMarginLeft, mPositionedObjectMarginTop);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetPositionAndDimensions() failed");
+ return rv;
+ }
+ if (NS_WARN_IF(absolutelyPositionedObject != mAbsolutelyPositionedObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<nsStyledElement> grabberStyledElement =
+ nsStyledElement::FromNodeOrNull(mGrabber.get());
+ if (!grabberStyledElement) {
+ return NS_OK;
+ }
+ rv = SetAnonymousElementPositionWithoutTransaction(
+ *grabberStyledElement, mPositionedObjectX + 12, mPositionedObjectY - 14);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::SetAnonymousElementPositionWithoutTransaction() failed");
+ return rv;
+ }
+ if (NS_WARN_IF(grabberStyledElement != mGrabber.get())) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+void HTMLEditor::HideGrabberInternal() {
+ if (NS_WARN_IF(!mAbsolutelyPositionedObject)) {
+ return;
+ }
+
+ // Move all members to the local variables first since mutation event
+ // listener may try to show grabber while we're hiding them.
+ RefPtr<Element> absolutePositioningObject =
+ std::move(mAbsolutelyPositionedObject);
+ ManualNACPtr grabber = std::move(mGrabber);
+ ManualNACPtr positioningShadow = std::move(mPositioningShadow);
+
+ // If we're still in dragging mode, it means that the dragging is canceled
+ // by the web app.
+ if (mGrabberClicked || mIsMoving) {
+ mGrabberClicked = false;
+ mIsMoving = false;
+ if (mEventListener) {
+ DebugOnly<nsresult> rvIgnored =
+ static_cast<HTMLEditorEventListener*>(mEventListener.get())
+ ->ListenToMouseMoveEventForGrabber(false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditorEventListener::"
+ "ListenToMouseMoveEventForGrabber(false) failed");
+ }
+ }
+
+ DebugOnly<nsresult> rv = absolutePositioningObject->UnsetAttr(
+ kNameSpaceID_None, nsGkAtoms::_moz_abspos, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Element::UnsetAttr(nsGkAtoms::_moz_abspos) failed, but ignored");
+
+ // We allow the pres shell to be null; when it is, we presume there
+ // are no document observers to notify, but we still want to
+ // UnbindFromTree.
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (grabber) {
+ DeleteRefToAnonymousNode(std::move(grabber), presShell);
+ }
+ if (positioningShadow) {
+ DeleteRefToAnonymousNode(std::move(positioningShadow), presShell);
+ }
+}
+
+nsresult HTMLEditor::ShowGrabberInternal(Element& aElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost) ||
+ NS_WARN_IF(!aElement.IsInclusiveDescendantOf(editingHost))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (NS_WARN_IF(mGrabber)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsAutoString classValue;
+ nsresult rv =
+ GetTemporaryStyleForFocusedPositionedElement(aElement, classValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::GetTemporaryStyleForFocusedPositionedElement() failed");
+ return rv;
+ }
+
+ rv = aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::_moz_abspos, classValue,
+ true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Element::SetAttr(nsGkAtoms::_moz_abspos) failed");
+ return rv;
+ }
+
+ mAbsolutelyPositionedObject = &aElement;
+
+ Element* parentElement = aElement.GetParentElement();
+ if (NS_WARN_IF(!parentElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!CreateGrabberInternal(*parentElement)) {
+ NS_WARNING("HTMLEditor::CreateGrabberInternal() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // If we succeeded to create the grabber, HideGrabberInternal() hasn't been
+ // called yet. So, mAbsolutelyPositionedObject should be non-nullptr.
+ MOZ_ASSERT(mAbsolutelyPositionedObject);
+
+ // Finally, move the grabber to proper position.
+ rv = RefreshGrabberInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RefereshGrabberInternal() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::StartMoving() {
+ MOZ_ASSERT(mGrabber);
+
+ RefPtr<Element> parentElement = mGrabber->GetParentElement();
+ if (NS_WARN_IF(!parentElement) || NS_WARN_IF(!mAbsolutelyPositionedObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // now, let's create the resizing shadow
+ mPositioningShadow =
+ CreateShadow(*parentElement, *mAbsolutelyPositionedObject);
+ if (!mPositioningShadow) {
+ NS_WARNING("HTMLEditor::CreateShadow() failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (!mAbsolutelyPositionedObject) {
+ NS_WARNING("The target has gone during HTMLEditor::CreateShadow()");
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<Element> positioningShadow = mPositioningShadow.get();
+ RefPtr<Element> absolutelyPositionedObject = mAbsolutelyPositionedObject;
+ nsresult rv =
+ SetShadowPosition(*positioningShadow, *absolutelyPositionedObject,
+ mPositionedObjectX, mPositionedObjectY);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetShadowPosition() failed");
+ return rv;
+ }
+
+ // make the shadow appear
+ DebugOnly<nsresult> rvIgnored =
+ mPositioningShadow->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::UnsetAttr(nsGkAtoms::_class) failed, but ignored");
+
+ // position it
+ if (RefPtr<nsStyledElement> positioningShadowStyledElement =
+ nsStyledElement::FromNode(mPositioningShadow.get())) {
+ nsresult rv;
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *positioningShadowStyledElement, *nsGkAtoms::width,
+ mPositionedObjectWidth);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction("
+ "nsGkAtoms::width) destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction("
+ "nsGkAtoms::width) failed, but ignored");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *positioningShadowStyledElement, *nsGkAtoms::height,
+ mPositionedObjectHeight);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction("
+ "nsGkAtoms::height) destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction("
+ "nsGkAtoms::height) failed, but ignored");
+ }
+
+ mIsMoving = true;
+ return NS_OK; // XXX Looks like nobody refers this result
+}
+
+void HTMLEditor::SnapToGrid(int32_t& newX, int32_t& newY) const {
+ if (mSnapToGridEnabled && mGridSize) {
+ newX = (int32_t)floor(((float)newX / (float)mGridSize) + 0.5f) * mGridSize;
+ newY = (int32_t)floor(((float)newY / (float)mGridSize) + 0.5f) * mGridSize;
+ }
+}
+
+nsresult HTMLEditor::GrabberClicked() {
+ if (NS_WARN_IF(!mEventListener)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv = static_cast<HTMLEditorEventListener*>(mEventListener.get())
+ ->ListenToMouseMoveEventForGrabber(true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditorEventListener::ListenToMouseMoveEventForGrabber(true) "
+ "failed, but ignored");
+ return NS_OK;
+ }
+ mGrabberClicked = true;
+ return NS_OK;
+}
+
+nsresult HTMLEditor::EndMoving() {
+ if (mPositioningShadow) {
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ DeleteRefToAnonymousNode(std::move(mPositioningShadow), presShell);
+
+ mPositioningShadow = nullptr;
+ }
+
+ if (mEventListener) {
+ DebugOnly<nsresult> rvIgnored =
+ static_cast<HTMLEditorEventListener*>(mEventListener.get())
+ ->ListenToMouseMoveEventForGrabber(false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditorEventListener::"
+ "ListenToMouseMoveEventForGrabber(false) failed");
+ }
+
+ mGrabberClicked = false;
+ mIsMoving = false;
+ nsresult rv = RefreshEditingUI();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RefreshEditingUI() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SetFinalPosition(int32_t aX, int32_t aY) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsresult rv = EndMoving();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::EndMoving() failed");
+ return rv;
+ }
+
+ // we have now to set the new width and height of the resized object
+ // we don't set the x and y position because we don't control that in
+ // a normal HTML layout
+ int32_t newX = mPositionedObjectX + aX - mOriginalX -
+ (mPositionedObjectBorderLeft + mPositionedObjectMarginLeft);
+ int32_t newY = mPositionedObjectY + aY - mOriginalY -
+ (mPositionedObjectBorderTop + mPositionedObjectMarginTop);
+
+ SnapToGrid(newX, newY);
+
+ nsAutoString x, y;
+ x.AppendInt(newX);
+ y.AppendInt(newY);
+
+ // we want one transaction only from a user's point of view
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ if (NS_WARN_IF(!mAbsolutelyPositionedObject)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (RefPtr<nsStyledElement> styledAbsolutelyPositionedElement =
+ nsStyledElement::FromNode(mAbsolutelyPositionedObject)) {
+ nsresult rv;
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(
+ *this, *styledAbsolutelyPositionedElement, *nsGkAtoms::top, newY);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "failed, but ignored");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(
+ *this, *styledAbsolutelyPositionedElement, *nsGkAtoms::left, newX);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left) "
+ "failed, but ignored");
+ }
+ // keep track of that size
+ mPositionedObjectX = newX;
+ mPositionedObjectY = newY;
+
+ rv = RefreshResizersInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RefreshResizersInternal() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SetPositionToAbsoluteOrStatic(Element& aElement,
+ bool aEnabled) {
+ nsAutoString positionValue;
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetComputedProperty(
+ aElement, *nsGkAtoms::position, positionValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::position) "
+ "failed, but ignored");
+ // nothing to do if the element is already in the state we want
+ if (positionValue.EqualsLiteral("absolute") == aEnabled) {
+ return NS_OK;
+ }
+
+ if (aEnabled) {
+ nsresult rv = SetPositionToAbsolute(aElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetPositionToAbsolute() failed");
+ return rv;
+ }
+
+ nsresult rv = SetPositionToStatic(aElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetPositionToStatic() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SetPositionToAbsolute(Element& aElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ int32_t x, y;
+ DebugOnly<nsresult> rvIgnored = GetElementOrigin(aElement, x, y);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::GetElementOrigin() failed, but ignored");
+
+ nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement);
+ if (styledElement) {
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ nsresult rv = CSSEditUtils::SetCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::position,
+ u"absolute"_ns);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSProperyWithTransaction(nsGkAtoms::Position) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::SetCSSPropertyWithTransaction(nsGkAtoms::position, "
+ "absolute) failed, but ignored");
+ }
+
+ SnapToGrid(x, y);
+ if (styledElement) {
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ nsresult rv =
+ SetTopAndLeftWithTransaction(MOZ_KnownLive(*styledElement), x, y);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetTopAndLeftWithTransaction() failed");
+ return rv;
+ }
+ }
+
+ // we may need to create a br if the positioned element is alone in its
+ // container
+ nsINode* parentNode = aElement.GetParentNode();
+ if (parentNode->GetChildCount() != 1) {
+ return NS_OK;
+ }
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, EditorDOMPoint(parentNode, 0u));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ // XXX Is this intentional selection change?
+ nsresult rv = insertBRElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CreateElementResult::SuggestCaretPointTo() failed");
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ return rv;
+}
+
+nsresult HTMLEditor::SetPositionToStatic(Element& aElement) {
+ nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement);
+ if (NS_WARN_IF(!styledElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ nsresult rv;
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::position, u""_ns);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::position) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::position) "
+ "failed, but ignored");
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::top, u""_ns);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::top) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::top) "
+ "failed, but ignored");
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::left, u""_ns);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::left) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::left) "
+ "failed, but ignored");
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::z_index, u""_ns);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::z_index) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::z_index) "
+ "failed, but ignored");
+
+ if (!HTMLEditUtils::IsImage(styledElement)) {
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::width, u""_ns);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::width) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::width) "
+ "failed, but ignored");
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::height, u""_ns);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::height) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::height) "
+ "failed, but ignored");
+ }
+
+ if (!styledElement->IsHTMLElement(nsGkAtoms::div) ||
+ HTMLEditor::HasStyleOrIdOrClassAttribute(*styledElement)) {
+ return NS_OK;
+ }
+
+ EditorDOMPoint pointToPutCaret;
+ // Make sure the first fild and last child of aElement starts/ends hard
+ // line(s) even after removing `aElement`.
+ {
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ Result<CreateElementResult, nsresult>
+ maybeInsertBRElementBeforeFirstChildResult =
+ EnsureHardLineBeginsWithFirstChildOf(MOZ_KnownLive(*styledElement));
+ if (MOZ_UNLIKELY(maybeInsertBRElementBeforeFirstChildResult.isErr())) {
+ NS_WARNING("HTMLEditor::EnsureHardLineBeginsWithFirstChildOf() failed");
+ return maybeInsertBRElementBeforeFirstChildResult.unwrapErr();
+ }
+ CreateElementResult unwrappedResult =
+ maybeInsertBRElementBeforeFirstChildResult.unwrap();
+ if (unwrappedResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedResult.UnwrapCaretPoint();
+ }
+ }
+ {
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ Result<CreateElementResult, nsresult>
+ maybeInsertBRElementAfterLastChildResult =
+ EnsureHardLineEndsWithLastChildOf(MOZ_KnownLive(*styledElement));
+ if (MOZ_UNLIKELY(maybeInsertBRElementAfterLastChildResult.isErr())) {
+ NS_WARNING("HTMLEditor::EnsureHardLineEndsWithLastChildOf() failed");
+ return maybeInsertBRElementAfterLastChildResult.unwrapErr();
+ }
+ CreateElementResult unwrappedResult =
+ maybeInsertBRElementAfterLastChildResult.unwrap();
+ if (unwrappedResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedResult.UnwrapCaretPoint();
+ }
+ }
+ {
+ // MOZ_KnownLive(*styledElement): aElement's lifetime must be guarantted
+ // by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ Result<EditorDOMPoint, nsresult> unwrapStyledElementResult =
+ RemoveContainerWithTransaction(MOZ_KnownLive(*styledElement));
+ if (MOZ_UNLIKELY(unwrapStyledElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapStyledElementResult.unwrapErr();
+ }
+ if (unwrapStyledElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapStyledElementResult.unwrap();
+ }
+ }
+ if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
+ return NS_OK;
+ }
+ rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::SetSnapToGridEnabled(bool aEnabled) {
+ mSnapToGridEnabled = aEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetSnapToGridEnabled(bool* aIsEnabled) {
+ *aIsEnabled = mSnapToGridEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::SetGridSize(uint32_t aSize) {
+ mGridSize = aSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetGridSize(uint32_t* aSize) {
+ *aSize = mGridSize;
+ return NS_OK;
+}
+
+nsresult HTMLEditor::SetTopAndLeftWithTransaction(
+ nsStyledElement& aStyledElement, int32_t aX, int32_t aY) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv;
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(*this, aStyledElement,
+ *nsGkAtoms::left, aX);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left) "
+ "failed, but ignored");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(*this, aStyledElement,
+ *nsGkAtoms::top, aY);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "failed, but ignored");
+ return NS_OK;
+}
+
+nsresult HTMLEditor::GetTemporaryStyleForFocusedPositionedElement(
+ Element& aElement, nsAString& aReturn) {
+ // we are going to outline the positioned element and bring it to the
+ // front to overlap any other element intersecting with it. But
+ // first, let's see what's the background and foreground colors of the
+ // positioned element.
+ // if background-image computed value is 'none,
+ // If the background color is 'auto' and R G B values of the foreground are
+ // each above #d0, use a black background
+ // If the background color is 'auto' and at least one of R G B values of
+ // the foreground is below #d0, use a white background
+ // Otherwise don't change background/foreground
+ aReturn.Truncate();
+
+ nsAutoString backgroundImageValue;
+ nsresult rv = CSSEditUtils::GetComputedProperty(
+ aElement, *nsGkAtoms::background_image, backgroundImageValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::background_image) "
+ "failed");
+ return rv;
+ }
+ if (!backgroundImageValue.EqualsLiteral("none")) {
+ return NS_OK;
+ }
+
+ nsAutoString backgroundColorValue;
+ rv = CSSEditUtils::GetComputedProperty(aElement, *nsGkAtoms::backgroundColor,
+ backgroundColorValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::backgroundColor) "
+ "failed");
+ return rv;
+ }
+ if (!backgroundColorValue.EqualsLiteral("rgba(0, 0, 0, 0)")) {
+ return NS_OK;
+ }
+
+ RefPtr<const ComputedStyle> style =
+ nsComputedDOMStyle::GetComputedStyle(&aElement);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (!style) {
+ NS_WARNING("nsComputedDOMStyle::GetComputedStyle() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ static const uint8_t kBlackBgTrigger = 0xd0;
+
+ auto color = style->StyleText()->mColor.ToColor();
+ if (NS_GET_R(color) >= kBlackBgTrigger &&
+ NS_GET_G(color) >= kBlackBgTrigger &&
+ NS_GET_B(color) >= kBlackBgTrigger) {
+ aReturn.AssignLiteral("black");
+ } else {
+ aReturn.AssignLiteral("white");
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLAnonymousNodeEditor.cpp b/editor/libeditor/HTMLAnonymousNodeEditor.cpp
new file mode 100644
index 0000000000..f89b4e334a
--- /dev/null
+++ b/editor/libeditor/HTMLAnonymousNodeEditor.cpp
@@ -0,0 +1,606 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditor.h"
+
+#include "CSSEditUtils.h"
+#include "HTMLEditUtils.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/PresShellInlines.h"
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/EventTarget.h"
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsComputedDOMStyle.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "nsAtom.h"
+#include "nsIContent.h"
+#include "nsID.h"
+#include "mozilla/dom/Document.h"
+#include "nsIDocumentObserver.h"
+#include "nsStubMutationObserver.h"
+#include "nsINode.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsUtils.h"
+#include "nsLiteralString.h"
+#include "nsPresContext.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStyledElement.h"
+#include "nsUnicharUtils.h"
+#include "nscore.h"
+#include "nsContentUtils.h" // for nsAutoScriptBlocker
+#include "nsROCSSPrimitiveValue.h"
+
+class nsIDOMEventListener;
+
+namespace mozilla {
+
+using namespace dom;
+
+// Retrieve the rounded number of CSS pixels from a computed CSS property.
+//
+// Note that this should only be called for properties whose resolved value
+// is CSS pixels (like width, height, left, top, right, bottom, margin, padding,
+// border-*-width, ...).
+//
+// See: https://drafts.csswg.org/cssom/#resolved-values
+static int32_t GetCSSFloatValue(nsComputedDOMStyle* aComputedStyle,
+ const nsACString& aProperty) {
+ MOZ_ASSERT(aComputedStyle);
+
+ // get the computed CSSValue of the property
+ nsAutoCString value;
+ aComputedStyle->GetPropertyValue(aProperty, value);
+ // We only care about resolved values, not a big deal if the element is
+ // undisplayed, for example, and the value is "auto" or what not.
+ nsresult rv = NS_OK;
+ int32_t val = value.ToInteger(&rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsAString::ToInteger() failed");
+ return NS_SUCCEEDED(rv) ? val : 0;
+}
+
+/******************************************************************************
+ * mozilla::ElementDeletionObserver
+ *****************************************************************************/
+
+class ElementDeletionObserver final : public nsStubMultiMutationObserver {
+ public:
+ ElementDeletionObserver(nsIContent* aNativeAnonNode,
+ Element* aObservedElement)
+ : mNativeAnonNode(aNativeAnonNode), mObservedElement(aObservedElement) {
+ AddMutationObserverToNode(aNativeAnonNode);
+ AddMutationObserverToNode(aObservedElement);
+ }
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
+
+ protected:
+ ~ElementDeletionObserver() = default;
+ nsIContent* mNativeAnonNode;
+ Element* mObservedElement;
+};
+
+NS_IMPL_ISUPPORTS(ElementDeletionObserver, nsIMutationObserver)
+
+void ElementDeletionObserver::ParentChainChanged(nsIContent* aContent) {
+ // If the native anonymous content has been unbound already in
+ // DeleteRefToAnonymousNode, mNativeAnonNode's parentNode is null.
+ if (aContent != mObservedElement || !mNativeAnonNode ||
+ mNativeAnonNode->GetParent() != aContent) {
+ return;
+ }
+
+ ManualNACPtr::RemoveContentFromNACArray(mNativeAnonNode);
+
+ mObservedElement->RemoveMutationObserver(this);
+ mObservedElement = nullptr;
+ mNativeAnonNode->RemoveMutationObserver(this);
+ mNativeAnonNode = nullptr;
+ NS_RELEASE_THIS();
+}
+
+void ElementDeletionObserver::NodeWillBeDestroyed(nsINode* aNode) {
+ NS_ASSERTION(aNode == mNativeAnonNode || aNode == mObservedElement,
+ "Wrong aNode!");
+ if (aNode == mNativeAnonNode) {
+ mObservedElement->RemoveMutationObserver(this);
+ mObservedElement = nullptr;
+ } else {
+ mNativeAnonNode->RemoveMutationObserver(this);
+ mNativeAnonNode->UnbindFromTree();
+ mNativeAnonNode = nullptr;
+ }
+
+ NS_RELEASE_THIS();
+}
+
+/******************************************************************************
+ * mozilla::HTMLEditor
+ *****************************************************************************/
+
+ManualNACPtr HTMLEditor::CreateAnonymousElement(nsAtom* aTag,
+ nsIContent& aParentContent,
+ const nsAString& aAnonClass,
+ bool aIsCreatedHidden) {
+ // Don't put anonymous editor element into non-HTML element.
+ // It is mainly for avoiding other anonymous element being inserted
+ // into <svg:use>, but in general we probably don't want to insert
+ // some random HTML anonymous element into a non-HTML element.
+ if (!aParentContent.IsHTMLElement()) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!GetDocument())) {
+ return nullptr;
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return nullptr;
+ }
+
+ // Create a new node through the element factory
+ RefPtr<Element> newElement = CreateHTMLContent(aTag);
+ if (!newElement) {
+ NS_WARNING("EditorBase::CreateHTMLContent() failed");
+ return nullptr;
+ }
+
+ // add the "hidden" class if needed
+ if (aIsCreatedHidden) {
+ nsresult rv = newElement->SetAttr(kNameSpaceID_None, nsGkAtoms::_class,
+ u"hidden"_ns, true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Element::SetAttr(nsGkAtoms::_class, hidden) failed");
+ return nullptr;
+ }
+ }
+
+ // add an _moz_anonclass attribute if needed
+ if (!aAnonClass.IsEmpty()) {
+ nsresult rv = newElement->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::_moz_anonclass, aAnonClass, true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Element::SetAttr(nsGkAtoms::_moz_anonclass) failed");
+ return nullptr;
+ }
+ }
+
+ {
+ nsAutoScriptBlocker scriptBlocker;
+
+ // establish parenthood of the element
+ newElement->SetIsNativeAnonymousRoot();
+ BindContext context(*aParentContent.AsElement(),
+ BindContext::ForNativeAnonymous);
+ nsresult rv = newElement->BindToTree(context, aParentContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Element::BindToTree(BindContext::ForNativeAnonymous) failed");
+ newElement->UnbindFromTree();
+ return nullptr;
+ }
+ }
+
+ ManualNACPtr newNativeAnonymousContent(newElement.forget());
+
+ // Must style the new element, otherwise the PostRecreateFramesFor call
+ // below will do nothing.
+ ServoStyleSet* styleSet = presShell->StyleSet();
+ // Sometimes editor likes to append anonymous content to elements
+ // in display:none subtrees, so avoid styling in those cases.
+ if (ServoStyleSet::MayTraverseFrom(newNativeAnonymousContent)) {
+ styleSet->StyleNewSubtree(newNativeAnonymousContent);
+ }
+
+ auto* observer = new ElementDeletionObserver(newNativeAnonymousContent,
+ aParentContent.AsElement());
+ NS_ADDREF(observer); // NodeWillBeDestroyed releases.
+
+#ifdef DEBUG
+ // Editor anonymous content gets passed to PostRecreateFramesFor... which
+ // can't _really_ deal with anonymous content (because it can't get the frame
+ // tree ordering right). But for us the ordering doesn't matter so this is
+ // sort of ok.
+ newNativeAnonymousContent->SetProperty(nsGkAtoms::restylableAnonymousNode,
+ reinterpret_cast<void*>(true));
+#endif // DEBUG
+
+ // display the element
+ presShell->PostRecreateFramesFor(newNativeAnonymousContent);
+
+ return newNativeAnonymousContent;
+}
+
+// Removes event listener and calls DeleteRefToAnonymousNode.
+void HTMLEditor::RemoveListenerAndDeleteRef(const nsAString& aEvent,
+ nsIDOMEventListener* aListener,
+ bool aUseCapture,
+ ManualNACPtr aElement,
+ PresShell* aPresShell) {
+ if (aElement) {
+ aElement->RemoveEventListener(aEvent, aListener, aUseCapture);
+ }
+ DeleteRefToAnonymousNode(std::move(aElement), aPresShell);
+}
+
+// Deletes all references to an anonymous element
+void HTMLEditor::DeleteRefToAnonymousNode(ManualNACPtr aContent,
+ PresShell* aPresShell) {
+ // call ContentRemoved() for the anonymous content
+ // node so its references get removed from the frame manager's
+ // undisplay map, and its layout frames get destroyed!
+
+ if (NS_WARN_IF(!aContent)) {
+ return;
+ }
+
+ if (NS_WARN_IF(!aContent->GetParent())) {
+ // aContent was already removed?
+ return;
+ }
+
+ nsAutoScriptBlocker scriptBlocker;
+ // Need to check whether aPresShell has been destroyed (but not yet deleted).
+ // See bug 338129.
+ if (aContent->IsInComposedDoc() && aPresShell &&
+ !aPresShell->IsDestroying()) {
+ MOZ_ASSERT(aContent->IsRootOfNativeAnonymousSubtree());
+ MOZ_ASSERT(!aContent->GetPreviousSibling(), "NAC has no siblings");
+
+ // FIXME(emilio): This is the only caller to PresShell::ContentRemoved that
+ // passes NAC into it. This is not great!
+ aPresShell->ContentRemoved(aContent, nullptr);
+ }
+
+ // The ManualNACPtr destructor will invoke UnbindFromTree.
+}
+
+void HTMLEditor::HideAnonymousEditingUIs() {
+ if (mAbsolutelyPositionedObject) {
+ HideGrabberInternal();
+ NS_ASSERTION(!mAbsolutelyPositionedObject,
+ "HTMLEditor::HideGrabberInternal() failed, but ignored");
+ }
+ if (mInlineEditedCell) {
+ HideInlineTableEditingUIInternal();
+ NS_ASSERTION(
+ !mInlineEditedCell,
+ "HTMLEditor::HideInlineTableEditingUIInternal() failed, but ignored");
+ }
+ if (mResizedObject) {
+ DebugOnly<nsresult> rvIgnored = HideResizersInternal();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::HideResizersInternal() failed, but ignored");
+ NS_ASSERTION(!mResizedObject,
+ "HTMLEditor::HideResizersInternal() failed, but ignored");
+ }
+}
+
+void HTMLEditor::HideAnonymousEditingUIsIfUnnecessary() {
+ // XXX Perhaps, this is wrong approach to hide multiple UIs because
+ // hiding one UI may causes overwriting existing UI with newly
+ // created one. In such case, we will leak ovewritten UI.
+ if (!IsAbsolutePositionEditorEnabled() && mAbsolutelyPositionedObject) {
+ // XXX If we're moving something, we need to cancel or commit the
+ // operation now.
+ HideGrabberInternal();
+ NS_ASSERTION(!mAbsolutelyPositionedObject,
+ "HTMLEditor::HideGrabberInternal() failed, but ignored");
+ }
+ if (!IsInlineTableEditorEnabled() && mInlineEditedCell) {
+ // XXX If we're resizing a table element, we need to cancel or commit the
+ // operation now.
+ HideInlineTableEditingUIInternal();
+ NS_ASSERTION(
+ !mInlineEditedCell,
+ "HTMLEditor::HideInlineTableEditingUIInternal() failed, but ignored");
+ }
+ if (!IsObjectResizerEnabled() && mResizedObject) {
+ // XXX If we're resizing something, we need to cancel or commit the
+ // operation now.
+ DebugOnly<nsresult> rvIgnored = HideResizersInternal();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::HideResizersInternal() failed, but ignored");
+ NS_ASSERTION(!mResizedObject,
+ "HTMLEditor::HideResizersInternal() failed, but ignored");
+ }
+}
+
+NS_IMETHODIMP HTMLEditor::CheckSelectionStateForAnonymousButtons() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = RefreshEditingUI();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RefereshEditingUI() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::RefreshEditingUI() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // First, we need to remove unnecessary editing UI now since some of them
+ // may be disabled while them are visible.
+ HideAnonymousEditingUIsIfUnnecessary();
+
+ // early way out if all contextual UI extensions are disabled
+ if (!IsObjectResizerEnabled() && !IsAbsolutePositionEditorEnabled() &&
+ !IsInlineTableEditorEnabled()) {
+ return NS_OK;
+ }
+
+ // Don't change selection state if we're moving.
+ if (mIsMoving) {
+ return NS_OK;
+ }
+
+ // let's get the containing element of the selection
+ RefPtr<Element> selectionContainerElement = GetSelectionContainerElement();
+ if (NS_WARN_IF(!selectionContainerElement)) {
+ return NS_OK;
+ }
+
+ // If we're not in a document, don't try to add resizers
+ if (!selectionContainerElement->IsInComposedDoc()) {
+ return NS_OK;
+ }
+
+ // what's its tag?
+ RefPtr<Element> focusElement = std::move(selectionContainerElement);
+ nsAtom* focusTagAtom = focusElement->NodeInfo()->NameAtom();
+
+ RefPtr<Element> absPosElement;
+ if (IsAbsolutePositionEditorEnabled()) {
+ // Absolute Positioning support is enabled, is the selection contained
+ // in an absolutely positioned element ?
+ absPosElement = GetAbsolutelyPositionedSelectionContainer();
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ }
+
+ RefPtr<Element> cellElement;
+ if (IsObjectResizerEnabled() || IsInlineTableEditorEnabled()) {
+ // Resizing or Inline Table Editing is enabled, we need to check if the
+ // selection is contained in a table cell
+ cellElement = GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td);
+ }
+
+ if (IsObjectResizerEnabled() && cellElement) {
+ // we are here because Resizing is enabled AND selection is contained in
+ // a cell
+
+ // get the enclosing table
+ if (nsGkAtoms::img != focusTagAtom) {
+ // the element container of the selection is not an image, so we'll show
+ // the resizers around the table
+ // XXX There may be a bug. cellElement may be not in <table> in invalid
+ // tree. So, perhaps, GetClosestAncestorTableElement() returns
+ // nullptr, we should not set focusTagAtom to nsGkAtoms::table.
+ focusElement =
+ HTMLEditUtils::GetClosestAncestorTableElement(*cellElement);
+ focusTagAtom = nsGkAtoms::table;
+ }
+ }
+
+ // we allow resizers only around images, tables, and absolutely positioned
+ // elements. If we don't have image/table, let's look at the latter case.
+ if (nsGkAtoms::img != focusTagAtom && nsGkAtoms::table != focusTagAtom) {
+ focusElement = absPosElement;
+ }
+
+ // at this point, focusElement contains the element for Resizing,
+ // cellElement contains the element for InlineTableEditing
+ // absPosElement contains the element for Positioning
+
+ // Note: All the Hide/Show methods below may change attributes on real
+ // content which means a DOMAttrModified handler may cause arbitrary
+ // side effects while this code runs (bug 420439).
+
+ if (IsAbsolutePositionEditorEnabled() && mAbsolutelyPositionedObject &&
+ absPosElement != mAbsolutelyPositionedObject) {
+ HideGrabberInternal();
+ NS_ASSERTION(!mAbsolutelyPositionedObject,
+ "HTMLEditor::HideGrabberInternal() failed, but ignored");
+ }
+
+ if (IsObjectResizerEnabled() && mResizedObject &&
+ mResizedObject != focusElement) {
+ // Perhaps, even if HideResizersInternal() failed, we should try to hide
+ // inline table editing UI. However, it returns error only when we cannot
+ // do anything. So, it's okay for now.
+ nsresult rv = HideResizersInternal();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::HideResizersInternal() failed");
+ return rv;
+ }
+ NS_ASSERTION(!mResizedObject,
+ "HTMLEditor::HideResizersInternal() failed, but ignored");
+ }
+
+ if (IsInlineTableEditorEnabled() && mInlineEditedCell &&
+ mInlineEditedCell != cellElement) {
+ HideInlineTableEditingUIInternal();
+ NS_ASSERTION(
+ !mInlineEditedCell,
+ "HTMLEditor::HideInlineTableEditingUIInternal failed, but ignored");
+ }
+
+ // now, let's display all contextual UI for good
+ nsIContent* hostContent = ComputeEditingHost();
+
+ if (IsObjectResizerEnabled() && focusElement &&
+ HTMLEditUtils::IsSimplyEditableNode(*focusElement) &&
+ focusElement != hostContent) {
+ if (nsGkAtoms::img == focusTagAtom) {
+ mResizedObjectIsAnImage = true;
+ }
+ if (mResizedObject) {
+ nsresult rv = RefreshResizersInternal();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::RefreshResizersInternal() failed");
+ return rv;
+ }
+ } else {
+ nsresult rv = ShowResizersInternal(*focusElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::ShowResizersInternal() failed");
+ return rv;
+ }
+ }
+ }
+
+ if (IsAbsolutePositionEditorEnabled() && absPosElement &&
+ HTMLEditUtils::IsSimplyEditableNode(*absPosElement) &&
+ absPosElement != hostContent) {
+ if (mAbsolutelyPositionedObject) {
+ nsresult rv = RefreshGrabberInternal();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::RefreshGrabberInternal() failed");
+ return rv;
+ }
+ } else {
+ nsresult rv = ShowGrabberInternal(*absPosElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::ShowGrabberInternal() failed");
+ return rv;
+ }
+ }
+ }
+
+ // XXX Shouldn't we check whether the `<table>` element is editable or not?
+ if (IsInlineTableEditorEnabled() && cellElement &&
+ HTMLEditUtils::IsSimplyEditableNode(*cellElement) &&
+ cellElement != hostContent) {
+ if (mInlineEditedCell) {
+ nsresult rv = RefreshInlineTableEditingUIInternal();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::RefreshInlineTableEditingUIInternal() failed");
+ return rv;
+ }
+ } else {
+ nsresult rv = ShowInlineTableEditingUIInternal(*cellElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::ShowInlineTableEditingUIInternal() failed");
+ return rv;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// Resizing and Absolute Positioning need to know everything about the
+// containing box of the element: position, size, margins, borders
+nsresult HTMLEditor::GetPositionAndDimensions(Element& aElement, int32_t& aX,
+ int32_t& aY, int32_t& aW,
+ int32_t& aH, int32_t& aBorderLeft,
+ int32_t& aBorderTop,
+ int32_t& aMarginLeft,
+ int32_t& aMarginTop) {
+ // Is the element positioned ? let's check the cheap way first...
+ bool isPositioned = aElement.HasAttr(nsGkAtoms::_moz_abspos);
+ if (!isPositioned) {
+ // hmmm... the expensive way now...
+ nsAutoString positionValue;
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetComputedProperty(
+ aElement, *nsGkAtoms::position, positionValue);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::"
+ "position) failed, but ignored");
+ isPositioned = positionValue.EqualsLiteral("absolute");
+ }
+
+ if (isPositioned) {
+ // Yes, it is absolutely positioned
+ mResizedObjectIsAbsolutelyPositioned = true;
+
+ // Get the all the computed css styles attached to the element node
+ RefPtr<nsComputedDOMStyle> computedDOMStyle =
+ CSSEditUtils::GetComputedStyle(&aElement);
+ if (NS_WARN_IF(!computedDOMStyle)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aBorderLeft = GetCSSFloatValue(computedDOMStyle, "border-left-width"_ns);
+ aBorderTop = GetCSSFloatValue(computedDOMStyle, "border-top-width"_ns);
+ aMarginLeft = GetCSSFloatValue(computedDOMStyle, "margin-left"_ns);
+ aMarginTop = GetCSSFloatValue(computedDOMStyle, "margin-top"_ns);
+
+ aX = GetCSSFloatValue(computedDOMStyle, "left"_ns) + aMarginLeft +
+ aBorderLeft;
+ aY = GetCSSFloatValue(computedDOMStyle, "top"_ns) + aMarginTop + aBorderTop;
+ aW = GetCSSFloatValue(computedDOMStyle, "width"_ns);
+ aH = GetCSSFloatValue(computedDOMStyle, "height"_ns);
+ } else {
+ mResizedObjectIsAbsolutelyPositioned = false;
+ RefPtr<nsGenericHTMLElement> htmlElement =
+ nsGenericHTMLElement::FromNode(aElement);
+ if (!htmlElement) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ DebugOnly<nsresult> rvIgnored = GetElementOrigin(aElement, aX, aY);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::GetElementOrigin() failed, but ignored");
+
+ aW = htmlElement->OffsetWidth();
+ aH = htmlElement->OffsetHeight();
+
+ aBorderLeft = 0;
+ aBorderTop = 0;
+ aMarginLeft = 0;
+ aMarginTop = 0;
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::SetAnonymousElementPositionWithoutTransaction(
+ nsStyledElement& aStyledElement, int32_t aX, int32_t aY) {
+ nsresult rv;
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ aStyledElement, *nsGkAtoms::left, aX);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::left) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::left) "
+ "failed, but ignored");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ aStyledElement, *nsGkAtoms::top, aY);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::top) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(nsGkAtoms::top) "
+ "failed, but ignored");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditHelpers.cpp b/editor/libeditor/HTMLEditHelpers.cpp
new file mode 100644
index 0000000000..cb1f93795c
--- /dev/null
+++ b/editor/libeditor/HTMLEditHelpers.cpp
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditHelpers.h"
+
+#include "CSSEditUtils.h"
+#include "EditorDOMPoint.h"
+#include "HTMLEditor.h"
+#include "PendingStyles.h"
+#include "WSRunObject.h"
+
+#include "mozilla/ContentIterator.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/Text.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsRange.h"
+
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+
+template void DOMIterator::AppendAllNodesToArray(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfNodes) const;
+template void DOMIterator::AppendAllNodesToArray(
+ nsTArray<OwningNonNull<HTMLBRElement>>& aArrayOfNodes) const;
+template void DOMIterator::AppendNodesToArray(
+ BoolFunctor aFunctor, nsTArray<OwningNonNull<nsIContent>>& aArrayOfNodes,
+ void* aClosure) const;
+template void DOMIterator::AppendNodesToArray(
+ BoolFunctor aFunctor, nsTArray<OwningNonNull<Element>>& aArrayOfNodes,
+ void* aClosure) const;
+template void DOMIterator::AppendNodesToArray(
+ BoolFunctor aFunctor, nsTArray<OwningNonNull<Text>>& aArrayOfNodes,
+ void* aClosure) const;
+
+/******************************************************************************
+ * some helper classes for iterating the dom tree
+ *****************************************************************************/
+
+DOMIterator::DOMIterator(nsINode& aNode) : mIter(&mPostOrderIter) {
+ DebugOnly<nsresult> rv = mIter->Init(&aNode);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+nsresult DOMIterator::Init(nsRange& aRange) { return mIter->Init(&aRange); }
+
+nsresult DOMIterator::Init(const RawRangeBoundary& aStartRef,
+ const RawRangeBoundary& aEndRef) {
+ return mIter->Init(aStartRef, aEndRef);
+}
+
+DOMIterator::DOMIterator() : mIter(&mPostOrderIter) {}
+
+template <class NodeClass>
+void DOMIterator::AppendAllNodesToArray(
+ nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes) const {
+ for (; !mIter->IsDone(); mIter->Next()) {
+ if (NodeClass* node = NodeClass::FromNode(mIter->GetCurrentNode())) {
+ aArrayOfNodes.AppendElement(*node);
+ }
+ }
+}
+
+template <class NodeClass>
+void DOMIterator::AppendNodesToArray(
+ BoolFunctor aFunctor, nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes,
+ void* aClosure /* = nullptr */) const {
+ for (; !mIter->IsDone(); mIter->Next()) {
+ NodeClass* node = NodeClass::FromNode(mIter->GetCurrentNode());
+ if (node && aFunctor(*node, aClosure)) {
+ aArrayOfNodes.AppendElement(*node);
+ }
+ }
+}
+
+DOMSubtreeIterator::DOMSubtreeIterator() { mIter = &mSubtreeIter; }
+
+nsresult DOMSubtreeIterator::Init(nsRange& aRange) {
+ return mIter->Init(&aRange);
+}
+
+/******************************************************************************
+ * mozilla::EditorElementStyle
+ *****************************************************************************/
+
+bool EditorElementStyle::IsCSSSettable(const nsStaticAtom& aTagName) const {
+ return CSSEditUtils::IsCSSEditableStyle(aTagName, *this);
+}
+
+bool EditorElementStyle::IsCSSSettable(const Element& aElement) const {
+ return CSSEditUtils::IsCSSEditableStyle(aElement, *this);
+}
+
+bool EditorElementStyle::IsCSSRemovable(const nsStaticAtom& aTagName) const {
+ // <font size> cannot be applied with CSS font-size for now, but it should be
+ // removable.
+ return EditorElementStyle::IsCSSSettable(aTagName) ||
+ (IsInlineStyle() && AsInlineStyle().IsStyleOfFontSize());
+}
+
+bool EditorElementStyle::IsCSSRemovable(const Element& aElement) const {
+ // <font size> cannot be applied with CSS font-size for now, but it should be
+ // removable.
+ return EditorElementStyle::IsCSSSettable(aElement) ||
+ (IsInlineStyle() && AsInlineStyle().IsStyleOfFontSize());
+}
+
+/******************************************************************************
+ * mozilla::EditorInlineStyle
+ *****************************************************************************/
+
+PendingStyleCache EditorInlineStyle::ToPendingStyleCache(
+ nsAString&& aValue) const {
+ return PendingStyleCache(*mHTMLProperty,
+ mAttribute ? mAttribute->AsStatic() : nullptr,
+ std::move(aValue));
+}
+
+bool EditorInlineStyle::IsRepresentedBy(const nsIContent& aContent) const {
+ MOZ_ASSERT(!IsStyleToClearAllInlineStyles());
+
+ if (!aContent.IsHTMLElement()) {
+ return false;
+ }
+ const Element& element = *aContent.AsElement();
+ if (mHTMLProperty == element.NodeInfo()->NameAtom() ||
+ mHTMLProperty == GetSimilarElementNameAtom()) {
+ // <a> cannot be nested. Therefore, if we're the style of <a>, we should
+ // treat existing it even if the attribute does not match.
+ if (mHTMLProperty == nsGkAtoms::a) {
+ return true;
+ }
+ return !mAttribute || element.HasAttr(mAttribute);
+ }
+ // Special case for linking or naming an <a> element.
+ if ((mHTMLProperty == nsGkAtoms::href && HTMLEditUtils::IsLink(&element)) ||
+ (mHTMLProperty == nsGkAtoms::name &&
+ HTMLEditUtils::IsNamedAnchor(&element))) {
+ return true;
+ }
+ // If the style is font size, it's also represented by <big> or <small>.
+ if (mHTMLProperty == nsGkAtoms::font && mAttribute == nsGkAtoms::size &&
+ aContent.IsAnyOfHTMLElements(nsGkAtoms::big, nsGkAtoms::small)) {
+ return true;
+ }
+ return false;
+}
+
+Result<bool, nsresult> EditorInlineStyle::IsSpecifiedBy(
+ const HTMLEditor& aHTMLEditor, Element& aElement) const {
+ MOZ_ASSERT(!IsStyleToClearAllInlineStyles());
+ if (!IsCSSSettable(aElement) && !IsCSSRemovable(aElement)) {
+ return false;
+ }
+ // Special case in the CSS mode. We should treat <u>, <s>, <strike>, <ins>
+ // and <del> specifies text-decoration (bug 1802668).
+ if (aHTMLEditor.IsCSSEnabled() &&
+ IsStyleOfTextDecoration(IgnoreSElement::No) &&
+ aElement.IsAnyOfHTMLElements(nsGkAtoms::u, nsGkAtoms::s,
+ nsGkAtoms::strike, nsGkAtoms::ins,
+ nsGkAtoms::del)) {
+ return true;
+ }
+ return CSSEditUtils::HaveSpecifiedCSSEquivalentStyles(aHTMLEditor, aElement,
+ *this);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditHelpers.h b/editor/libeditor/HTMLEditHelpers.h
new file mode 100644
index 0000000000..fc04c9d3f6
--- /dev/null
+++ b/editor/libeditor/HTMLEditHelpers.h
@@ -0,0 +1,1209 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLEditHelpers_h
+#define HTMLEditHelpers_h
+
+/**
+ * This header declares/defines trivial helper classes which are used by
+ * HTMLEditor. If you want to create or look for static utility methods,
+ * see HTMLEditUtils.h.
+ */
+
+#include "EditorDOMPoint.h"
+#include "EditorForwards.h"
+#include "EditorUtils.h" // for CaretPoint
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RangeBoundary.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/StaticRange.h"
+
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsRange.h"
+#include "nsString.h"
+
+class nsISimpleEnumerator;
+
+namespace mozilla {
+
+enum class BlockInlineCheck : uint8_t {
+ // BlockInlineCheck is not expected by the root caller.
+ Unused,
+ // Refer only the HTML default style at considering whether block or inline.
+ // All non-HTML elements are treated as inline.
+ UseHTMLDefaultStyle,
+ // Refer the element's computed style of display-outside at considering
+ // whether block or inline.
+ // FYI: If editor.block_inline_check.use_computed_style pref is set to false,
+ // this is same as HTMLDefaultStyle.
+ UseComputedDisplayOutsideStyle,
+ // Refer the element's computed style of display at considering whether block
+ // or inline. I.e., this is a good value to look for any block boundary.
+ // E.g., this is proper value when:
+ // * Checking visibility of collapsible white-spaces or <br>
+ // * Looking for whether a padding <br> is required
+ // * Looking for a caret position
+ // FYI: If editor.block_inline_check.use_computed_style pref is set to false,
+ // this is same as HTMLDefaultStyle.
+ UseComputedDisplayStyle,
+};
+
+/**
+ * Even if the caller wants block boundary caused by display-inline: flow-root
+ * like inline-block, because it's required only when scanning from in it.
+ * I.e., if scanning needs to go to siblings, we don't want to treat
+ * inline-block siblings as inline.
+ */
+[[nodiscard]] inline BlockInlineCheck IgnoreInsideBlockBoundary(
+ BlockInlineCheck aBlockInlineCheck) {
+ return aBlockInlineCheck == BlockInlineCheck::UseComputedDisplayStyle
+ ? BlockInlineCheck::UseComputedDisplayOutsideStyle
+ : aBlockInlineCheck;
+}
+
+enum class WithTransaction { No, Yes };
+inline std::ostream& operator<<(std::ostream& aStream,
+ WithTransaction aWithTransaction) {
+ aStream << "WithTransaction::"
+ << (aWithTransaction == WithTransaction::Yes ? "Yes" : "No");
+ return aStream;
+}
+
+/*****************************************************************************
+ * MoveNodeResult is a simple class for MoveSomething() methods.
+ * This stores whether it's handled or not, and next insertion point and a
+ * suggestion for new caret position.
+ *****************************************************************************/
+class MOZ_STACK_CLASS MoveNodeResult final : public CaretPoint {
+ public:
+ constexpr bool Handled() const { return mHandled; }
+ constexpr bool Ignored() const { return !Handled(); }
+ constexpr const EditorDOMPoint& NextInsertionPointRef() const {
+ return mNextInsertionPoint;
+ }
+ constexpr EditorDOMPoint&& UnwrapNextInsertionPoint() {
+ return std::move(mNextInsertionPoint);
+ }
+ template <typename EditorDOMPointType>
+ EditorDOMPointType NextInsertionPoint() const {
+ return mNextInsertionPoint.To<EditorDOMPointType>();
+ }
+
+ /**
+ * Override the result as "handled" forcibly.
+ */
+ void MarkAsHandled() { mHandled = true; }
+
+ MoveNodeResult(const MoveNodeResult& aOther) = delete;
+ MoveNodeResult& operator=(const MoveNodeResult& aOther) = delete;
+ MoveNodeResult(MoveNodeResult&& aOther) = default;
+ MoveNodeResult& operator=(MoveNodeResult&& aOther) = default;
+
+ MoveNodeResult& operator|=(const MoveNodeResult& aOther) {
+ MOZ_ASSERT(this != &aOther);
+ // aOther is merged with this instance so that its caret suggestion
+ // shouldn't be handled anymore.
+ aOther.IgnoreCaretPointSuggestion();
+ // Should be handled again even if it's already handled
+ UnmarkAsHandledCaretPoint();
+
+ mHandled |= aOther.mHandled;
+
+ // Take the new one for the next insertion point.
+ mNextInsertionPoint = aOther.mNextInsertionPoint;
+
+ // Take the new caret point if and only if it's suggested.
+ if (aOther.HasCaretPointSuggestion()) {
+ SetCaretPoint(aOther.CaretPointRef());
+ }
+ return *this;
+ }
+
+#ifdef DEBUG
+ ~MoveNodeResult() {
+ MOZ_ASSERT_IF(Handled(), !HasCaretPointSuggestion() || CaretPointHandled());
+ }
+#endif
+
+ /*****************************************************************************
+ * When a move node handler (or its helper) does nothing,
+ * the result of these factory methods should be returned.
+ * aNextInsertionPoint Must be set and valid.
+ *****************************************************************************/
+ static MoveNodeResult IgnoredResult(
+ const EditorDOMPoint& aNextInsertionPoint) {
+ return MoveNodeResult(aNextInsertionPoint, false);
+ }
+ static MoveNodeResult IgnoredResult(EditorDOMPoint&& aNextInsertionPoint) {
+ return MoveNodeResult(std::move(aNextInsertionPoint), false);
+ }
+
+ /*****************************************************************************
+ * When a move node handler (or its helper) handled and not canceled,
+ * the result of these factory methods should be returned.
+ * aNextInsertionPoint Must be set and valid.
+ *****************************************************************************/
+ static MoveNodeResult HandledResult(
+ const EditorDOMPoint& aNextInsertionPoint) {
+ return MoveNodeResult(aNextInsertionPoint, true);
+ }
+
+ static MoveNodeResult HandledResult(EditorDOMPoint&& aNextInsertionPoint) {
+ return MoveNodeResult(std::move(aNextInsertionPoint), true);
+ }
+
+ static MoveNodeResult HandledResult(const EditorDOMPoint& aNextInsertionPoint,
+ const EditorDOMPoint& aPointToPutCaret) {
+ return MoveNodeResult(aNextInsertionPoint, aPointToPutCaret);
+ }
+
+ static MoveNodeResult HandledResult(EditorDOMPoint&& aNextInsertionPoint,
+ const EditorDOMPoint& aPointToPutCaret) {
+ return MoveNodeResult(std::move(aNextInsertionPoint), aPointToPutCaret);
+ }
+
+ static MoveNodeResult HandledResult(const EditorDOMPoint& aNextInsertionPoint,
+ EditorDOMPoint&& aPointToPutCaret) {
+ return MoveNodeResult(aNextInsertionPoint, std::move(aPointToPutCaret));
+ }
+
+ static MoveNodeResult HandledResult(EditorDOMPoint&& aNextInsertionPoint,
+ EditorDOMPoint&& aPointToPutCaret) {
+ return MoveNodeResult(std::move(aNextInsertionPoint),
+ std::move(aPointToPutCaret));
+ }
+
+ private:
+ explicit MoveNodeResult(const EditorDOMPoint& aNextInsertionPoint,
+ bool aHandled)
+ : mNextInsertionPoint(aNextInsertionPoint),
+ mHandled(aHandled && aNextInsertionPoint.IsSet()) {
+ AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
+ mNextInsertionPoint);
+ }
+ explicit MoveNodeResult(EditorDOMPoint&& aNextInsertionPoint, bool aHandled)
+ : mNextInsertionPoint(std::move(aNextInsertionPoint)),
+ mHandled(aHandled && mNextInsertionPoint.IsSet()) {
+ AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
+ mNextInsertionPoint);
+ }
+ explicit MoveNodeResult(const EditorDOMPoint& aNextInsertionPoint,
+ const EditorDOMPoint& aPointToPutCaret)
+ : CaretPoint(aPointToPutCaret),
+ mNextInsertionPoint(aNextInsertionPoint),
+ mHandled(mNextInsertionPoint.IsSet()) {
+ AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
+ mNextInsertionPoint);
+ }
+ explicit MoveNodeResult(EditorDOMPoint&& aNextInsertionPoint,
+ const EditorDOMPoint& aPointToPutCaret)
+ : CaretPoint(aPointToPutCaret),
+ mNextInsertionPoint(std::move(aNextInsertionPoint)),
+ mHandled(mNextInsertionPoint.IsSet()) {
+ AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
+ mNextInsertionPoint);
+ }
+ explicit MoveNodeResult(const EditorDOMPoint& aNextInsertionPoint,
+ EditorDOMPoint&& aPointToPutCaret)
+ : CaretPoint(std::move(aPointToPutCaret)),
+ mNextInsertionPoint(aNextInsertionPoint),
+ mHandled(mNextInsertionPoint.IsSet()) {
+ AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
+ mNextInsertionPoint);
+ }
+ explicit MoveNodeResult(EditorDOMPoint&& aNextInsertionPoint,
+ EditorDOMPoint&& aPointToPutCaret)
+ : CaretPoint(std::move(aPointToPutCaret)),
+ mNextInsertionPoint(std::move(aNextInsertionPoint)),
+ mHandled(mNextInsertionPoint.IsSet()) {
+ AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
+ mNextInsertionPoint);
+ }
+
+ EditorDOMPoint mNextInsertionPoint;
+ bool mHandled;
+};
+
+/*****************************************************************************
+ * SplitNodeResult is a simple class for
+ * HTMLEditor::SplitNodeDeepWithTransaction().
+ * This makes the callers' code easier to read.
+ *****************************************************************************/
+class MOZ_STACK_CLASS SplitNodeResult final : public CaretPoint {
+ public:
+ bool Handled() const { return mPreviousNode || mNextNode; }
+
+ /**
+ * DidSplit() returns true if a node was actually split.
+ */
+ bool DidSplit() const { return mPreviousNode && mNextNode; }
+
+ /**
+ * GetPreviousContent() returns previous content node at the split point.
+ */
+ MOZ_KNOWN_LIVE nsIContent* GetPreviousContent() const {
+ if (mGivenSplitPoint.IsSet()) {
+ return mGivenSplitPoint.IsEndOfContainer() ? mGivenSplitPoint.GetChild()
+ : nullptr;
+ }
+ return mPreviousNode;
+ }
+ template <typename NodeType>
+ MOZ_KNOWN_LIVE NodeType* GetPreviousContentAs() const {
+ return NodeType::FromNodeOrNull(GetPreviousContent());
+ }
+ template <typename EditorDOMPointType>
+ EditorDOMPointType AtPreviousContent() const {
+ if (nsIContent* previousContent = GetPreviousContent()) {
+ return EditorDOMPointType(previousContent);
+ }
+ return EditorDOMPointType();
+ }
+
+ /**
+ * GetNextContent() returns next content node at the split point.
+ */
+ MOZ_KNOWN_LIVE nsIContent* GetNextContent() const {
+ if (mGivenSplitPoint.IsSet()) {
+ return !mGivenSplitPoint.IsEndOfContainer() ? mGivenSplitPoint.GetChild()
+ : nullptr;
+ }
+ return mNextNode;
+ }
+ template <typename NodeType>
+ MOZ_KNOWN_LIVE NodeType* GetNextContentAs() const {
+ return NodeType::FromNodeOrNull(GetNextContent());
+ }
+ template <typename EditorDOMPointType>
+ EditorDOMPointType AtNextContent() const {
+ if (nsIContent* nextContent = GetNextContent()) {
+ return EditorDOMPointType(nextContent);
+ }
+ return EditorDOMPointType();
+ }
+
+ /**
+ * Returns new content node which is created at splitting a node. I.e., this
+ * returns nullptr if no node was split.
+ */
+ MOZ_KNOWN_LIVE nsIContent* GetNewContent() const {
+ if (!DidSplit()) {
+ return nullptr;
+ }
+ return mNextNode;
+ }
+ template <typename NodeType>
+ MOZ_KNOWN_LIVE NodeType* GetNewContentAs() const {
+ return NodeType::FromNodeOrNull(GetNewContent());
+ }
+ template <typename EditorDOMPointType>
+ EditorDOMPointType AtNewContent() const {
+ if (nsIContent* newContent = GetNewContent()) {
+ return EditorDOMPointType(newContent);
+ }
+ return EditorDOMPointType();
+ }
+
+ /**
+ * Returns original content node which is (or is just tried to be) split.
+ */
+ MOZ_KNOWN_LIVE nsIContent* GetOriginalContent() const {
+ if (mGivenSplitPoint.IsSet()) {
+ // Different from previous/next content, if the creator didn't split a
+ // node, the container of the split point is the original node.
+ return mGivenSplitPoint.GetContainerAs<nsIContent>();
+ }
+ return mPreviousNode ? mPreviousNode : mNextNode;
+ }
+ template <typename NodeType>
+ MOZ_KNOWN_LIVE NodeType* GetOriginalContentAs() const {
+ return NodeType::FromNodeOrNull(GetOriginalContent());
+ }
+ template <typename EditorDOMPointType>
+ EditorDOMPointType AtOriginalContent() const {
+ if (nsIContent* originalContent = GetOriginalContent()) {
+ return EditorDOMPointType(originalContent);
+ }
+ return EditorDOMPointType();
+ }
+
+ /**
+ * AtSplitPoint() returns the split point in the container.
+ * HTMLEditor::CreateAndInsertElement() or something similar methods.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType AtSplitPoint() const {
+ if (mGivenSplitPoint.IsSet()) {
+ return mGivenSplitPoint.To<EditorDOMPointType>();
+ }
+ if (!mPreviousNode) {
+ return EditorDOMPointType(mNextNode);
+ }
+ return EditorDOMPointType::After(mPreviousNode);
+ }
+
+ SplitNodeResult(const SplitNodeResult&) = delete;
+ SplitNodeResult& operator=(const SplitNodeResult&) = delete;
+ SplitNodeResult(SplitNodeResult&&) = default;
+ SplitNodeResult& operator=(SplitNodeResult&&) = default;
+
+ /**
+ * This constructor should be used for setting specific caret point instead of
+ * aSplitResult's one.
+ */
+ SplitNodeResult(SplitNodeResult&& aSplitResult,
+ const EditorDOMPoint& aNewCaretPoint)
+ : SplitNodeResult(std::move(aSplitResult)) {
+ SetCaretPoint(aNewCaretPoint);
+ }
+ SplitNodeResult(SplitNodeResult&& aSplitResult,
+ EditorDOMPoint&& aNewCaretPoint)
+ : SplitNodeResult(std::move(aSplitResult)) {
+ SetCaretPoint(std::move(aNewCaretPoint));
+ }
+
+ /**
+ * This constructor shouldn't be used by anybody except methods which
+ * use this as result when it succeeds.
+ *
+ * @param aNewNode The node which is newly created.
+ * @param aSplitNode The node which was split.
+ * @param aNewCaretPoint
+ * An optional new caret position. If this is omitted,
+ * the point between new node and split node will be
+ * suggested.
+ */
+ SplitNodeResult(nsIContent& aNewNode, nsIContent& aSplitNode,
+ const Maybe<EditorDOMPoint>& aNewCaretPoint = Nothing())
+ : CaretPoint(aNewCaretPoint.isSome()
+ ? aNewCaretPoint.ref()
+ : EditorDOMPoint::AtEndOf(aSplitNode)),
+ mPreviousNode(&aSplitNode),
+ mNextNode(&aNewNode) {}
+
+ SplitNodeResult ToHandledResult() const {
+ CaretPointHandled();
+ SplitNodeResult result;
+ result.mPreviousNode = GetPreviousContent();
+ result.mNextNode = GetNextContent();
+ MOZ_DIAGNOSTIC_ASSERT(result.Handled());
+ // Don't recompute the caret position because in this case, split has not
+ // occurred yet. In the case, the caller shouldn't need to update
+ // selection.
+ result.SetCaretPoint(CaretPointRef());
+ return result;
+ }
+
+ /**
+ * The following factory methods creates a SplitNodeResult instance for the
+ * special cases.
+ *
+ * @param aDeeperSplitNodeResult
+ * If the splitter has already split a child or a
+ * descendant of the latest split node, the split node
+ * result should be specified.
+ */
+ static inline SplitNodeResult HandledButDidNotSplitDueToEndOfContainer(
+ nsIContent& aNotSplitNode,
+ const SplitNodeResult* aDeeperSplitNodeResult = nullptr) {
+ SplitNodeResult result;
+ result.mPreviousNode = &aNotSplitNode;
+ // Caret should be put at the last split point instead of current node.
+ if (aDeeperSplitNodeResult) {
+ result.SetCaretPoint(aDeeperSplitNodeResult->CaretPointRef());
+ aDeeperSplitNodeResult->IgnoreCaretPointSuggestion();
+ }
+ return result;
+ }
+
+ static inline SplitNodeResult HandledButDidNotSplitDueToStartOfContainer(
+ nsIContent& aNotSplitNode,
+ const SplitNodeResult* aDeeperSplitNodeResult = nullptr) {
+ SplitNodeResult result;
+ result.mNextNode = &aNotSplitNode;
+ // Caret should be put at the last split point instead of current node.
+ if (aDeeperSplitNodeResult) {
+ result.SetCaretPoint(aDeeperSplitNodeResult->CaretPointRef());
+ aDeeperSplitNodeResult->IgnoreCaretPointSuggestion();
+ }
+ return result;
+ }
+
+ template <typename PT, typename CT>
+ static inline SplitNodeResult NotHandled(
+ const EditorDOMPointBase<PT, CT>& aGivenSplitPoint,
+ const SplitNodeResult* aDeeperSplitNodeResult = nullptr) {
+ SplitNodeResult result;
+ result.mGivenSplitPoint = aGivenSplitPoint;
+ // Caret should be put at the last split point instead of current node.
+ if (aDeeperSplitNodeResult) {
+ result.SetCaretPoint(aDeeperSplitNodeResult->CaretPointRef());
+ aDeeperSplitNodeResult->IgnoreCaretPointSuggestion();
+ }
+ return result;
+ }
+
+ /**
+ * Returns aSplitNodeResult as-is unless it didn't split a node but
+ * aDeeperSplitNodeResult has already split a child or a descendant and has a
+ * valid point to put caret around there. In the case, this return
+ * aSplitNodeResult which suggests a caret position around the last split
+ * point.
+ */
+ static inline SplitNodeResult MergeWithDeeperSplitNodeResult(
+ SplitNodeResult&& aSplitNodeResult,
+ const SplitNodeResult& aDeeperSplitNodeResult) {
+ aSplitNodeResult.UnmarkAsHandledCaretPoint();
+ aDeeperSplitNodeResult.IgnoreCaretPointSuggestion();
+ if (aSplitNodeResult.DidSplit() ||
+ !aDeeperSplitNodeResult.HasCaretPointSuggestion()) {
+ return std::move(aSplitNodeResult);
+ }
+ SplitNodeResult result(std::move(aSplitNodeResult));
+ result.SetCaretPoint(aDeeperSplitNodeResult.CaretPointRef());
+ return result;
+ }
+
+#ifdef DEBUG
+ ~SplitNodeResult() {
+ MOZ_ASSERT(!HasCaretPointSuggestion() || CaretPointHandled());
+ }
+#endif
+
+ private:
+ SplitNodeResult() = default;
+
+ // When methods which return this class split some nodes actually, they
+ // need to set a set of left node and right node to this class. However,
+ // one or both of them may be moved or removed by mutation observer.
+ // In such case, we cannot represent the point with EditorDOMPoint since
+ // it requires current container node. Therefore, we need to use
+ // nsCOMPtr<nsIContent> here instead.
+ nsCOMPtr<nsIContent> mPreviousNode;
+ nsCOMPtr<nsIContent> mNextNode;
+
+ // Methods which return this class may not split any nodes actually. Then,
+ // they may want to return given split point as is since such behavior makes
+ // their callers simpler. In this case, the point may be in a text node
+ // which cannot be represented as a node. Therefore, we need EditorDOMPoint
+ // for representing the point.
+ EditorDOMPoint mGivenSplitPoint;
+};
+
+/*****************************************************************************
+ * JoinNodesResult is a simple class for HTMLEditor::JoinNodesWithTransaction().
+ * This makes the callers' code easier to read.
+ *****************************************************************************/
+class MOZ_STACK_CLASS JoinNodesResult final {
+ public:
+ MOZ_KNOWN_LIVE nsIContent* ExistingContent() const {
+ return mJoinedPoint.ContainerAs<nsIContent>();
+ }
+ template <typename EditorDOMPointType>
+ EditorDOMPointType AtExistingContent() const {
+ return EditorDOMPointType(mJoinedPoint.ContainerAs<nsIContent>());
+ }
+
+ MOZ_KNOWN_LIVE nsIContent* RemovedContent() const { return mRemovedContent; }
+ template <typename EditorDOMPointType>
+ EditorDOMPointType AtRemovedContent() const {
+ if (mRemovedContent) {
+ return EditorDOMPointType(mRemovedContent);
+ }
+ return EditorDOMPointType();
+ }
+
+ template <typename EditorDOMPointType>
+ EditorDOMPointType AtJoinedPoint() const {
+ return mJoinedPoint.To<EditorDOMPointType>();
+ }
+
+ JoinNodesResult() = delete;
+
+ /**
+ * This constructor shouldn't be used by anybody except methods which
+ * use this as result when it succeeds.
+ *
+ * @param aJoinedPoint First child of right node or first character.
+ * @param aRemovedContent The node which was removed from the parent.
+ */
+ JoinNodesResult(const EditorDOMPoint& aJoinedPoint,
+ nsIContent& aRemovedContent)
+ : mJoinedPoint(aJoinedPoint), mRemovedContent(&aRemovedContent) {
+ MOZ_DIAGNOSTIC_ASSERT(aJoinedPoint.IsInContentNode());
+ }
+
+ JoinNodesResult(const JoinNodesResult& aOther) = delete;
+ JoinNodesResult& operator=(const JoinNodesResult& aOther) = delete;
+ JoinNodesResult(JoinNodesResult&& aOther) = default;
+ JoinNodesResult& operator=(JoinNodesResult&& aOther) = default;
+
+ private:
+ EditorDOMPoint mJoinedPoint;
+ MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mRemovedContent;
+};
+
+/*****************************************************************************
+ * SplitRangeOffFromNodeResult class is a simple class for methods which split a
+ * node at 2 points for making part of the node split off from the node.
+ *****************************************************************************/
+class MOZ_STACK_CLASS SplitRangeOffFromNodeResult final : public CaretPoint {
+ public:
+ /**
+ * GetLeftContent() returns new created node before the part of quarried out.
+ * This may return nullptr if the method didn't split at start edge of
+ * the node.
+ */
+ MOZ_KNOWN_LIVE nsIContent* GetLeftContent() const { return mLeftContent; }
+ template <typename ContentNodeType>
+ MOZ_KNOWN_LIVE ContentNodeType* GetLeftContentAs() const {
+ return ContentNodeType::FromNodeOrNull(GetLeftContent());
+ }
+ constexpr nsCOMPtr<nsIContent>&& UnwrapLeftContent() {
+ mMovedContent = true;
+ return std::move(mLeftContent);
+ }
+
+ /**
+ * GetMiddleContent() returns new created node between left node and right
+ * node. I.e., this is quarried out from the node. This may return nullptr
+ * if the method unwrapped the middle node.
+ */
+ MOZ_KNOWN_LIVE nsIContent* GetMiddleContent() const { return mMiddleContent; }
+ template <typename ContentNodeType>
+ MOZ_KNOWN_LIVE ContentNodeType* GetMiddleContentAs() const {
+ return ContentNodeType::FromNodeOrNull(GetMiddleContent());
+ }
+ constexpr nsCOMPtr<nsIContent>&& UnwrapMiddleContent() {
+ mMovedContent = true;
+ return std::move(mMiddleContent);
+ }
+
+ /**
+ * GetRightContent() returns the right node after the part of quarried out.
+ * This may return nullptr it the method didn't split at end edge of the
+ * node.
+ */
+ MOZ_KNOWN_LIVE nsIContent* GetRightContent() const { return mRightContent; }
+ template <typename ContentNodeType>
+ MOZ_KNOWN_LIVE ContentNodeType* GetRightContentAs() const {
+ return ContentNodeType::FromNodeOrNull(GetRightContent());
+ }
+ constexpr nsCOMPtr<nsIContent>&& UnwrapRightContent() {
+ mMovedContent = true;
+ return std::move(mRightContent);
+ }
+
+ /**
+ * GetLeftmostContent() returns the leftmost content after trying to
+ * split twice. If the node was not split, this returns the original node.
+ */
+ MOZ_KNOWN_LIVE nsIContent* GetLeftmostContent() const {
+ MOZ_ASSERT(!mMovedContent);
+ return mLeftContent ? mLeftContent
+ : (mMiddleContent ? mMiddleContent : mRightContent);
+ }
+ template <typename ContentNodeType>
+ MOZ_KNOWN_LIVE ContentNodeType* GetLeftmostContentAs() const {
+ return ContentNodeType::FromNodeOrNull(GetLeftmostContent());
+ }
+
+ /**
+ * GetRightmostContent() returns the rightmost content after trying to
+ * split twice. If the node was not split, this returns the original node.
+ */
+ MOZ_KNOWN_LIVE nsIContent* GetRightmostContent() const {
+ MOZ_ASSERT(!mMovedContent);
+ return mRightContent ? mRightContent
+ : (mMiddleContent ? mMiddleContent : mLeftContent);
+ }
+ template <typename ContentNodeType>
+ MOZ_KNOWN_LIVE ContentNodeType* GetRightmostContentAs() const {
+ return ContentNodeType::FromNodeOrNull(GetRightmostContent());
+ }
+
+ [[nodiscard]] bool DidSplit() const { return mLeftContent || mRightContent; }
+
+ SplitRangeOffFromNodeResult() = delete;
+
+ SplitRangeOffFromNodeResult(nsIContent* aLeftContent,
+ nsIContent* aMiddleContent,
+ nsIContent* aRightContent)
+ : mLeftContent(aLeftContent),
+ mMiddleContent(aMiddleContent),
+ mRightContent(aRightContent) {}
+
+ SplitRangeOffFromNodeResult(nsIContent* aLeftContent,
+ nsIContent* aMiddleContent,
+ nsIContent* aRightContent,
+ EditorDOMPoint&& aPointToPutCaret)
+ : CaretPoint(std::move(aPointToPutCaret)),
+ mLeftContent(aLeftContent),
+ mMiddleContent(aMiddleContent),
+ mRightContent(aRightContent) {}
+
+ SplitRangeOffFromNodeResult(const SplitRangeOffFromNodeResult& aOther) =
+ delete;
+ SplitRangeOffFromNodeResult& operator=(
+ const SplitRangeOffFromNodeResult& aOther) = delete;
+ SplitRangeOffFromNodeResult(SplitRangeOffFromNodeResult&& aOther) noexcept
+ : CaretPoint(aOther.UnwrapCaretPoint()),
+ mLeftContent(std::move(aOther.mLeftContent)),
+ mMiddleContent(std::move(aOther.mMiddleContent)),
+ mRightContent(std::move(aOther.mRightContent)) {
+ MOZ_ASSERT(!aOther.mMovedContent);
+ }
+ SplitRangeOffFromNodeResult& operator=(SplitRangeOffFromNodeResult&& aOther) =
+ delete; // due to bug 1792638
+
+#ifdef DEBUG
+ ~SplitRangeOffFromNodeResult() {
+ MOZ_ASSERT(!HasCaretPointSuggestion() || CaretPointHandled());
+ }
+#endif
+
+ private:
+ MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mLeftContent;
+ MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mMiddleContent;
+ MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mRightContent;
+
+ bool mutable mMovedContent = false;
+};
+
+/*****************************************************************************
+ * SplitRangeOffResult class is a simple class for methods which splits
+ * specific ancestor elements at 2 DOM points.
+ *****************************************************************************/
+class MOZ_STACK_CLASS SplitRangeOffResult final : public CaretPoint {
+ public:
+ constexpr bool Handled() const { return mHandled; }
+
+ /**
+ * The start boundary is at the right of split at split point. The end
+ * boundary is at right node of split at end point, i.e., the end boundary
+ * points out of the range to have been split off.
+ */
+ constexpr const EditorDOMRange& RangeRef() const { return mRange; }
+
+ SplitRangeOffResult() = delete;
+
+ /**
+ * Constructor for success case.
+ *
+ * @param aTrackedRangeStart The range whose start is at topmost
+ * right node child at start point if
+ * actually split there, or at the point
+ * to be tried to split, and whose end is
+ * at topmost right node child at end point
+ * if actually split there, or at the point
+ * to be tried to split. Note that if the
+ * method allows to run script after
+ * splitting the range boundaries, they
+ * should be tracked with
+ * AutoTrackDOMRange.
+ * @param aSplitNodeResultAtStart Raw split node result at start point.
+ * @param aSplitNodeResultAtEnd Raw split node result at start point.
+ */
+ SplitRangeOffResult(EditorDOMRange&& aTrackedRange,
+ SplitNodeResult&& aSplitNodeResultAtStart,
+ SplitNodeResult&& aSplitNodeResultAtEnd)
+ : mRange(std::move(aTrackedRange)),
+ mHandled(aSplitNodeResultAtStart.Handled() ||
+ aSplitNodeResultAtEnd.Handled()) {
+ MOZ_ASSERT(mRange.StartRef().IsSet());
+ MOZ_ASSERT(mRange.EndRef().IsSet());
+ // The given results are created for creating this instance so that the
+ // caller may not need to handle with them. For making who taking the
+ // responsible clearer, we should move them into this constructor.
+ EditorDOMPoint pointToPutCaret;
+ SplitNodeResult splitNodeResultAtStart(std::move(aSplitNodeResultAtStart));
+ SplitNodeResult splitNodeResultAtEnd(std::move(aSplitNodeResultAtEnd));
+ splitNodeResultAtStart.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ splitNodeResultAtEnd.MoveCaretPointTo(pointToPutCaret,
+ {SuggestCaret::OnlyIfHasSuggestion});
+ SetCaretPoint(std::move(pointToPutCaret));
+ }
+
+ SplitRangeOffResult(const SplitRangeOffResult& aOther) = delete;
+ SplitRangeOffResult& operator=(const SplitRangeOffResult& aOther) = delete;
+ SplitRangeOffResult(SplitRangeOffResult&& aOther) = default;
+ SplitRangeOffResult& operator=(SplitRangeOffResult&& aOther) = default;
+
+ private:
+ EditorDOMRange mRange;
+
+ // If you need to store previous and/or next node at start/end point,
+ // you might be able to use `SplitNodeResult::GetPreviousNode()` etc in the
+ // constructor only when `SplitNodeResult::Handled()` returns true. But
+ // the node might have gone with another DOM tree mutation. So, be careful
+ // if you do it.
+
+ bool mHandled;
+};
+
+/******************************************************************************
+ * DOM tree iterators
+ *****************************************************************************/
+
+class MOZ_RAII DOMIterator {
+ public:
+ explicit DOMIterator();
+ explicit DOMIterator(nsINode& aNode);
+ virtual ~DOMIterator() = default;
+
+ nsresult Init(nsRange& aRange);
+ nsresult Init(const RawRangeBoundary& aStartRef,
+ const RawRangeBoundary& aEndRef);
+
+ template <class NodeClass>
+ void AppendAllNodesToArray(
+ nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes) const;
+
+ /**
+ * AppendNodesToArray() calls aFunctor before appending found node to
+ * aArrayOfNodes. If aFunctor returns false, the node will be ignored.
+ * You can use aClosure instead of capturing something with lambda.
+ * Note that aNode is guaranteed that it's an instance of NodeClass
+ * or its sub-class.
+ * XXX If we can make type of aNode templated without std::function,
+ * it'd be better, though.
+ */
+ using BoolFunctor = bool (*)(nsINode& aNode, void* aClosure);
+ template <class NodeClass>
+ void AppendNodesToArray(BoolFunctor aFunctor,
+ nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes,
+ void* aClosure = nullptr) const;
+
+ protected:
+ SafeContentIteratorBase* mIter;
+ PostContentIterator mPostOrderIter;
+};
+
+class MOZ_RAII DOMSubtreeIterator final : public DOMIterator {
+ public:
+ explicit DOMSubtreeIterator();
+ virtual ~DOMSubtreeIterator() = default;
+
+ nsresult Init(nsRange& aRange);
+
+ private:
+ ContentSubtreeIterator mSubtreeIter;
+ explicit DOMSubtreeIterator(nsINode& aNode) = delete;
+};
+
+/******************************************************************************
+ * ReplaceRangeData
+ *
+ * This represents range to be replaced and replacing string.
+ *****************************************************************************/
+
+template <typename EditorDOMPointType>
+class MOZ_STACK_CLASS ReplaceRangeDataBase final {
+ public:
+ ReplaceRangeDataBase() = default;
+ template <typename OtherEditorDOMRangeType>
+ ReplaceRangeDataBase(const OtherEditorDOMRangeType& aRange,
+ const nsAString& aReplaceString)
+ : mRange(aRange), mReplaceString(aReplaceString) {}
+ template <typename StartPointType, typename EndPointType>
+ ReplaceRangeDataBase(const StartPointType& aStart, const EndPointType& aEnd,
+ const nsAString& aReplaceString)
+ : mRange(aStart, aEnd), mReplaceString(aReplaceString) {}
+
+ bool IsSet() const { return mRange.IsPositioned(); }
+ bool IsSetAndValid() const { return mRange.IsPositionedAndValid(); }
+ bool Collapsed() const { return mRange.Collapsed(); }
+ bool HasReplaceString() const { return !mReplaceString.IsEmpty(); }
+ const EditorDOMPointType& StartRef() const { return mRange.StartRef(); }
+ const EditorDOMPointType& EndRef() const { return mRange.EndRef(); }
+ const EditorDOMRangeBase<EditorDOMPointType>& RangeRef() const {
+ return mRange;
+ }
+ const nsString& ReplaceStringRef() const { return mReplaceString; }
+
+ template <typename PointType>
+ MOZ_NEVER_INLINE_DEBUG void SetStart(const PointType& aStart) {
+ mRange.SetStart(aStart);
+ }
+ template <typename PointType>
+ MOZ_NEVER_INLINE_DEBUG void SetEnd(const PointType& aEnd) {
+ mRange.SetEnd(aEnd);
+ }
+ template <typename StartPointType, typename EndPointType>
+ MOZ_NEVER_INLINE_DEBUG void SetStartAndEnd(const StartPointType& aStart,
+ const EndPointType& aEnd) {
+ mRange.SetRange(aStart, aEnd);
+ }
+ template <typename OtherEditorDOMRangeType>
+ MOZ_NEVER_INLINE_DEBUG void SetRange(const OtherEditorDOMRangeType& aRange) {
+ mRange = aRange;
+ }
+ void SetReplaceString(const nsAString& aReplaceString) {
+ mReplaceString = aReplaceString;
+ }
+ template <typename StartPointType, typename EndPointType>
+ MOZ_NEVER_INLINE_DEBUG void SetStartAndEnd(const StartPointType& aStart,
+ const EndPointType& aEnd,
+ const nsAString& aReplaceString) {
+ SetStartAndEnd(aStart, aEnd);
+ SetReplaceString(aReplaceString);
+ }
+ template <typename OtherEditorDOMRangeType>
+ MOZ_NEVER_INLINE_DEBUG void Set(const OtherEditorDOMRangeType& aRange,
+ const nsAString& aReplaceString) {
+ SetRange(aRange);
+ SetReplaceString(aReplaceString);
+ }
+
+ private:
+ EditorDOMRangeBase<EditorDOMPointType> mRange;
+ // This string may be used with ReplaceTextTransaction. Therefore, for
+ // avoiding memory copy, we should store it with nsString rather than
+ // nsAutoString.
+ nsString mReplaceString;
+};
+
+/******************************************************************************
+ * EditorElementStyle represents a generic style of element
+ ******************************************************************************/
+
+class MOZ_STACK_CLASS EditorElementStyle {
+ public:
+#define DEFINE_FACTORY(aName, aAttr) \
+ constexpr static EditorElementStyle aName() { \
+ return EditorElementStyle(*(aAttr)); \
+ }
+
+ // text-align, caption-side, a pair of margin-left and margin-right
+ DEFINE_FACTORY(Align, nsGkAtoms::align)
+ // background-color
+ DEFINE_FACTORY(BGColor, nsGkAtoms::bgcolor)
+ // background-image
+ DEFINE_FACTORY(Background, nsGkAtoms::background)
+ // border
+ DEFINE_FACTORY(Border, nsGkAtoms::border)
+ // height
+ DEFINE_FACTORY(Height, nsGkAtoms::height)
+ // color
+ DEFINE_FACTORY(Text, nsGkAtoms::text)
+ // list-style-type
+ DEFINE_FACTORY(Type, nsGkAtoms::type)
+ // vertical-align
+ DEFINE_FACTORY(VAlign, nsGkAtoms::valign)
+ // width
+ DEFINE_FACTORY(Width, nsGkAtoms::width)
+
+ static EditorElementStyle Create(const nsAtom& aAttribute) {
+ MOZ_DIAGNOSTIC_ASSERT(IsHTMLStyle(&aAttribute));
+ return EditorElementStyle(*aAttribute.AsStatic());
+ }
+
+ [[nodiscard]] static bool IsHTMLStyle(const nsAtom* aAttribute) {
+ return aAttribute == nsGkAtoms::align || aAttribute == nsGkAtoms::bgcolor ||
+ aAttribute == nsGkAtoms::background ||
+ aAttribute == nsGkAtoms::border || aAttribute == nsGkAtoms::height ||
+ aAttribute == nsGkAtoms::text || aAttribute == nsGkAtoms::type ||
+ aAttribute == nsGkAtoms::valign || aAttribute == nsGkAtoms::width;
+ }
+
+ /**
+ * Returns true if the style can be represented by CSS and it's possible to
+ * apply the style with CSS.
+ */
+ [[nodiscard]] bool IsCSSSettable(const nsStaticAtom& aTagName) const;
+ [[nodiscard]] bool IsCSSSettable(const dom::Element& aElement) const;
+
+ /**
+ * Returns true if the style can be represented by CSS and it's possible to
+ * remove the style with CSS.
+ */
+ [[nodiscard]] bool IsCSSRemovable(const nsStaticAtom& aTagName) const;
+ [[nodiscard]] bool IsCSSRemovable(const dom::Element& aElement) const;
+
+ nsStaticAtom* Style() const { return mStyle; }
+
+ [[nodiscard]] bool IsInlineStyle() const { return !mStyle; }
+ inline EditorInlineStyle& AsInlineStyle();
+ inline const EditorInlineStyle& AsInlineStyle() const;
+
+ protected:
+ MOZ_KNOWN_LIVE nsStaticAtom* mStyle = nullptr;
+ EditorElementStyle() = default;
+
+ private:
+ constexpr explicit EditorElementStyle(const nsStaticAtom& aStyle)
+ // Needs const_cast hack here because the this class users may want
+ // non-const nsStaticAtom pointer due to bug 1794954
+ : mStyle(const_cast<nsStaticAtom*>(&aStyle)) {}
+};
+
+/******************************************************************************
+ * EditorInlineStyle represents an inline style.
+ ******************************************************************************/
+
+struct MOZ_STACK_CLASS EditorInlineStyle : public EditorElementStyle {
+ // nullptr if you want to remove all inline styles.
+ // Otherwise, one of the presentation tag names which we support in style
+ // editor, and there special cases: nsGkAtoms::href means <a href="...">,
+ // and nsGkAtoms::name means <a name="...">.
+ MOZ_KNOWN_LIVE nsStaticAtom* const mHTMLProperty = nullptr;
+ // For some mHTMLProperty values, need to be set to its attribute name.
+ // E.g., nsGkAtoms::size and nsGkAtoms::face for nsGkAtoms::font.
+ // Otherwise, nullptr.
+ // TODO: Once we stop using these structure to wrap selected content nodes
+ // with <a href> elements, we can make this nsStaticAtom*.
+ MOZ_KNOWN_LIVE const RefPtr<nsAtom> mAttribute;
+
+ /**
+ * Returns true if the style means that all inline styles should be removed.
+ */
+ [[nodiscard]] bool IsStyleToClearAllInlineStyles() const {
+ return !mHTMLProperty;
+ }
+
+ /**
+ * Returns true if the style is about <a>.
+ */
+ [[nodiscard]] bool IsStyleOfAnchorElement() const {
+ return mHTMLProperty == nsGkAtoms::a || mHTMLProperty == nsGkAtoms::href ||
+ mHTMLProperty == nsGkAtoms::name;
+ }
+
+ /**
+ * Returns true if the style is invertible with CSS.
+ */
+ [[nodiscard]] bool IsInvertibleWithCSS() const {
+ return mHTMLProperty == nsGkAtoms::b;
+ }
+
+ /**
+ * Returns true if the style can be specified with text-decoration.
+ */
+ enum class IgnoreSElement { No, Yes };
+ [[nodiscard]] bool IsStyleOfTextDecoration(
+ IgnoreSElement aIgnoreSElement) const {
+ return mHTMLProperty == nsGkAtoms::u ||
+ mHTMLProperty == nsGkAtoms::strike ||
+ (aIgnoreSElement == IgnoreSElement::No &&
+ mHTMLProperty == nsGkAtoms::s);
+ }
+
+ /**
+ * Returns true if the style can be represented with <font>.
+ */
+ [[nodiscard]] bool IsStyleOfFontElement() const {
+ MOZ_ASSERT_IF(
+ mHTMLProperty == nsGkAtoms::font,
+ mAttribute == nsGkAtoms::bgcolor || mAttribute == nsGkAtoms::color ||
+ mAttribute == nsGkAtoms::face || mAttribute == nsGkAtoms::size);
+ return mHTMLProperty == nsGkAtoms::font && mAttribute != nsGkAtoms::bgcolor;
+ }
+
+ /**
+ * Returns true if the style is font-size or <font size="...">.
+ */
+ [[nodiscard]] bool IsStyleOfFontSize() const {
+ return mHTMLProperty == nsGkAtoms::font && mAttribute == nsGkAtoms::size;
+ }
+
+ /**
+ * Returns true if the style is conflict with vertical-align even though
+ * they are not mapped to vertical-align in the CSS mode.
+ */
+ [[nodiscard]] bool IsStyleConflictingWithVerticalAlign() const {
+ return mHTMLProperty == nsGkAtoms::sup || mHTMLProperty == nsGkAtoms::sub;
+ }
+
+ /**
+ * If the style has a similar element which should be removed when applying
+ * the style, this retuns an element name. Otherwise, returns nullptr.
+ */
+ [[nodiscard]] nsStaticAtom* GetSimilarElementNameAtom() const {
+ if (mHTMLProperty == nsGkAtoms::b) {
+ return nsGkAtoms::strong;
+ }
+ if (mHTMLProperty == nsGkAtoms::i) {
+ return nsGkAtoms::em;
+ }
+ if (mHTMLProperty == nsGkAtoms::strike) {
+ return nsGkAtoms::s;
+ }
+ return nullptr;
+ }
+
+ /**
+ * Returns true if aContent is an HTML element and represents the style.
+ */
+ [[nodiscard]] bool IsRepresentedBy(const nsIContent& aContent) const;
+
+ /**
+ * Returns true if aElement has style attribute and specifies this style.
+ *
+ * TODO: Make aElement be constant, but it needs to touch CSSEditUtils a lot.
+ */
+ [[nodiscard]] Result<bool, nsresult> IsSpecifiedBy(
+ const HTMLEditor& aHTMLEditor, dom::Element& aElement) const;
+
+ explicit EditorInlineStyle(const nsStaticAtom& aHTMLProperty,
+ nsAtom* aAttribute = nullptr)
+ : EditorInlineStyle(aHTMLProperty, aAttribute, HasValue::No) {}
+ EditorInlineStyle(const nsStaticAtom& aHTMLProperty,
+ RefPtr<nsAtom>&& aAttribute)
+ : EditorInlineStyle(aHTMLProperty, aAttribute, HasValue::No) {}
+
+ /**
+ * Returns the instance which means remove all inline styles.
+ */
+ static EditorInlineStyle RemoveAllStyles() { return EditorInlineStyle(); }
+
+ PendingStyleCache ToPendingStyleCache(nsAString&& aValue) const;
+
+ bool operator==(const EditorInlineStyle& aOther) const {
+ return mHTMLProperty == aOther.mHTMLProperty &&
+ mAttribute == aOther.mAttribute;
+ }
+
+ bool MaybeHasValue() const { return mMaybeHasValue; }
+ inline EditorInlineStyleAndValue& AsInlineStyleAndValue();
+ inline const EditorInlineStyleAndValue& AsInlineStyleAndValue() const;
+
+ protected:
+ const bool mMaybeHasValue = false;
+
+ enum class HasValue { No, Yes };
+ EditorInlineStyle(const nsStaticAtom& aHTMLProperty, nsAtom* aAttribute,
+ HasValue aHasValue)
+ // Needs const_cast hack here because the struct users may want
+ // non-const nsStaticAtom pointer due to bug 1794954
+ : mHTMLProperty(const_cast<nsStaticAtom*>(&aHTMLProperty)),
+ mAttribute(aAttribute),
+ mMaybeHasValue(aHasValue == HasValue::Yes) {}
+ EditorInlineStyle(const nsStaticAtom& aHTMLProperty,
+ RefPtr<nsAtom>&& aAttribute, HasValue aHasValue)
+ // Needs const_cast hack here because the struct users may want
+ // non-const nsStaticAtom pointer due to bug 1794954
+ : mHTMLProperty(const_cast<nsStaticAtom*>(&aHTMLProperty)),
+ mAttribute(std::move(aAttribute)),
+ mMaybeHasValue(aHasValue == HasValue::Yes) {}
+ EditorInlineStyle(const EditorInlineStyle& aStyle, HasValue aHasValue)
+ : mHTMLProperty(aStyle.mHTMLProperty),
+ mAttribute(aStyle.mAttribute),
+ mMaybeHasValue(aHasValue == HasValue::Yes) {}
+
+ private:
+ EditorInlineStyle() = default;
+
+ using EditorElementStyle::AsInlineStyle;
+ using EditorElementStyle::IsInlineStyle;
+ using EditorElementStyle::Style;
+};
+
+inline EditorInlineStyle& EditorElementStyle::AsInlineStyle() {
+ return reinterpret_cast<EditorInlineStyle&>(*this);
+}
+
+inline const EditorInlineStyle& EditorElementStyle::AsInlineStyle() const {
+ return reinterpret_cast<const EditorInlineStyle&>(*this);
+}
+
+/******************************************************************************
+ * EditorInlineStyleAndValue represents an inline style and stores its value.
+ ******************************************************************************/
+
+struct MOZ_STACK_CLASS EditorInlineStyleAndValue : public EditorInlineStyle {
+ // Stores the value of mAttribute.
+ nsString const mAttributeValue;
+
+ bool IsStyleToClearAllInlineStyles() const = delete;
+ EditorInlineStyleAndValue() = delete;
+
+ explicit EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty)
+ : EditorInlineStyle(aHTMLProperty, nullptr, HasValue::No) {}
+ EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty, nsAtom& aAttribute,
+ const nsAString& aValue)
+ : EditorInlineStyle(aHTMLProperty, &aAttribute, HasValue::Yes),
+ mAttributeValue(aValue) {}
+ EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty,
+ RefPtr<nsAtom>&& aAttribute,
+ const nsAString& aValue)
+ : EditorInlineStyle(aHTMLProperty, std::move(aAttribute), HasValue::Yes),
+ mAttributeValue(aValue) {
+ MOZ_ASSERT(mAttribute);
+ }
+ EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty, nsAtom& aAttribute,
+ nsString&& aValue)
+ : EditorInlineStyle(aHTMLProperty, &aAttribute, HasValue::Yes),
+ mAttributeValue(std::move(aValue)) {}
+ EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty,
+ RefPtr<nsAtom>&& aAttribute, nsString&& aValue)
+ : EditorInlineStyle(aHTMLProperty, std::move(aAttribute), HasValue::Yes),
+ mAttributeValue(aValue) {}
+
+ [[nodiscard]] static EditorInlineStyleAndValue ToInvert(
+ const EditorInlineStyle& aStyle) {
+ MOZ_ASSERT(aStyle.IsInvertibleWithCSS());
+ return EditorInlineStyleAndValue(aStyle, u"-moz-editor-invert-value"_ns);
+ }
+
+ // mHTMLProperty is never nullptr since all constructors guarantee it.
+ // Therefore, hide it and expose its reference instead.
+ MOZ_KNOWN_LIVE nsStaticAtom& HTMLPropertyRef() const {
+ MOZ_DIAGNOSTIC_ASSERT(mHTMLProperty);
+ return *mHTMLProperty;
+ }
+
+ [[nodiscard]] bool IsStyleToInvert() const {
+ return mAttributeValue.EqualsLiteral(u"-moz-editor-invert-value");
+ }
+
+ /**
+ * Returns true if this style is representable with HTML.
+ */
+ [[nodiscard]] bool IsRepresentableWithHTML() const {
+ // Use background-color in any elements
+ if (mAttribute == nsGkAtoms::bgcolor) {
+ return false;
+ }
+ // Inverting the style means that it's invertible with CSS
+ if (IsStyleToInvert()) {
+ return false;
+ }
+ return true;
+ }
+
+ private:
+ using EditorInlineStyle::mHTMLProperty;
+
+ EditorInlineStyleAndValue(const EditorInlineStyle& aStyle,
+ const nsAString& aValue)
+ : EditorInlineStyle(aStyle, HasValue::Yes), mAttributeValue(aValue) {}
+
+ using EditorInlineStyle::AsInlineStyleAndValue;
+ using EditorInlineStyle::HasValue;
+};
+
+inline EditorInlineStyleAndValue& EditorInlineStyle::AsInlineStyleAndValue() {
+ return reinterpret_cast<EditorInlineStyleAndValue&>(*this);
+}
+
+inline const EditorInlineStyleAndValue&
+EditorInlineStyle::AsInlineStyleAndValue() const {
+ return reinterpret_cast<const EditorInlineStyleAndValue&>(*this);
+}
+
+} // namespace mozilla
+
+#endif // #ifndef HTMLEditHelpers_h
diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp
new file mode 100644
index 0000000000..28e0ad1595
--- /dev/null
+++ b/editor/libeditor/HTMLEditSubActionHandler.cpp
@@ -0,0 +1,12090 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditor.h"
+#include "HTMLEditorInlines.h"
+#include "HTMLEditorNestedClasses.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "AutoRangeArray.h"
+#include "CSSEditUtils.h"
+#include "EditAction.h"
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditUtils.h"
+#include "PendingStyles.h" // for SpecifiedStyle
+#include "WSRunObject.h"
+
+#include "ErrorList.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/InternalMutationEvent.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/RangeUtils.h"
+#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
+#include "mozilla/TextComposition.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/RangeBinding.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h"
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsAtom.h"
+#include "nsCRT.h"
+#include "nsCRTGlue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsFrameSelection.h"
+#include "nsGkAtoms.h"
+#include "nsHTMLDocument.h"
+#include "nsIContent.h"
+#include "nsID.h"
+#include "nsIFrame.h"
+#include "nsINode.h"
+#include "nsLiteralString.h"
+#include "nsPrintfCString.h"
+#include "nsRange.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStyledElement.h"
+#include "nsTArray.h"
+#include "nsTextNode.h"
+#include "nsThreadUtils.h"
+#include "nsUnicharUtils.h"
+
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions;
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
+using WalkTextOption = HTMLEditUtils::WalkTextOption;
+using WalkTreeDirection = HTMLEditUtils::WalkTreeDirection;
+using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
+
+/********************************************************
+ * first some helpful functors we will use
+ ********************************************************/
+
+static bool IsPendingStyleCachePreservingSubAction(
+ EditSubAction aEditSubAction) {
+ switch (aEditSubAction) {
+ case EditSubAction::eDeleteSelectedContent:
+ case EditSubAction::eInsertLineBreak:
+ case EditSubAction::eInsertParagraphSeparator:
+ case EditSubAction::eCreateOrChangeList:
+ case EditSubAction::eIndent:
+ case EditSubAction::eOutdent:
+ case EditSubAction::eSetOrClearAlignment:
+ case EditSubAction::eCreateOrRemoveBlock:
+ case EditSubAction::eFormatBlockForHTMLCommand:
+ case EditSubAction::eMergeBlockContents:
+ case EditSubAction::eRemoveList:
+ case EditSubAction::eCreateOrChangeDefinitionListItem:
+ case EditSubAction::eInsertElement:
+ case EditSubAction::eInsertQuotation:
+ case EditSubAction::eInsertQuotedText:
+ return true;
+ default:
+ return false;
+ }
+}
+
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMRange& aRange);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorRawDOMRange& aRange);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorRawDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint);
+
+nsresult HTMLEditor::InitEditorContentAndSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // We should do nothing with the result of GetRoot() if only a part of the
+ // document is editable.
+ if (!EntireDocumentIsEditable()) {
+ return NS_OK;
+ }
+
+ nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() failed");
+ return rv;
+ }
+
+ // If the selection hasn't been set up yet, set it up collapsed to the end of
+ // our editable content.
+ // XXX I think that this shouldn't do it in `HTMLEditor` because it maybe
+ // removed by the web app and if they call `Selection::AddRange()` without
+ // checking the range count, it may cause multiple selection ranges.
+ if (!SelectionRef().RangeCount()) {
+ nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() "
+ "failed");
+ return rv;
+ }
+ }
+
+ if (IsPlaintextMailComposer()) {
+ // XXX Should we do this in HTMLEditor? It's odd to guarantee that last
+ // empty line is visible only when it's in the plain text mode.
+ nsresult rv = EnsurePaddingBRElementInMultilineEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::EnsurePaddingBRElementInMultilineEditor() failed");
+ return rv;
+ }
+ }
+
+ Element* bodyOrDocumentElement = GetRoot();
+ if (NS_WARN_IF(!bodyOrDocumentElement && !GetDocument())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!bodyOrDocumentElement) {
+ return NS_OK;
+ }
+
+ rv = InsertBRElementToEmptyListItemsAndTableCellsInRange(
+ RawRangeBoundary(bodyOrDocumentElement, 0u),
+ RawRangeBoundary(bodyOrDocumentElement,
+ bodyOrDocumentElement->GetChildCount()));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange() "
+ "failed, but ignored");
+ return NS_OK;
+}
+
+void HTMLEditor::OnStartToHandleTopLevelEditSubAction(
+ EditSubAction aTopLevelEditSubAction,
+ nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRv.Failed());
+
+ EditorBase::OnStartToHandleTopLevelEditSubAction(
+ aTopLevelEditSubAction, aDirectionOfTopLevelEditSubAction, aRv);
+
+ MOZ_ASSERT(GetTopLevelEditSubAction() == aTopLevelEditSubAction);
+ MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() ==
+ aDirectionOfTopLevelEditSubAction);
+
+ if (NS_WARN_IF(Destroyed())) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ if (!mInitSucceeded) {
+ return; // We should do nothing if we're being initialized.
+ }
+
+ NS_WARNING_ASSERTION(
+ !aRv.Failed(),
+ "EditorBase::OnStartToHandleTopLevelEditSubAction() failed");
+
+ // Let's work with the latest layout information after (maybe) dispatching
+ // `beforeinput` event.
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+ document->FlushPendingNotifications(FlushType::Frames);
+ if (NS_WARN_IF(Destroyed())) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ // Remember where our selection was before edit action took place:
+ const auto atCompositionStart =
+ GetFirstIMESelectionStartPoint<EditorRawDOMPoint>();
+ if (atCompositionStart.IsSet()) {
+ // If there is composition string, let's remember current composition
+ // range.
+ TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(
+ atCompositionStart, GetLastIMESelectionEndPoint<EditorRawDOMPoint>());
+ } else {
+ // Get the selection location
+ // XXX This may occur so that I think that we shouldn't throw exception
+ // in this case.
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+ if (const nsRange* range = SelectionRef().GetRangeAt(0)) {
+ TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(*range);
+ }
+ }
+
+ // Register with range updater to track this as we perturb the doc
+ RangeUpdaterRef().RegisterRangeItem(
+ *TopLevelEditSubActionDataRef().mSelectedRange);
+
+ // Remember current inline styles for deletion and normal insertion ops
+ const bool cacheInlineStyles = [&]() {
+ switch (aTopLevelEditSubAction) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eDeleteSelectedContent:
+ return true;
+ default:
+ return IsPendingStyleCachePreservingSubAction(aTopLevelEditSubAction);
+ }
+ }();
+ if (cacheInlineStyles) {
+ const RefPtr<Element> editingHost =
+ ComputeEditingHost(LimitInBodyElement::No);
+ if (NS_WARN_IF(!editingHost)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsIContent* const startContainer =
+ HTMLEditUtils::GetContentToPreserveInlineStyles(
+ TopLevelEditSubActionDataRef()
+ .mSelectedRange->StartPoint<EditorRawDOMPoint>(),
+ *editingHost);
+ if (NS_WARN_IF(!startContainer)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ if (const RefPtr<Element> startContainerElement =
+ startContainer->GetAsElementOrParentElement()) {
+ nsresult rv = CacheInlineStyles(*startContainerElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::CacheInlineStyles() failed");
+ aRv.Throw(rv);
+ return;
+ }
+ }
+ }
+
+ // Stabilize the document against contenteditable count changes
+ if (document->GetEditingState() == Document::EditingState::eContentEditable) {
+ document->ChangeContentEditableCount(nullptr, +1);
+ TopLevelEditSubActionDataRef().mRestoreContentEditableCount = true;
+ }
+
+ // Check that selection is in subtree defined by body node
+ nsresult rv = EnsureSelectionInBodyOrDocumentElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() "
+ "failed, but ignored");
+}
+
+nsresult HTMLEditor::OnEndHandlingTopLevelEditSubAction() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ nsresult rv;
+ while (true) {
+ if (NS_WARN_IF(Destroyed())) {
+ rv = NS_ERROR_EDITOR_DESTROYED;
+ break;
+ }
+
+ if (!mInitSucceeded) {
+ rv = NS_OK; // We should do nothing if we're being initialized.
+ break;
+ }
+
+ // Do all the tricky stuff
+ rv = OnEndHandlingTopLevelEditSubActionInternal();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() failied");
+ // Perhaps, we need to do the following jobs even if the editor has been
+ // destroyed since they adjust some states of HTML document but don't
+ // modify the DOM tree nor Selection.
+
+ // Free up selectionState range item
+ if (TopLevelEditSubActionDataRef().mSelectedRange) {
+ RangeUpdaterRef().DropRangeItem(
+ *TopLevelEditSubActionDataRef().mSelectedRange);
+ }
+
+ // Reset the contenteditable count to its previous value
+ if (TopLevelEditSubActionDataRef().mRestoreContentEditableCount) {
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ if (document->GetEditingState() ==
+ Document::EditingState::eContentEditable) {
+ document->ChangeContentEditableCount(nullptr, -1);
+ }
+ }
+ break;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ EditorBase::OnEndHandlingTopLevelEditSubAction();
+ NS_WARNING_ASSERTION(
+ NS_FAILED(rv) || NS_SUCCEEDED(rvIgnored),
+ "EditorBase::OnEndHandlingTopLevelEditSubAction() failed, but ignored");
+ MOZ_ASSERT(!GetTopLevelEditSubAction());
+ MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == eNone);
+ return rv;
+}
+
+nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ nsresult rv = EnsureSelectionInBodyOrDocumentElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() "
+ "failed, but ignored");
+
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eReplaceHeadWithHTMLSource:
+ case EditSubAction::eCreatePaddingBRElementForEmptyEditor:
+ return NS_OK;
+ default:
+ break;
+ }
+
+ if (TopLevelEditSubActionDataRef().mChangedRange->IsPositioned() &&
+ GetTopLevelEditSubAction() != EditSubAction::eUndo &&
+ GetTopLevelEditSubAction() != EditSubAction::eRedo) {
+ // don't let any txns in here move the selection around behind our back.
+ // Note that this won't prevent explicit selection setting from working.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ {
+ EditorDOMRange changedRange(
+ *TopLevelEditSubActionDataRef().mChangedRange);
+ if (changedRange.IsPositioned() &&
+ changedRange.EnsureNotInNativeAnonymousSubtree()) {
+ bool isBlockLevelSubAction = false;
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eInsertLineBreak:
+ case EditSubAction::eInsertParagraphSeparator:
+ case EditSubAction::eDeleteText: {
+ // XXX We should investigate whether this is really needed because
+ // it seems that the following code does not handle the
+ // white-spaces.
+ RefPtr<nsRange> extendedChangedRange =
+ CreateRangeIncludingAdjuscentWhiteSpaces(changedRange);
+ if (extendedChangedRange) {
+ MOZ_ASSERT(extendedChangedRange->IsPositioned());
+ // Use extended range temporarily.
+ TopLevelEditSubActionDataRef().mChangedRange =
+ std::move(extendedChangedRange);
+ }
+ break;
+ }
+ case EditSubAction::eCreateOrChangeList:
+ case EditSubAction::eCreateOrChangeDefinitionListItem:
+ case EditSubAction::eRemoveList:
+ case EditSubAction::eFormatBlockForHTMLCommand:
+ case EditSubAction::eCreateOrRemoveBlock:
+ case EditSubAction::eIndent:
+ case EditSubAction::eOutdent:
+ case EditSubAction::eSetOrClearAlignment:
+ case EditSubAction::eSetPositionToAbsolute:
+ case EditSubAction::eSetPositionToStatic:
+ case EditSubAction::eDecreaseZIndex:
+ case EditSubAction::eIncreaseZIndex:
+ isBlockLevelSubAction = true;
+ [[fallthrough]];
+ default: {
+ Element* editingHost = ComputeEditingHost();
+ if (MOZ_UNLIKELY(!editingHost)) {
+ break;
+ }
+ RefPtr<nsRange> extendedChangedRange = AutoRangeArray::
+ CreateRangeWrappingStartAndEndLinesContainingBoundaries(
+ changedRange, GetTopLevelEditSubAction(),
+ isBlockLevelSubAction
+ ? BlockInlineCheck::UseHTMLDefaultStyle
+ : BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ *editingHost);
+ if (!extendedChangedRange) {
+ break;
+ }
+ MOZ_ASSERT(extendedChangedRange->IsPositioned());
+ // Use extended range temporarily.
+ TopLevelEditSubActionDataRef().mChangedRange =
+ std::move(extendedChangedRange);
+ break;
+ }
+ }
+ }
+ }
+
+ // if we did a ranged deletion or handling backspace key, make sure we have
+ // a place to put caret.
+ // Note we only want to do this if the overall operation was deletion,
+ // not if deletion was done along the way for
+ // EditSubAction::eInsertHTMLSource, EditSubAction::eInsertText, etc.
+ // That's why this is here rather than DeleteSelectionAsSubAction().
+ // However, we shouldn't insert <br> elements if we've already removed
+ // empty block parents because users may want to disappear the line by
+ // the deletion.
+ // XXX We should make HandleDeleteSelection() store expected container
+ // for handling this here since we cannot trust current selection is
+ // collapsed at deleted point.
+ if (GetTopLevelEditSubAction() == EditSubAction::eDeleteSelectedContent &&
+ TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange &&
+ !TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks) {
+ const auto newCaretPosition =
+ GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (!newCaretPosition.IsSet()) {
+ NS_WARNING("There was no selection range");
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ Result<CaretPoint, nsresult> caretPointOrError =
+ InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ newCaretPosition);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() "
+ "failed");
+ return caretPointOrError.unwrapErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ }
+
+ // add in any needed <br>s, and remove any unneeded ones.
+ nsresult rv = InsertBRElementToEmptyListItemsAndTableCellsInRange(
+ TopLevelEditSubActionDataRef().mChangedRange->StartRef().AsRaw(),
+ TopLevelEditSubActionDataRef().mChangedRange->EndRef().AsRaw());
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange()"
+ " failed, but ignored");
+
+ // merge any adjacent text nodes
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ break;
+ default: {
+ nsresult rv = CollapseAdjacentTextNodes(
+ MOZ_KnownLive(*TopLevelEditSubActionDataRef().mChangedRange));
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::CollapseAdjacentTextNodes() failed");
+ return rv;
+ }
+ break;
+ }
+ }
+
+ // Clean up any empty nodes in the changed range unless they are inserted
+ // intentionally.
+ if (TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements) {
+ nsresult rv = RemoveEmptyNodesIn(
+ EditorDOMRange(*TopLevelEditSubActionDataRef().mChangedRange));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::RemoveEmptyNodesIn() failed");
+ return rv;
+ }
+ }
+
+ // attempt to transform any unneeded nbsp's into spaces after doing various
+ // operations
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eDeleteSelectedContent:
+ if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) {
+ break;
+ }
+ [[fallthrough]];
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eInsertLineBreak:
+ case EditSubAction::eInsertParagraphSeparator:
+ case EditSubAction::ePasteHTMLContent:
+ case EditSubAction::eInsertHTMLSource: {
+ // Due to the replacement of white-spaces in
+ // WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(),
+ // selection ranges may be changed since DOM ranges track the DOM
+ // mutation by themselves. However, we want to keep selection as-is.
+ // Therefore, we should restore `Selection` after replacing
+ // white-spaces.
+ AutoSelectionRestorer restoreSelection(*this);
+ // TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII
+ // white-spaces with NPSPs and then, we'll replace them with ASCII
+ // white-spaces here. We should avoid this overwriting things as
+ // far as possible because replacing characters in text nodes
+ // causes running mutation event listeners which are really
+ // expensive.
+ // Adjust end of composition string if there is composition string.
+ auto pointToAdjust = GetLastIMESelectionEndPoint<EditorDOMPoint>();
+ if (!pointToAdjust.IsInContentNode()) {
+ // Otherwise, adjust current selection start point.
+ pointToAdjust = GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToAdjust.IsInContentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ if (EditorUtils::IsEditableContent(
+ *pointToAdjust.ContainerAs<nsIContent>(), EditorType::HTML)) {
+ AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(),
+ &pointToAdjust);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ *this, pointToAdjust);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
+ "failed");
+ return rv;
+ }
+ }
+
+ // also do this for original selection endpoints.
+ // XXX Hmm, if `NormalizeVisibleWhiteSpacesAt()` runs mutation event
+ // listener and that causes changing `mSelectedRange`, what we
+ // should do?
+ if (NS_WARN_IF(!TopLevelEditSubActionDataRef()
+ .mSelectedRange->IsPositioned())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorDOMPoint atStart =
+ TopLevelEditSubActionDataRef().mSelectedRange->StartPoint();
+ if (atStart != pointToAdjust && atStart.IsInContentNode() &&
+ EditorUtils::IsEditableContent(*atStart.ContainerAs<nsIContent>(),
+ EditorType::HTML)) {
+ AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(),
+ &pointToAdjust);
+ AutoTrackDOMPoint trackStartPoint(RangeUpdaterRef(), &atStart);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ *this, atStart);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
+ "failed, but ignored");
+ }
+ // we only need to handle old selection endpoint if it was different
+ // from start
+ EditorDOMPoint atEnd =
+ TopLevelEditSubActionDataRef().mSelectedRange->EndPoint();
+ if (!TopLevelEditSubActionDataRef().mSelectedRange->Collapsed() &&
+ atEnd != pointToAdjust && atEnd != atStart &&
+ atEnd.IsInContentNode() &&
+ EditorUtils::IsEditableContent(*atEnd.ContainerAs<nsIContent>(),
+ EditorType::HTML)) {
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(*this,
+ atEnd);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
+ "failed, but ignored");
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ // Adjust selection for insert text, html paste, and delete actions if
+ // we haven't removed new empty blocks. Note that if empty block parents
+ // are removed, Selection should've been adjusted by the method which
+ // did it.
+ if (!TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks &&
+ SelectionRef().IsCollapsed()) {
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eDeleteSelectedContent:
+ case EditSubAction::eInsertLineBreak:
+ case EditSubAction::eInsertParagraphSeparator:
+ case EditSubAction::ePasteHTMLContent:
+ case EditSubAction::eInsertHTMLSource:
+ // XXX AdjustCaretPositionAndEnsurePaddingBRElement() intentionally
+ // does not create padding `<br>` element for empty editor.
+ // Investigate which is better that whether this should does it
+ // or wait MaybeCreatePaddingBRElementForEmptyEditor().
+ rv = AdjustCaretPositionAndEnsurePaddingBRElement(
+ GetDirectionOfTopLevelEditSubAction());
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::AdjustCaretPositionAndEnsurePaddingBRElement() "
+ "failed");
+ return rv;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ // check for any styles which were removed inappropriately
+ bool reapplyCachedStyle;
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eDeleteSelectedContent:
+ reapplyCachedStyle = true;
+ break;
+ default:
+ reapplyCachedStyle =
+ IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction());
+ break;
+ }
+
+ // If the selection is in empty inline HTML elements, we should delete
+ // them unless it's inserted intentionally.
+ if (mPlaceholderBatch &&
+ TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements &&
+ SelectionRef().IsCollapsed() && SelectionRef().GetFocusNode()) {
+ RefPtr<Element> mostDistantEmptyInlineAncestor = nullptr;
+ for (Element* ancestor :
+ SelectionRef().GetFocusNode()->InclusiveAncestorsOfType<Element>()) {
+ if (!ancestor->IsHTMLElement() ||
+ !HTMLEditUtils::IsRemovableFromParentNode(*ancestor) ||
+ !HTMLEditUtils::IsEmptyInlineContainer(
+ *ancestor, {EmptyCheckOption::TreatSingleBRElementAsVisible},
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ break;
+ }
+ mostDistantEmptyInlineAncestor = ancestor;
+ }
+ if (mostDistantEmptyInlineAncestor) {
+ nsresult rv =
+ DeleteNodeWithTransaction(*mostDistantEmptyInlineAncestor);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteNodeWithTransaction() failed at deleting "
+ "empty inline ancestors");
+ return rv;
+ }
+ }
+ }
+
+ // But the cached inline styles should be restored from type-in-state later.
+ if (reapplyCachedStyle) {
+ DebugOnly<nsresult> rvIgnored =
+ mPendingStylesToApplyToNewContent->UpdateSelState(*this);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "PendingStyles::UpdateSelState() failed, but ignored");
+ rvIgnored = ReapplyCachedStyles();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::ReapplyCachedStyles() failed, but ignored");
+ TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
+ }
+ }
+
+ rv = HandleInlineSpellCheck(
+ TopLevelEditSubActionDataRef().mSelectedRange->StartPoint(),
+ TopLevelEditSubActionDataRef().mChangedRange);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::HandleInlineSpellCheck() failed");
+ return rv;
+ }
+
+ // detect empty doc
+ // XXX Need to investigate when the padding <br> element is removed because
+ // I don't see the <br> element with testing manually. If it won't be
+ // used, we can get rid of this cost.
+ rv = MaybeCreatePaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed");
+ return rv;
+ }
+
+ // adjust selection HINT if needed
+ if (!TopLevelEditSubActionDataRef().mDidExplicitlySetInterLine &&
+ SelectionRef().IsCollapsed()) {
+ SetSelectionInterlinePosition();
+ }
+
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::CanHandleHTMLEditSubAction(
+ CheckSelectionInReplacedElement aCheckSelectionInReplacedElement
+ /* = CheckSelectionInReplacedElement::Yes */) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ // If there is not selection ranges, we should ignore the result.
+ if (!SelectionRef().RangeCount()) {
+ return EditActionResult::CanceledResult();
+ }
+
+ const nsRange* range = SelectionRef().GetRangeAt(0);
+ nsINode* selStartNode = range->GetStartContainer();
+ if (NS_WARN_IF(!selStartNode) || NS_WARN_IF(!selStartNode->IsContent())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (!HTMLEditUtils::IsSimplyEditableNode(*selStartNode)) {
+ return EditActionResult::CanceledResult();
+ }
+
+ nsINode* selEndNode = range->GetEndContainer();
+ if (NS_WARN_IF(!selEndNode) || NS_WARN_IF(!selEndNode->IsContent())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (selStartNode == selEndNode) {
+ if (aCheckSelectionInReplacedElement ==
+ CheckSelectionInReplacedElement::Yes &&
+ HTMLEditUtils::IsNonEditableReplacedContent(
+ *selStartNode->AsContent())) {
+ return EditActionResult::CanceledResult();
+ }
+ return EditActionResult::IgnoredResult();
+ }
+
+ if (HTMLEditUtils::IsNonEditableReplacedContent(*selStartNode->AsContent()) ||
+ HTMLEditUtils::IsNonEditableReplacedContent(*selEndNode->AsContent())) {
+ return EditActionResult::CanceledResult();
+ }
+
+ if (!HTMLEditUtils::IsSimplyEditableNode(*selEndNode)) {
+ return EditActionResult::CanceledResult();
+ }
+
+ // If anchor node is in an HTML element which has inert attribute, we should
+ // do nothing.
+ // XXX HTMLEditor typically uses first range instead of anchor/focus range.
+ // Therefore, referring first range here is more reasonable than
+ // anchor/focus range of Selection.
+ nsIContent* const selAnchorContent = SelectionRef().GetDirection() == eDirNext
+ ? nsIContent::FromNode(selStartNode)
+ : nsIContent::FromNode(selEndNode);
+ if (selAnchorContent &&
+ HTMLEditUtils::ContentIsInert(*selAnchorContent->AsContent())) {
+ return EditActionResult::CanceledResult();
+ }
+
+ // XXX What does it mean the common ancestor is editable? I have no idea.
+ // It should be in same (active) editing host, and even if it's editable,
+ // there may be non-editable contents in the range.
+ nsINode* commonAncestor = range->GetClosestCommonInclusiveAncestor();
+ if (MOZ_UNLIKELY(!commonAncestor)) {
+ NS_WARNING(
+ "AbstractRange::GetClosestCommonInclusiveAncestor() returned nullptr");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return HTMLEditUtils::IsSimplyEditableNode(*commonAncestor)
+ ? EditActionResult::IgnoredResult()
+ : EditActionResult::CanceledResult();
+}
+
+MOZ_CAN_RUN_SCRIPT static nsStaticAtom& MarginPropertyAtomForIndent(
+ nsIContent& aContent) {
+ nsAutoString direction;
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetComputedProperty(
+ aContent, *nsGkAtoms::direction, direction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::direction)"
+ " failed, but ignored");
+ return direction.EqualsLiteral("rtl") ? *nsGkAtoms::marginRight
+ : *nsGkAtoms::marginLeft;
+}
+
+nsresult HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(SelectionRef().IsCollapsed());
+
+ // If we are after a padding `<br>` element for empty last line in the same
+ // block, then move selection to be before it
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorRawDOMPoint atSelectionStart(firstRange->StartRef());
+ if (NS_WARN_IF(!atSelectionStart.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atSelectionStart.IsSetAndValid());
+
+ if (!atSelectionStart.IsInContentNode()) {
+ return NS_OK;
+ }
+
+ Element* editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ NS_WARNING(
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() did nothing "
+ "because of no editing host");
+ return NS_OK;
+ }
+
+ nsIContent* previousBRElement = HTMLEditUtils::GetPreviousContent(
+ atSelectionStart, {}, BlockInlineCheck::UseComputedDisplayStyle,
+ editingHost);
+ if (!previousBRElement || !previousBRElement->IsHTMLElement(nsGkAtoms::br) ||
+ !previousBRElement->GetParent() ||
+ !EditorUtils::IsEditableContent(*previousBRElement->GetParent(),
+ EditorType::HTML) ||
+ !HTMLEditUtils::IsInvisibleBRElement(*previousBRElement)) {
+ return NS_OK;
+ }
+
+ const RefPtr<const Element> blockElementAtSelectionStart =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *atSelectionStart.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ const RefPtr<const Element> parentBlockElementOfBRElement =
+ HTMLEditUtils::GetAncestorElement(
+ *previousBRElement, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+
+ if (!blockElementAtSelectionStart ||
+ blockElementAtSelectionStart != parentBlockElementOfBRElement) {
+ return NS_OK;
+ }
+
+ // If we are here then the selection is right after a padding <br>
+ // element for empty last line that is in the same block as the
+ // selection. We need to move the selection start to be before the
+ // padding <br> element.
+ EditorRawDOMPoint atInvisibleBRElement(previousBRElement);
+ nsresult rv = CollapseSelectionTo(atInvisibleBRElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (mPaddingBRElementForEmptyEditor) {
+ return NS_OK;
+ }
+
+ // XXX I think that we should not insert a <br> element if we're for a web
+ // content. Probably, this is required only by chrome editors such as
+ // the mail composer of Thunderbird and the composer of SeaMonkey.
+
+ const RefPtr<Element> bodyOrDocumentElement = GetRoot();
+ if (!bodyOrDocumentElement) {
+ return NS_OK;
+ }
+
+ // Skip adding the padding <br> element for empty editor if body
+ // is read-only.
+ if (!HTMLEditUtils::IsSimplyEditableNode(*bodyOrDocumentElement)) {
+ return NS_OK;
+ }
+
+ // Now we've got the body element. Iterate over the body element's children,
+ // looking for editable content. If no editable content is found, insert the
+ // padding <br> element.
+ EditorType editorType = GetEditorType();
+ bool isRootEditable =
+ EditorUtils::IsEditableContent(*bodyOrDocumentElement, editorType);
+ for (nsIContent* child = bodyOrDocumentElement->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (EditorUtils::IsPaddingBRElementForEmptyEditor(*child) ||
+ !isRootEditable || EditorUtils::IsEditableContent(*child, editorType) ||
+ HTMLEditUtils::IsBlockElement(
+ *child, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return NS_OK;
+ }
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eCreatePaddingBRElementForEmptyEditor,
+ nsIEditor::eNone, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // Create a br.
+ RefPtr<Element> newBRElement = CreateHTMLContent(nsGkAtoms::br);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_WARN_IF(!newBRElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mPaddingBRElementForEmptyEditor =
+ static_cast<HTMLBRElement*>(newBRElement.get());
+
+ // Give it a special attribute.
+ newBRElement->SetFlags(NS_PADDING_FOR_EMPTY_EDITOR);
+
+ // Put the node in the document.
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertNodeWithTransaction<Element>(
+ *newBRElement, EditorDOMPoint(bodyOrDocumentElement, 0u));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertBRElementResult.unwrapErr();
+ }
+
+ // Set selection.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionToStartOf() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+ return NS_OK;
+}
+
+nsresult HTMLEditor::EnsureNoPaddingBRElementForEmptyEditor() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!mPaddingBRElementForEmptyEditor) {
+ return NS_OK;
+ }
+
+ // If we're an HTML editor, a mutation event listener may recreate padding
+ // <br> element for empty editor again during the call of
+ // DeleteNodeWithTransaction(). So, move it first.
+ RefPtr<HTMLBRElement> paddingBRElement(
+ std::move(mPaddingBRElementForEmptyEditor));
+ nsresult rv = DeleteNodeWithTransaction(*paddingBRElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::ReflectPaddingBRElementForEmptyEditor() {
+ if (NS_WARN_IF(!mRootElement)) {
+ NS_WARNING("Failed to handle padding BR element due to no root element");
+ return NS_ERROR_FAILURE;
+ }
+ // The idea here is to see if the magic empty node has suddenly reappeared. If
+ // it has, set our state so we remember it. There is a tradeoff between doing
+ // here and at redo, or doing it everywhere else that might care. Since undo
+ // and redo are relatively rare, it makes sense to take the (small)
+ // performance hit here.
+ nsIContent* firstLeafChild = HTMLEditUtils::GetFirstLeafContent(
+ *mRootElement, {LeafNodeType::OnlyLeafNode});
+ if (firstLeafChild &&
+ EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) {
+ mPaddingBRElementForEmptyEditor =
+ static_cast<HTMLBRElement*>(firstLeafChild);
+ } else {
+ mPaddingBRElementForEmptyEditor = nullptr;
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::PrepareInlineStylesForCaret() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(SelectionRef().IsCollapsed());
+
+ // XXX This method works with the top level edit sub-action, but this
+ // must be wrong if we are handling nested edit action.
+
+ if (TopLevelEditSubActionDataRef().mDidDeleteSelection) {
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eDeleteSelectedContent: {
+ nsresult rv = ReapplyCachedStyles();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::ReapplyCachedStyles() failed");
+ return rv;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ // For most actions we want to clear the cached styles, but there are
+ // exceptions
+ if (!IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) {
+ TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
+ }
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
+ EditSubAction aEditSubAction, const nsAString& aInsertionString,
+ SelectionHandling aSelectionHandling) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText ||
+ aEditSubAction == EditSubAction::eInsertTextComingFromIME);
+ MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore,
+ aEditSubAction == EditSubAction::eInsertTextComingFromIME);
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ // If the selection isn't collapsed, delete it. Don't delete existing inline
+ // tags, because we're hopefully going to insert text (bug 787432).
+ if (!SelectionRef().IsCollapsed() &&
+ aSelectionHandling == SelectionHandling::Delete) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(nsIEditor::eNone, "
+ "nsIEditor::eNoStrip) failed");
+ return Err(rv);
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost(
+ GetDocument()->IsXMLDocument() ? LimitInBodyElement::No
+ : LimitInBodyElement::Yes);
+ if (NS_WARN_IF(!editingHost)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ auto pointToInsert = GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (MOZ_UNLIKELY(!pointToInsert.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // for every property that is set, insert a new inline style node
+ Result<EditorDOMPoint, nsresult> setStyleResult =
+ CreateStyleForInsertText(pointToInsert, *editingHost);
+ if (MOZ_UNLIKELY(setStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed");
+ return setStyleResult.propagateErr();
+ }
+ if (setStyleResult.inspect().IsSet()) {
+ pointToInsert = setStyleResult.unwrap();
+ }
+
+ if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) ||
+ NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ MOZ_ASSERT(pointToInsert.IsSetAndValid());
+
+ // If the point is not in an element which can contain text nodes, climb up
+ // the DOM tree.
+ if (!pointToInsert.IsInTextNode()) {
+ while (!HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(),
+ *nsGkAtoms::textTagName)) {
+ if (NS_WARN_IF(pointToInsert.GetContainer() == editingHost) ||
+ NS_WARN_IF(!pointToInsert.GetContainerParentAs<nsIContent>())) {
+ NS_WARNING("Selection start point couldn't have text nodes");
+ return Err(NS_ERROR_FAILURE);
+ }
+ pointToInsert.Set(pointToInsert.ContainerAs<nsIContent>());
+ }
+ }
+
+ if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
+ auto compositionStartPoint =
+ GetFirstIMESelectionStartPoint<EditorDOMPoint>();
+ if (!compositionStartPoint.IsSet()) {
+ compositionStartPoint = pointToInsert;
+ }
+
+ if (aInsertionString.IsEmpty()) {
+ // Right now the WhiteSpaceVisibilityKeeper code bails on empty strings,
+ // but IME needs the InsertTextWithTransaction() call to still happen
+ // since empty strings are meaningful there.
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, aInsertionString,
+ compositionStartPoint);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ nsresult rv = insertTextResult.unwrap().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+
+ auto compositionEndPoint = GetLastIMESelectionEndPoint<EditorDOMPoint>();
+ if (!compositionEndPoint.IsSet()) {
+ compositionEndPoint = compositionStartPoint;
+ }
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ WhiteSpaceVisibilityKeeper::ReplaceText(
+ *this, aInsertionString,
+ EditorDOMRange(compositionStartPoint, compositionEndPoint),
+ *editingHost);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // CompositionTransaction should've set selection so that we should ignore
+ // caret suggestion.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+
+ compositionStartPoint = GetFirstIMESelectionStartPoint<EditorDOMPoint>();
+ compositionEndPoint = GetLastIMESelectionEndPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!compositionStartPoint.IsSet()) ||
+ NS_WARN_IF(!compositionEndPoint.IsSet())) {
+ // Mutation event listener has changed the DOM tree...
+ return EditActionResult::HandledResult();
+ }
+ nsresult rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
+ compositionStartPoint.ToRawRangeBoundary(),
+ compositionEndPoint.ToRawRangeBoundary());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText);
+
+ // find where we are
+ EditorDOMPoint currentPoint(pointToInsert);
+
+ // is our text going to be PREformatted?
+ // We remember this so that we know how to handle tabs.
+ const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
+ *pointToInsert.ContainerAs<nsIContent>());
+
+ // turn off the edit listener: we know how to
+ // build the "doc changed range" ourselves, and it's
+ // must faster to do it once here than to track all
+ // the changes one at a time.
+ AutoRestore<bool> disableListener(
+ EditSubActionDataRef().mAdjustChangedRangeFromListener);
+ EditSubActionDataRef().mAdjustChangedRangeFromListener = false;
+
+ // don't change my selection in subtransactions
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+ int32_t pos = 0;
+ constexpr auto newlineStr = NS_LITERAL_STRING_FROM_CSTRING(LFSTR);
+
+ {
+ AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert);
+
+ // for efficiency, break out the pre case separately. This is because
+ // its a lot cheaper to search the input string for only newlines than
+ // it is to search for both tabs and newlines.
+ if (!isWhiteSpaceCollapsible || IsPlaintextMailComposer()) {
+ while (pos != -1 &&
+ pos < AssertedCast<int32_t>(aInsertionString.Length())) {
+ int32_t oldPos = pos;
+ int32_t subStrLen;
+ pos = aInsertionString.FindChar(nsCRT::LF, oldPos);
+
+ if (pos != -1) {
+ subStrLen = pos - oldPos;
+ // if first char is newline, then use just it
+ if (!subStrLen) {
+ subStrLen = 1;
+ }
+ } else {
+ subStrLen = aInsertionString.Length() - oldPos;
+ pos = aInsertionString.Length();
+ }
+
+ nsDependentSubstring subStr(aInsertionString, oldPos, subStrLen);
+
+ // is it a return?
+ if (subStr.Equals(newlineStr)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, currentPoint);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // We don't want to update selection here because we've blocked
+ // InsertNodeTransaction updating selection with
+ // dontChangeMySelection.
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(!AllowsTransactionsToChangeSelection());
+
+ pos++;
+ RefPtr<Element> brElement =
+ unwrappedInsertBRElementResult.UnwrapNewNode();
+ if (brElement->GetNextSibling()) {
+ pointToInsert.Set(brElement->GetNextSibling());
+ } else {
+ pointToInsert.SetToEndOf(currentPoint.GetContainer());
+ }
+ // XXX In most cases, pointToInsert and currentPoint are same here.
+ // But if the <br> element has been moved to different point by
+ // mutation observer, those points become different.
+ currentPoint.SetAfter(brElement);
+ NS_WARNING_ASSERTION(currentPoint.IsSet(),
+ "Failed to set after the <br> element");
+ NS_WARNING_ASSERTION(currentPoint == pointToInsert,
+ "Perhaps, <br> element position has been moved "
+ "to different point "
+ "by mutation observer");
+ } else {
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, subStr, currentPoint);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ // Ignore the caret suggestion because of `dontChangeMySelection`
+ // above.
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ if (insertTextResult.inspect().Handled()) {
+ pointToInsert = currentPoint = insertTextResult.unwrap()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>();
+ } else {
+ pointToInsert = currentPoint;
+ }
+ }
+ }
+ } else {
+ constexpr auto tabStr = u"\t"_ns;
+ constexpr auto spacesStr = u" "_ns;
+ nsAutoString insertionString(aInsertionString); // For FindCharInSet().
+ while (pos != -1 &&
+ pos < AssertedCast<int32_t>(insertionString.Length())) {
+ int32_t oldPos = pos;
+ int32_t subStrLen;
+ pos = insertionString.FindCharInSet(u"\t\n", oldPos);
+
+ if (pos != -1) {
+ subStrLen = pos - oldPos;
+ // if first char is newline, then use just it
+ if (!subStrLen) {
+ subStrLen = 1;
+ }
+ } else {
+ subStrLen = insertionString.Length() - oldPos;
+ pos = insertionString.Length();
+ }
+
+ nsDependentSubstring subStr(insertionString, oldPos, subStrLen);
+
+ // is it a tab?
+ if (subStr.Equals(tabStr)) {
+ Result<InsertTextResult, nsresult> insertTextResult =
+ WhiteSpaceVisibilityKeeper::InsertText(
+ *this, spacesStr, currentPoint, *editingHost);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed");
+ return insertTextResult.propagateErr();
+ }
+ // Ignore the caret suggestion because of `dontChangeMySelection`
+ // above.
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ pos++;
+ if (insertTextResult.inspect().Handled()) {
+ pointToInsert = currentPoint = insertTextResult.unwrap()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>();
+ MOZ_ASSERT(pointToInsert.IsSet());
+ } else {
+ pointToInsert = currentPoint;
+ MOZ_ASSERT(pointToInsert.IsSet());
+ }
+ }
+ // is it a return?
+ else if (subStr.Equals(newlineStr)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ WhiteSpaceVisibilityKeeper::InsertBRElement(*this, currentPoint,
+ *editingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // TODO: Some methods called for handling non-preformatted text use
+ // ComputeEditingHost(). Therefore, they depend on the latest
+ // selection. So we cannot skip updating selection here.
+ nsresult rv = unwrappedInsertBRElementResult.SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ pos++;
+ RefPtr<Element> newBRElement =
+ unwrappedInsertBRElementResult.UnwrapNewNode();
+ MOZ_DIAGNOSTIC_ASSERT(newBRElement);
+ if (newBRElement->GetNextSibling()) {
+ pointToInsert.Set(newBRElement->GetNextSibling());
+ } else {
+ pointToInsert.SetToEndOf(currentPoint.GetContainer());
+ }
+ currentPoint.SetAfter(newBRElement);
+ NS_WARNING_ASSERTION(currentPoint.IsSet(),
+ "Failed to set after the new <br> element");
+ // XXX If the newBRElement has been moved or removed by mutation
+ // observer, we hit this assert. We need to check if
+ // newBRElement is in expected point, though, we must have
+ // a lot of same bugs...
+ NS_WARNING_ASSERTION(
+ currentPoint == pointToInsert,
+ "Perhaps, newBRElement has been moved or removed unexpectedly");
+ } else {
+ Result<InsertTextResult, nsresult> insertTextResult =
+ WhiteSpaceVisibilityKeeper::InsertText(
+ *this, subStr, currentPoint, *editingHost);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed");
+ return insertTextResult.propagateErr();
+ }
+ // Ignore the caret suggestion because of `dontChangeMySelection`
+ // above.
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ if (insertTextResult.inspect().Handled()) {
+ pointToInsert = currentPoint = insertTextResult.unwrap()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>();
+ MOZ_ASSERT(pointToInsert.IsSet());
+ } else {
+ pointToInsert = currentPoint;
+ MOZ_ASSERT(pointToInsert.IsSet());
+ }
+ }
+ }
+ }
+
+ // After this block, pointToInsert is updated by AutoTrackDOMPoint.
+ }
+
+ if (currentPoint.IsSet()) {
+ currentPoint.SetInterlinePosition(InterlinePosition::EndOfLine);
+ nsresult rv = CollapseSelectionTo(currentPoint);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Selection::Collapse() failed, but ignored");
+
+ // manually update the doc changed range so that AfterEdit will clean up
+ // the correct portion of the document.
+ rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
+ pointToInsert.ToRawRangeBoundary(), currentPoint.ToRawRangeBoundary());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "EndOfLine) failed, but ignored");
+ rv = TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(pointToInsert);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsRange::CollapseTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::InsertLineBreakAsSubAction() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ // XXX This may be called by execCommand() with "insertLineBreak".
+ // In such case, naming the transaction "TypingTxnName" is odd.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+
+ // calling it text insertion to trigger moz br treatment by rules
+ // XXX Why do we use EditSubAction::eInsertText here? Looks like
+ // EditSubAction::eInsertLineBreak or EditSubAction::eInsertNode
+ // is better.
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ UndefineCaretBidiLevel();
+
+ // If the selection isn't collapsed, delete it.
+ if (!SelectionRef().IsCollapsed()) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed");
+ return rv;
+ }
+ }
+
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorDOMPoint atStartOfSelection(firstRange->StartRef());
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
+
+ RefPtr<Element> editingHost = ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // For backward compatibility, we should not insert a linefeed if
+ // paragraph separator is set to "br" which is Gecko-specific mode.
+ if (GetDefaultParagraphSeparator() == ParagraphSeparator::br ||
+ !HTMLEditUtils::ShouldInsertLinefeedCharacter(atStartOfSelection,
+ *editingHost)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, atStartOfSelection,
+ nsIEditor::eNext);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+
+ auto pointToPutCaret =
+ EditorDOMPoint::After(*unwrappedInsertBRElementResult.GetNewNode());
+ if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) {
+ NS_WARNING("Inserted <br> was unexpectedly removed");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ WSScanResult backwardScanFromBeforeBRElementResult =
+ WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
+ editingHost,
+ EditorDOMPoint(unwrappedInsertBRElementResult.GetNewNode()),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(backwardScanFromBeforeBRElementResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ WSScanResult forwardScanFromAfterBRElementResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ editingHost, pointToPutCaret,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool brElementIsAfterBlock =
+ backwardScanFromBeforeBRElementResult.ReachedBlockBoundary();
+ const bool brElementIsBeforeBlock =
+ forwardScanFromAfterBRElementResult.ReachedBlockBoundary();
+ const bool isEmptyEditingHost = HTMLEditUtils::IsEmptyNode(
+ *editingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible});
+ if (brElementIsBeforeBlock &&
+ (isEmptyEditingHost || !brElementIsAfterBlock)) {
+ // Empty last line is invisible if it's immediately before either parent
+ // or another block's boundary so that we need to put invisible <br>
+ // element here for making it visible.
+ Result<CreateElementResult, nsresult> invisibleAdditionalBRElementResult =
+ WhiteSpaceVisibilityKeeper::InsertBRElement(*this, pointToPutCaret,
+ *editingHost);
+ if (MOZ_UNLIKELY(invisibleAdditionalBRElementResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed");
+ return invisibleAdditionalBRElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInvisibleAdditionalBRElement =
+ invisibleAdditionalBRElementResult.unwrap();
+ pointToPutCaret.Set(unwrappedInvisibleAdditionalBRElement.GetNewNode());
+ unwrappedInvisibleAdditionalBRElement.IgnoreCaretPointSuggestion();
+ } else if (forwardScanFromAfterBRElementResult
+ .InVisibleOrCollapsibleCharacters()) {
+ pointToPutCaret =
+ forwardScanFromAfterBRElementResult.Point<EditorDOMPoint>();
+ } else if (forwardScanFromAfterBRElementResult.ReachedSpecialContent()) {
+ // Next inserting text should be inserted into styled inline elements if
+ // they have first visible thing in the new line.
+ pointToPutCaret =
+ forwardScanFromAfterBRElementResult.PointAtContent<EditorDOMPoint>();
+ }
+
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CreateElementResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ atStartOfSelection = EditorDOMPoint(firstRange->StartRef());
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
+
+ // Do nothing if the node is read-only
+ if (!HTMLEditUtils::IsSimplyEditableNode(
+ *atStartOfSelection.GetContainer())) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ Result<EditorDOMPoint, nsresult> insertLineFeedResult =
+ HandleInsertLinefeed(atStartOfSelection, *editingHost);
+ if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertLinefeed() failed");
+ return insertLineFeedResult.unwrapErr();
+ }
+ rv = CollapseSelectionTo(insertLineFeedResult.inspect());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::InsertParagraphSeparatorAsSubAction(const Element& aEditingHost) {
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction(
+ CheckSelectionInReplacedElement::OnlyWhenNotInSameNode);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ // XXX This may be called by execCommand() with "insertParagraph".
+ // In such case, naming the transaction "TypingTxnName" is odd.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertParagraphSeparator, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ UndefineCaretBidiLevel();
+
+ // If the selection isn't collapsed, delete it.
+ if (!SelectionRef().IsCollapsed()) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed");
+ return Err(rv);
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ AutoRangeArray selectionRanges(SelectionRef());
+ {
+ // If the editing host is the body element, the selection may be outside
+ // aEditingHost. In the case, we should use the editing host outside the
+ // <body> only here for keeping our traditional behavior for now.
+ // This should be fixed in bug 1634351.
+ const Element* editingHostMaybeOutsideBody = &aEditingHost;
+ if (aEditingHost.IsHTMLElement(nsGkAtoms::body)) {
+ editingHostMaybeOutsideBody = ComputeEditingHost(LimitInBodyElement::No);
+ if (NS_WARN_IF(!editingHostMaybeOutsideBody)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ selectionRanges.EnsureOnlyEditableRanges(*editingHostMaybeOutsideBody);
+ if (NS_WARN_IF(selectionRanges.Ranges().IsEmpty())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ auto pointToInsert =
+ selectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ while (true) {
+ Element* element = pointToInsert.GetContainerOrContainerParentElement();
+ if (MOZ_UNLIKELY(!element)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // If the element can have a <br> element (it means that the element or its
+ // container must be able to have <div> or <p> too), we can handle
+ // insertParagraph at the point.
+ if (HTMLEditUtils::CanNodeContain(*element, *nsGkAtoms::br)) {
+ break;
+ }
+ // Otherwise, try to insert paragraph at the parent.
+ pointToInsert = pointToInsert.ParentPoint();
+ }
+
+ if (IsMailEditor()) {
+ if (RefPtr<Element> mailCiteElement = GetMostDistantAncestorMailCiteElement(
+ *pointToInsert.ContainerAs<nsIContent>())) {
+ // Split any mailcites in the way. Should we abort this if we encounter
+ // table cell boundaries?
+ Result<EditorDOMPoint, nsresult> atNewBRElementOrError =
+ HandleInsertParagraphInMailCiteElement(*mailCiteElement,
+ pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(atNewBRElementOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::HandleInsertParagraphInMailCiteElement() failed");
+ return atNewBRElementOrError.propagateErr();
+ }
+ EditorDOMPoint pointToPutCaret = atNewBRElementOrError.unwrap();
+ MOZ_ASSERT(pointToPutCaret.IsSet());
+ pointToPutCaret.SetInterlinePosition(InterlinePosition::StartOfNextLine);
+ MOZ_ASSERT(pointToPutCaret.GetChild());
+ MOZ_ASSERT(pointToPutCaret.GetChild()->IsHTMLElement(nsGkAtoms::br));
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+ }
+
+ // If the active editing host is an inline element, or if the active editing
+ // host is the block parent itself and we're configured to use <br> as a
+ // paragraph separator, just append a <br>.
+ // If the editing host parent element is editable, it means that the editing
+ // host must be a <body> element and the selection may be outside the body
+ // element. If the selection is outside the editing host, we should not
+ // insert new paragraph nor <br> element.
+ // XXX Currently, we don't support editing outside <body> element, but Blink
+ // does it.
+ if (aEditingHost.GetParentElement() &&
+ HTMLEditUtils::IsSimplyEditableNode(*aEditingHost.GetParentElement()) &&
+ !nsContentUtils::ContentIsFlattenedTreeDescendantOf(
+ pointToInsert.ContainerAs<nsIContent>(), &aEditingHost)) {
+ return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
+ }
+
+ auto InsertLineBreakInstead =
+ [](const Element* aEditableBlockElement,
+ const EditorDOMPoint& aCandidatePointToSplit,
+ ParagraphSeparator aDefaultParagraphSeparator,
+ const Element& aEditingHost) {
+ // If there is no block parent in the editing host, i.e., the editing
+ // host itself is also a non-block element, we should insert a line
+ // break.
+ if (!aEditableBlockElement) {
+ // XXX Chromium checks if the CSS box of the editing host is a block.
+ return true;
+ }
+
+ // If the editable block element is not splittable, e.g., it's an
+ // editing host, and the default paragraph separator is <br> or the
+ // element cannot contain a <p> element, we should insert a <br>
+ // element.
+ if (!HTMLEditUtils::IsSplittableNode(*aEditableBlockElement)) {
+ return aDefaultParagraphSeparator == ParagraphSeparator::br ||
+ !HTMLEditUtils::CanElementContainParagraph(
+ *aEditableBlockElement) ||
+ (HTMLEditUtils::ShouldInsertLinefeedCharacter(
+ aCandidatePointToSplit, aEditingHost) &&
+ HTMLEditUtils::IsDisplayOutsideInline(aEditingHost));
+ }
+
+ // If the nearest block parent is a single-line container declared in
+ // the execCommand spec and not the editing host, we should separate the
+ // block even if the default paragraph separator is <br> element.
+ if (HTMLEditUtils::IsSingleLineContainer(*aEditableBlockElement)) {
+ return false;
+ }
+
+ // Otherwise, unless there is no block ancestor which can contain <p>
+ // element, we shouldn't insert a line break here.
+ for (const Element* editableBlockAncestor = aEditableBlockElement;
+ editableBlockAncestor;
+ editableBlockAncestor = HTMLEditUtils::GetAncestorElement(
+ *editableBlockAncestor,
+ HTMLEditUtils::ClosestEditableBlockElementOrButtonElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ if (HTMLEditUtils::CanElementContainParagraph(
+ *editableBlockAncestor)) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ // Look for the nearest parent block. However, don't return error even if
+ // there is no block parent here because in such case, i.e., editing host
+ // is an inline element, we should insert <br> simply.
+ RefPtr<Element> editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *pointToInsert.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrButtonElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+
+ // If we cannot insert a <p>/<div> element at the selection, we should insert
+ // a <br> element or a linefeed instead.
+ const ParagraphSeparator separator = GetDefaultParagraphSeparator();
+ if (InsertLineBreakInstead(editableBlockElement, pointToInsert, separator,
+ aEditingHost)) {
+ // For backward compatibility, we should not insert a linefeed if
+ // paragraph separator is set to "br" which is Gecko-specific mode.
+ if (separator != ParagraphSeparator::br &&
+ HTMLEditUtils::ShouldInsertLinefeedCharacter(pointToInsert,
+ aEditingHost)) {
+ Result<EditorDOMPoint, nsresult> insertLineFeedResult =
+ HandleInsertLinefeed(pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertLinefeed() failed");
+ return insertLineFeedResult.propagateErr();
+ }
+ nsresult rv = CollapseSelectionTo(insertLineFeedResult.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ HandleInsertBRElement(pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertBRElement() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ nsresult rv =
+ insertBRElementResult.inspect().SuggestCaretPointTo(*this, {});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ // If somebody wants to restrict caret position in a block element below,
+ // we should guarantee it. Otherwise, we can put caret to the candidate
+ // point.
+ auto CollapseSelection =
+ [this](const EditorDOMPoint& aCandidatePointToPutCaret,
+ const Element* aBlockElementShouldHaveCaret,
+ const SuggestCaretOptions& aOptions)
+ MOZ_CAN_RUN_SCRIPT -> nsresult {
+ if (!aCandidatePointToPutCaret.IsSet()) {
+ if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion)) {
+ return NS_OK;
+ }
+ return aOptions.contains(SuggestCaret::AndIgnoreTrivialError)
+ ? NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR
+ : NS_ERROR_FAILURE;
+ }
+ EditorDOMPoint pointToPutCaret(aCandidatePointToPutCaret);
+ if (aBlockElementShouldHaveCaret) {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorDOMPoint>(*aBlockElementShouldHaveCaret,
+ aCandidatePointToPutCaret);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() "
+ "failed, but ignored");
+ } else if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv) && MOZ_LIKELY(rv != NS_ERROR_EDITOR_DESTROYED) &&
+ aOptions.contains(SuggestCaret::AndIgnoreTrivialError)) {
+ rv = NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR;
+ }
+ return rv;
+ };
+
+ RefPtr<Element> blockElementToPutCaret;
+ // If the default paragraph separator is not <br> and selection is not in
+ // a splittable block element, we should wrap selected contents in a new
+ // paragraph, then, split it.
+ if (!HTMLEditUtils::IsSplittableNode(*editableBlockElement) &&
+ separator != ParagraphSeparator::br) {
+ MOZ_ASSERT(separator == ParagraphSeparator::div ||
+ separator == ParagraphSeparator::p);
+ // FIXME: If there is no splittable block element, the other browsers wrap
+ // the right nodes into new paragraph, but keep the left node as-is.
+ // We should follow them to make here simpler and better compatibility.
+ Result<RefPtr<Element>, nsresult> suggestBlockElementToPutCaretOrError =
+ FormatBlockContainerWithTransaction(
+ selectionRanges,
+ MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator)),
+ // For keeping the traditional behavior at insertParagraph command,
+ // let's use the XUL paragraph state command targets even if we're
+ // handling HTML insertParagraph command.
+ FormatBlockMode::XULParagraphStateCommand, aEditingHost);
+ if (MOZ_UNLIKELY(suggestBlockElementToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::FormatBlockContainerWithTransaction() failed");
+ return suggestBlockElementToPutCaretOrError.propagateErr();
+ }
+ if (selectionRanges.HasSavedRanges()) {
+ selectionRanges.RestoreFromSavedRanges();
+ }
+ pointToInsert = selectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(pointToInsert.IsSetAndValid());
+ blockElementToPutCaret = suggestBlockElementToPutCaretOrError.unwrap();
+
+ editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement(
+ *pointToInsert.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrButtonElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (NS_WARN_IF(!editableBlockElement)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ if (NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(*editableBlockElement))) {
+ // Didn't create a new block for some reason, fall back to <br>
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ HandleInsertBRElement(pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertBRElement() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ EditorDOMPoint pointToPutCaret =
+ unwrappedInsertBRElementResult.UnwrapCaretPoint();
+ if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) {
+ NS_WARNING(
+ "HTMLEditor::HandleInsertBRElement() didn't suggest a point to put "
+ "caret");
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsresult rv =
+ CollapseSelection(pointToPutCaret, blockElementToPutCaret, {});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CollapseSelection() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+ // We want to collapse selection in the editable block element.
+ blockElementToPutCaret = editableBlockElement;
+ }
+
+ // If block is empty, populate with br. (For example, imagine a div that
+ // contains the word "text". The user selects "text" and types return.
+ // "Text" is deleted leaving an empty block. We want to put in one br to
+ // make block have a line. Then code further below will put in a second br.)
+ RefPtr<Element> insertedPaddingBRElement;
+ if (HTMLEditUtils::IsEmptyBlockElement(
+ *editableBlockElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes,
+ EditorDOMPoint::AtEndOf(*editableBlockElement));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ insertedPaddingBRElement = unwrappedInsertBRElementResult.UnwrapNewNode();
+
+ pointToInsert = selectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ RefPtr<Element> maybeNonEditableListItem =
+ HTMLEditUtils::GetClosestAncestorListItemElement(*editableBlockElement,
+ &aEditingHost);
+ if (maybeNonEditableListItem &&
+ HTMLEditUtils::IsSplittableNode(*maybeNonEditableListItem)) {
+ Result<InsertParagraphResult, nsresult> insertParagraphInListItemResult =
+ HandleInsertParagraphInListItemElement(*maybeNonEditableListItem,
+ pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertParagraphInListItemResult.isErr())) {
+ if (NS_WARN_IF(insertParagraphInListItemResult.unwrapErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::HandleInsertParagraphInListItemElement() failed, but "
+ "ignored");
+ return EditActionResult::HandledResult();
+ }
+ InsertParagraphResult unwrappedInsertParagraphInListItemResult =
+ insertParagraphInListItemResult.unwrap();
+ MOZ_ASSERT(unwrappedInsertParagraphInListItemResult.Handled());
+ MOZ_ASSERT(unwrappedInsertParagraphInListItemResult.GetNewNode());
+ const RefPtr<Element> listItemOrParagraphElement =
+ unwrappedInsertParagraphInListItemResult.UnwrapNewNode();
+ const EditorDOMPoint pointToPutCaret =
+ unwrappedInsertParagraphInListItemResult.UnwrapCaretPoint();
+ nsresult rv = CollapseSelection(pointToPutCaret, listItemOrParagraphElement,
+ {SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CollapseSelection() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CollapseSelection() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+
+ if (HTMLEditUtils::IsHeader(*editableBlockElement)) {
+ Result<InsertParagraphResult, nsresult>
+ insertParagraphInHeadingElementResult =
+ HandleInsertParagraphInHeadingElement(*editableBlockElement,
+ pointToInsert);
+ if (MOZ_UNLIKELY(insertParagraphInHeadingElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::HandleInsertParagraphInHeadingElement() failed, but "
+ "ignored");
+ return EditActionResult::HandledResult();
+ }
+ InsertParagraphResult unwrappedInsertParagraphInHeadingElementResult =
+ insertParagraphInHeadingElementResult.unwrap();
+ if (unwrappedInsertParagraphInHeadingElementResult.Handled()) {
+ MOZ_ASSERT(unwrappedInsertParagraphInHeadingElementResult.GetNewNode());
+ blockElementToPutCaret =
+ unwrappedInsertParagraphInHeadingElementResult.UnwrapNewNode();
+ }
+ const EditorDOMPoint pointToPutCaret =
+ unwrappedInsertParagraphInHeadingElementResult.UnwrapCaretPoint();
+ nsresult rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CollapseSelection() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CollapseSelection() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+
+ // XXX Ideally, we should take same behavior with both <p> container and
+ // <div> container. However, we are still using <br> as default
+ // paragraph separator (non-standard) and we've split only <p> container
+ // long time. Therefore, some web apps may depend on this behavior like
+ // Gmail. So, let's use traditional odd behavior only when the default
+ // paragraph separator is <br>. Otherwise, take consistent behavior
+ // between <p> container and <div> container.
+ if ((separator == ParagraphSeparator::br &&
+ editableBlockElement->IsHTMLElement(nsGkAtoms::p)) ||
+ (separator != ParagraphSeparator::br &&
+ editableBlockElement->IsAnyOfHTMLElements(nsGkAtoms::p,
+ nsGkAtoms::div))) {
+ // Paragraphs: special rules to look for <br>s
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ HandleInsertParagraphInParagraph(
+ *editableBlockElement,
+ insertedPaddingBRElement ? EditorDOMPoint(insertedPaddingBRElement)
+ : pointToInsert,
+ aEditingHost);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertParagraphInParagraph() failed");
+ return splitNodeResult.propagateErr();
+ }
+ if (splitNodeResult.inspect().Handled()) {
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ const RefPtr<Element> rightParagraphElement =
+ unwrappedSplitNodeResult.DidSplit()
+ ? unwrappedSplitNodeResult.GetNextContentAs<Element>()
+ : blockElementToPutCaret.get();
+ const EditorDOMPoint pointToPutCaret =
+ unwrappedSplitNodeResult.UnwrapCaretPoint();
+ nsresult rv = CollapseSelection(pointToPutCaret, rightParagraphElement,
+ {SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CollapseSelection() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CollapseSelection() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+ MOZ_ASSERT(!splitNodeResult.inspect().HasCaretPointSuggestion());
+
+ // Fall through, if HandleInsertParagraphInParagraph() didn't handle it.
+ MOZ_ASSERT(pointToInsert.IsSetAndValid(),
+ "HTMLEditor::HandleInsertParagraphInParagraph() shouldn't touch "
+ "the DOM tree if it returns not-handled state");
+ }
+
+ // If nobody handles this edit action, let's insert new <br> at the selection.
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ HandleInsertBRElement(pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertBRElement() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ EditorDOMPoint pointToPutCaret =
+ unwrappedInsertBRElementResult.UnwrapCaretPoint();
+ rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret, {});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+Result<CreateElementResult, nsresult> HTMLEditor::HandleInsertBRElement(
+ const EditorDOMPoint& aPointToBreak, const Element& aEditingHost) {
+ MOZ_ASSERT(aPointToBreak.IsSet());
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ const bool editingHostIsEmpty = HTMLEditUtils::IsEmptyNode(
+ aEditingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible});
+ WSRunScanner wsRunScanner(&aEditingHost, aPointToBreak,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ WSScanResult backwardScanResult =
+ wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(aPointToBreak);
+ if (MOZ_UNLIKELY(backwardScanResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary();
+ WSScanResult forwardScanResult =
+ wsRunScanner.ScanNextVisibleNodeOrBlockBoundaryFrom(aPointToBreak);
+ if (MOZ_UNLIKELY(forwardScanResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary();
+
+ // First, insert a <br> element.
+ RefPtr<Element> brElement;
+ if (IsPlaintextMailComposer()) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, aPointToBreak);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult;
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // We'll return with suggesting new caret position and nobody refers
+ // selection after here. So we don't need to update selection here.
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ brElement = unwrappedInsertBRElementResult.UnwrapNewNode();
+ } else {
+ EditorDOMPoint pointToBreak(aPointToBreak);
+ // If the container of the break is a link, we need to split it and
+ // insert new <br> between the split links.
+ RefPtr<Element> linkNode =
+ HTMLEditor::GetLinkElement(pointToBreak.GetContainer());
+ if (linkNode) {
+ Result<SplitNodeResult, nsresult> splitLinkNodeResult =
+ SplitNodeDeepWithTransaction(
+ *linkNode, pointToBreak,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitLinkNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) failed");
+ return splitLinkNodeResult.propagateErr();
+ }
+ // TODO: Some methods called by
+ // WhiteSpaceVisibilityKeeper::InsertBRElement() use
+ // ComputeEditingHost() which depends on selection. Therefore,
+ // we cannot skip updating selection here.
+ nsresult rv = splitLinkNodeResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ pointToBreak =
+ splitLinkNodeResult.inspect().AtSplitPoint<EditorDOMPoint>();
+ }
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ WhiteSpaceVisibilityKeeper::InsertBRElement(*this, pointToBreak,
+ aEditingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed");
+ return insertBRElementResult;
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // We'll return with suggesting new caret position and nobody refers
+ // selection after here. So we don't need to update selection here.
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ brElement = unwrappedInsertBRElementResult.UnwrapNewNode();
+ MOZ_ASSERT(brElement);
+ }
+
+ if (MOZ_UNLIKELY(!brElement->GetParentNode())) {
+ NS_WARNING("Inserted <br> element was removed by the web app");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ auto afterBRElement = EditorDOMPoint::After(brElement);
+
+ auto InsertAdditionalInvisibleLineBreak =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<CreateElementResult, nsresult> {
+ // Empty last line is invisible if it's immediately before either parent or
+ // another block's boundary so that we need to put invisible <br> element
+ // here for making it visible.
+ Result<CreateElementResult, nsresult> invisibleAdditionalBRElementResult =
+ WhiteSpaceVisibilityKeeper::InsertBRElement(*this, afterBRElement,
+ aEditingHost);
+ if (MOZ_UNLIKELY(invisibleAdditionalBRElementResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed");
+ return invisibleAdditionalBRElementResult;
+ }
+ // afterBRElement points after the first <br> with referring an old child.
+ // Therefore, we need to update it with new child which is the new invisible
+ // <br>.
+ afterBRElement.Set(
+ invisibleAdditionalBRElementResult.inspect().GetNewNode());
+ return invisibleAdditionalBRElementResult;
+ };
+
+ if (brElementIsAfterBlock && brElementIsBeforeBlock) {
+ // We just placed a <br> between block boundaries. This is the one case
+ // where we want the selection to be before the br we just placed, as the
+ // br will be on a new line, rather than at end of prior line.
+ // XXX brElementIsAfterBlock and brElementIsBeforeBlock were set before
+ // modifying the DOM tree. So, now, the <br> element may not be
+ // between blocks.
+ EditorDOMPoint pointToPutCaret;
+ if (editingHostIsEmpty) {
+ Result<CreateElementResult, nsresult> invisibleAdditionalBRElementResult =
+ InsertAdditionalInvisibleLineBreak();
+ if (invisibleAdditionalBRElementResult.isErr()) {
+ return invisibleAdditionalBRElementResult;
+ }
+ invisibleAdditionalBRElementResult.unwrap().IgnoreCaretPointSuggestion();
+ pointToPutCaret = std::move(afterBRElement);
+ } else {
+ pointToPutCaret =
+ EditorDOMPoint(brElement, InterlinePosition::StartOfNextLine);
+ }
+ return CreateElementResult(std::move(brElement),
+ std::move(pointToPutCaret));
+ }
+
+ WSScanResult forwardScanFromAfterBRElementResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost, afterBRElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (forwardScanFromAfterBRElementResult.ReachedBRElement()) {
+ // The next thing after the break we inserted is another break. Move the
+ // second break to be the first break's sibling. This will prevent them
+ // from being in different inline nodes, which would break
+ // SetInterlinePosition(). It will also assure that if the user clicks
+ // away and then clicks back on their new blank line, they will still get
+ // the style from the line above.
+ if (brElement->GetNextSibling() !=
+ forwardScanFromAfterBRElementResult.BRElementPtr()) {
+ MOZ_ASSERT(forwardScanFromAfterBRElementResult.BRElementPtr());
+ Result<MoveNodeResult, nsresult> moveBRElementResult =
+ MoveNodeWithTransaction(
+ MOZ_KnownLive(
+ *forwardScanFromAfterBRElementResult.BRElementPtr()),
+ afterBRElement);
+ if (MOZ_UNLIKELY(moveBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveBRElementResult.propagateErr();
+ }
+ nsresult rv = moveBRElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ }
+ } else if (forwardScanFromAfterBRElementResult.ReachedBlockBoundary() &&
+ !brElementIsAfterBlock) {
+ Result<CreateElementResult, nsresult> invisibleAdditionalBRElementResult =
+ InsertAdditionalInvisibleLineBreak();
+ if (invisibleAdditionalBRElementResult.isErr()) {
+ return invisibleAdditionalBRElementResult;
+ }
+ invisibleAdditionalBRElementResult.unwrap().IgnoreCaretPointSuggestion();
+ }
+
+ // We want the caret to stick to whatever is past the break. This is because
+ // the break is on the same line we were on, but the next content will be on
+ // the following line.
+
+ // An exception to this is if the break has a next sibling that is a block
+ // node. Then we stick to the left to avoid an uber caret.
+ nsIContent* nextSiblingOfBRElement = brElement->GetNextSibling();
+ afterBRElement.SetInterlinePosition(
+ nextSiblingOfBRElement && HTMLEditUtils::IsBlockElement(
+ *nextSiblingOfBRElement,
+ BlockInlineCheck::UseComputedDisplayStyle)
+ ? InterlinePosition::EndOfLine
+ : InterlinePosition::StartOfNextLine);
+ return CreateElementResult(std::move(brElement), afterBRElement);
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::HandleInsertLinefeed(
+ const EditorDOMPoint& aPointToBreak, const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aPointToBreak.IsSet())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ const RefPtr<Document> document = GetDocument();
+ MOZ_DIAGNOSTIC_ASSERT(document);
+ if (NS_WARN_IF(!document)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // TODO: The following code is duplicated from `HandleInsertText`. They
+ // should be merged when we fix bug 92921.
+
+ Result<EditorDOMPoint, nsresult> setStyleResult =
+ CreateStyleForInsertText(aPointToBreak, aEditingHost);
+ if (MOZ_UNLIKELY(setStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed");
+ return setStyleResult.propagateErr();
+ }
+
+ EditorDOMPoint pointToInsert = setStyleResult.inspect().IsSet()
+ ? setStyleResult.inspect()
+ : aPointToBreak;
+ if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) ||
+ NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(pointToInsert.IsSetAndValid());
+
+ // The node may not be able to have a text node so that we need to check it
+ // here.
+ if (!pointToInsert.IsInTextNode() &&
+ !HTMLEditUtils::CanNodeContain(*pointToInsert.ContainerAs<nsIContent>(),
+ *nsGkAtoms::textTagName)) {
+ NS_WARNING(
+ "HTMLEditor::HandleInsertLinefeed() couldn't insert a linefeed because "
+ "the insertion position couldn't have text nodes");
+ return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
+ }
+
+ AutoRestore<bool> disableListener(
+ EditSubActionDataRef().mAdjustChangedRangeFromListener);
+ EditSubActionDataRef().mAdjustChangedRangeFromListener = false;
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ EditorDOMPoint pointToPutCaret;
+ {
+ AutoTrackDOMPoint trackingInsertingPosition(RangeUpdaterRef(),
+ &pointToInsert);
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, u"\n"_ns, pointToInsert);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ // Ignore the caret suggestion because of `dontChangeMySelection` above.
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ pointToPutCaret = insertTextResult.inspect().Handled()
+ ? insertTextResult.unwrap()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>()
+ : pointToInsert;
+ }
+
+ // Insert a padding <br> element at the end of the block element if there is
+ // no content between the inserted linefeed and the following block boundary
+ // to make sure that the last line is visible.
+ // XXX Blink/WebKit inserts another linefeed character in this case. However,
+ // for doing it, we need more work, e.g., updating serializer, deleting
+ // unnecessary padding <br> element at modifying the last line.
+ if (pointToPutCaret.IsInContentNode() && pointToPutCaret.IsEndOfContainer()) {
+ WSRunScanner wsScannerAtCaret(&aEditingHost, pointToPutCaret,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (wsScannerAtCaret.StartsFromPreformattedLineBreak() &&
+ wsScannerAtCaret.EndsByBlockBoundary() &&
+ HTMLEditUtils::CanNodeContain(*wsScannerAtCaret.GetEndReasonContent(),
+ *nsGkAtoms::br)) {
+ AutoTrackDOMPoint trackingInsertedPosition(RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToPutCaret);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We're tracking next caret position with newCaretPosition. Therefore,
+ // we don't need to update selection here.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ }
+ }
+
+ // manually update the doc changed range so that
+ // OnEndHandlingTopLevelEditSubActionInternal will clean up the correct
+ // portion of the document.
+ MOZ_ASSERT(pointToPutCaret.IsSet());
+ if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
+ // XXX Here is odd. We did mChangedRange->SetStartAndEnd(pointToInsert,
+ // pointToPutCaret), but it always fails because of the latter is unset.
+ // Therefore, always returning NS_ERROR_FAILURE from here is the
+ // traditional behavior...
+ // TODO: Stop updating the interline position of Selection with fixing here
+ // and returning expected point.
+ DebugOnly<nsresult> rvIgnored =
+ SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "EndOfLine) failed, but ignored");
+ if (NS_FAILED(TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(
+ pointToInsert))) {
+ NS_WARNING("nsRange::CollapseTo() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ NS_WARNING(
+ "We always return NS_ERROR_FAILURE here because of a failure of "
+ "updating mChangedRange");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (NS_FAILED(TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
+ pointToInsert.ToRawRangeBoundary(),
+ pointToPutCaret.ToRawRangeBoundary()))) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ pointToPutCaret.SetInterlinePosition(InterlinePosition::EndOfLine);
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::HandleInsertParagraphInMailCiteElement(
+ Element& aMailCiteElement, const EditorDOMPoint& aPointToSplit,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToSplit.IsSet());
+ NS_ASSERTION(!HTMLEditUtils::IsEmptyNode(
+ aMailCiteElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible}),
+ "The mail-cite element will be deleted, does it expected result "
+ "for you?");
+
+ auto splitCiteElementResult =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
+ EditorDOMPoint pointToSplit(aPointToSplit);
+
+ // If our selection is just before a break, nudge it to be just after
+ // it. This does two things for us. It saves us the trouble of having
+ // to add a break here ourselves to preserve the "blockness" of the
+ // inline span mailquote (in the inline case), and : it means the break
+ // won't end up making an empty line that happens to be inside a
+ // mailquote (in either inline or block case). The latter can confuse a
+ // user if they click there and start typing, because being in the
+ // mailquote may affect wrapping behavior, or font color, etc.
+ WSScanResult forwardScanFromPointToSplitResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost, pointToSplit, BlockInlineCheck::UseHTMLDefaultStyle);
+ if (forwardScanFromPointToSplitResult.Failed()) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // If selection start point is before a break and it's inside the
+ // mailquote, let's split it after the visible node.
+ if (forwardScanFromPointToSplitResult.ReachedBRElement() &&
+ forwardScanFromPointToSplitResult.BRElementPtr() != &aMailCiteElement &&
+ aMailCiteElement.Contains(
+ forwardScanFromPointToSplitResult.BRElementPtr())) {
+ pointToSplit =
+ forwardScanFromPointToSplitResult.PointAfterContent<EditorDOMPoint>();
+ }
+
+ if (NS_WARN_IF(!pointToSplit.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ Result<SplitNodeResult, nsresult> splitResult =
+ SplitNodeDeepWithTransaction(aMailCiteElement, pointToSplit,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(aMailCiteElement, "
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed");
+ return splitResult;
+ }
+ nsresult rv = splitResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ return splitResult;
+ }();
+ if (MOZ_UNLIKELY(splitCiteElementResult.isErr())) {
+ NS_WARNING("Failed to split a mail-cite element");
+ return splitCiteElementResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitCiteElementResult =
+ splitCiteElementResult.unwrap();
+ // When adding caret suggestion to SplitNodeResult, here didn't change
+ // selection so that just ignore it.
+ unwrappedSplitCiteElementResult.IgnoreCaretPointSuggestion();
+
+ // Add an invisible <br> to the end of left cite node if it was a <span> of
+ // style="display: block". This is important, since when serializing the cite
+ // to plain text, the span which caused the visual break is discarded. So the
+ // added <br> will guarantee that the serializer will insert a break where the
+ // user saw one.
+ // FYI: unwrappedSplitCiteElementResult grabs the previous node and the next
+ // node with nsCOMPtr or EditorDOMPoint. So, it's safe to access
+ // leftCiteElement and rightCiteElement even after changing the DOM tree
+ // and/or selection even though it's raw pointer.
+ auto* const leftCiteElement =
+ unwrappedSplitCiteElementResult.GetPreviousContentAs<Element>();
+ auto* const rightCiteElement =
+ unwrappedSplitCiteElementResult.GetNextContentAs<Element>();
+ if (leftCiteElement && leftCiteElement->IsHTMLElement(nsGkAtoms::span) &&
+ // XXX Oh, this depends on layout information of new element, and it's
+ // created by the hacky flush in DoSplitNode(). So we need to
+ // redesign around this for bug 1710784.
+ leftCiteElement->GetPrimaryFrame() &&
+ leftCiteElement->GetPrimaryFrame()->IsBlockFrameOrSubclass()) {
+ nsIContent* lastChild = leftCiteElement->GetLastChild();
+ if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) {
+ Result<CreateElementResult, nsresult> insertInvisibleBRElementResult =
+ InsertBRElement(WithTransaction::Yes,
+ EditorDOMPoint::AtEndOf(*leftCiteElement));
+ if (MOZ_UNLIKELY(insertInvisibleBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertInvisibleBRElementResult.propagateErr();
+ }
+ // We don't need to update selection here because we'll do another
+ // InsertBRElement call soon.
+ insertInvisibleBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(insertInvisibleBRElementResult.inspect().GetNewNode());
+ }
+ }
+
+ // In most cases, <br> should be inserted after current cite. However, if
+ // left cite hasn't been created because the split point was start of the
+ // cite node, <br> should be inserted before the current cite.
+ Result<CreateElementResult, nsresult> insertBRElementResult = InsertBRElement(
+ WithTransaction::Yes,
+ unwrappedSplitCiteElementResult.AtSplitPoint<EditorDOMPoint>());
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return Err(insertBRElementResult.unwrapErr());
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // We'll return with suggesting caret position. Therefore, we don't need
+ // to update selection here.
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+
+ // if aMailCiteElement wasn't a block, we might also want another break before
+ // it. We need to examine the content both before the br we just added and
+ // also just after it. If we don't have another br or block boundary
+ // adjacent, then we will need a 2nd br added to achieve blank line that user
+ // expects.
+ if (HTMLEditUtils::IsInlineContent(
+ aMailCiteElement, BlockInlineCheck::UseComputedDisplayStyle)) {
+ nsresult rvOfInsertingBRElement = [&]() MOZ_CAN_RUN_SCRIPT {
+ EditorDOMPoint pointToCreateNewBRElement(
+ unwrappedInsertBRElementResult.GetNewNode());
+
+ // XXX Cannot we replace this complicated check with just a call of
+ // HTMLEditUtils::IsVisibleBRElement with
+ // resultOfInsertingBRElement.inspect()?
+ WSScanResult backwardScanFromPointToCreateNewBRElementResult =
+ WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
+ &aEditingHost, pointToCreateNewBRElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(
+ backwardScanFromPointToCreateNewBRElementResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() "
+ "failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (!backwardScanFromPointToCreateNewBRElementResult
+ .InVisibleOrCollapsibleCharacters() &&
+ !backwardScanFromPointToCreateNewBRElementResult
+ .ReachedSpecialContent()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ WSScanResult forwardScanFromPointAfterNewBRElementResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost,
+ EditorRawDOMPoint::After(pointToCreateNewBRElement),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(forwardScanFromPointAfterNewBRElementResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (!forwardScanFromPointAfterNewBRElementResult
+ .InVisibleOrCollapsibleCharacters() &&
+ !forwardScanFromPointAfterNewBRElementResult
+ .ReachedSpecialContent() &&
+ // In case we're at the very end.
+ !forwardScanFromPointAfterNewBRElementResult
+ .ReachedCurrentBlockBoundary()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToCreateNewBRElement);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ return NS_OK;
+ }();
+
+ if (NS_FAILED(rvOfInsertingBRElement)) {
+ NS_WARNING(
+ "Failed to insert additional <br> element before the inline right "
+ "mail-cite element");
+ return Err(rvOfInsertingBRElement);
+ }
+ }
+
+ if (leftCiteElement &&
+ HTMLEditUtils::IsEmptyNode(
+ *leftCiteElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ // MOZ_KnownLive(leftCiteElement) because it's grabbed by
+ // unwrappedSplitCiteElementResult.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*leftCiteElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ if (rightCiteElement &&
+ HTMLEditUtils::IsEmptyNode(
+ *rightCiteElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ // MOZ_KnownLive(rightCiteElement) because it's grabbed by
+ // unwrappedSplitCiteElementResult.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*rightCiteElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ if (MOZ_UNLIKELY(!unwrappedInsertBRElementResult.GetNewNode()->GetParent())) {
+ NS_WARNING("Inserted <br> shouldn't become an orphan node");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ return EditorDOMPoint(unwrappedInsertBRElementResult.GetNewNode());
+}
+
+HTMLEditor::CharPointData
+HTMLEditor::GetPreviousCharPointDataForNormalizingWhiteSpaces(
+ const EditorDOMPointInText& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (!aPoint.IsStartOfContainer()) {
+ return CharPointData::InSameTextNode(
+ HTMLEditor::GetPreviousCharPointType(aPoint));
+ }
+ const auto previousCharPoint =
+ WSRunScanner::GetPreviousEditableCharPoint<EditorRawDOMPointInText>(
+ ComputeEditingHost(), aPoint,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (!previousCharPoint.IsSet()) {
+ return CharPointData::InDifferentTextNode(CharPointType::TextEnd);
+ }
+ return CharPointData::InDifferentTextNode(
+ HTMLEditor::GetCharPointType(previousCharPoint));
+}
+
+HTMLEditor::CharPointData
+HTMLEditor::GetInclusiveNextCharPointDataForNormalizingWhiteSpaces(
+ const EditorDOMPointInText& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (!aPoint.IsEndOfContainer()) {
+ return CharPointData::InSameTextNode(HTMLEditor::GetCharPointType(aPoint));
+ }
+ const auto nextCharPoint =
+ WSRunScanner::GetInclusiveNextEditableCharPoint<EditorRawDOMPointInText>(
+ ComputeEditingHost(), aPoint,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (!nextCharPoint.IsSet()) {
+ return CharPointData::InDifferentTextNode(CharPointType::TextEnd);
+ }
+ return CharPointData::InDifferentTextNode(
+ HTMLEditor::GetCharPointType(nextCharPoint));
+}
+
+// static
+void HTMLEditor::GenerateWhiteSpaceSequence(
+ nsAString& aResult, uint32_t aLength,
+ const CharPointData& aPreviousCharPointData,
+ const CharPointData& aNextCharPointData) {
+ MOZ_ASSERT(aResult.IsEmpty());
+ MOZ_ASSERT(aLength);
+ // For now, this method does not assume that result will be append to
+ // white-space sequence in the text node.
+ MOZ_ASSERT(aPreviousCharPointData.AcrossTextNodeBoundary() ||
+ !aPreviousCharPointData.IsCollapsibleWhiteSpace());
+ // For now, this method does not assume that the result will be inserted
+ // into white-space sequence nor start of white-space sequence.
+ MOZ_ASSERT(aNextCharPointData.AcrossTextNodeBoundary() ||
+ !aNextCharPointData.IsCollapsibleWhiteSpace());
+
+ if (aLength == 1) {
+ // Even if previous/next char is in different text node, we should put
+ // an ASCII white-space between visible characters.
+ // XXX This means that this does not allow to put an NBSP in HTML editor
+ // without preformatted style. However, Chrome has same issue too.
+ if (aPreviousCharPointData.Type() == CharPointType::VisibleChar &&
+ aNextCharPointData.Type() == CharPointType::VisibleChar) {
+ aResult.Assign(HTMLEditUtils::kSpace);
+ return;
+ }
+ // If it's start or end of text, put an NBSP.
+ if (aPreviousCharPointData.Type() == CharPointType::TextEnd ||
+ aNextCharPointData.Type() == CharPointType::TextEnd) {
+ aResult.Assign(HTMLEditUtils::kNBSP);
+ return;
+ }
+ // If the character is next to a preformatted linefeed, we need to put
+ // an NBSP for avoiding collapsed into the linefeed.
+ if (aPreviousCharPointData.Type() == CharPointType::PreformattedLineBreak ||
+ aNextCharPointData.Type() == CharPointType::PreformattedLineBreak) {
+ aResult.Assign(HTMLEditUtils::kNBSP);
+ return;
+ }
+ // Now, the white-space will be inserted to a white-space sequence, but not
+ // end of text. We can put an ASCII white-space only when both sides are
+ // not ASCII white-spaces.
+ aResult.Assign(
+ aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace ||
+ aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace
+ ? HTMLEditUtils::kNBSP
+ : HTMLEditUtils::kSpace);
+ return;
+ }
+
+ // Generate pairs of NBSP and ASCII white-space.
+ aResult.SetLength(aLength);
+ bool appendNBSP = true; // Basically, starts with an NBSP.
+ char16_t* lastChar = aResult.EndWriting() - 1;
+ for (char16_t* iter = aResult.BeginWriting(); iter != lastChar; iter++) {
+ *iter = appendNBSP ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace;
+ appendNBSP = !appendNBSP;
+ }
+
+ // If the final one is expected to an NBSP, we can put an NBSP simply.
+ if (appendNBSP) {
+ *lastChar = HTMLEditUtils::kNBSP;
+ return;
+ }
+
+ // If next char point is end of text node, an ASCII white-space or
+ // preformatted linefeed, we need to put an NBSP.
+ *lastChar =
+ aNextCharPointData.AcrossTextNodeBoundary() ||
+ aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace ||
+ aNextCharPointData.Type() == CharPointType::PreformattedLineBreak
+ ? HTMLEditUtils::kNBSP
+ : HTMLEditUtils::kSpace;
+}
+
+void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces(
+ EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
+ nsAString& aNormalizedWhiteSpacesInStartNode,
+ nsAString& aNormalizedWhiteSpacesInEndNode) const {
+ MOZ_ASSERT(aStartToDelete.IsSetAndValid());
+ MOZ_ASSERT(aEndToDelete.IsSetAndValid());
+ MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete));
+ MOZ_ASSERT(aNormalizedWhiteSpacesInStartNode.IsEmpty());
+ MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.IsEmpty());
+
+ // First, check whether there is surrounding white-spaces or not, and if there
+ // are, check whether they are collapsible or not. Note that we shouldn't
+ // touch white-spaces in different text nodes for performance, but we need
+ // adjacent text node's first or last character information in some cases.
+ Element* editingHost = ComputeEditingHost();
+ const EditorDOMPointInText precedingCharPoint =
+ WSRunScanner::GetPreviousEditableCharPoint(
+ editingHost, aStartToDelete,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ const EditorDOMPointInText followingCharPoint =
+ WSRunScanner::GetInclusiveNextEditableCharPoint(
+ editingHost, aEndToDelete, BlockInlineCheck::UseComputedDisplayStyle);
+ // Blink-compat: Normalize white-spaces in first node only when not removing
+ // its last character or no text nodes follow the first node.
+ // If removing last character of first node and there are
+ // following text nodes, white-spaces in following text node are
+ // normalized instead.
+ const bool removingLastCharOfStartNode =
+ aStartToDelete.ContainerAs<Text>() != aEndToDelete.ContainerAs<Text>() ||
+ (aEndToDelete.IsEndOfContainer() && followingCharPoint.IsSet());
+ const bool maybeNormalizePrecedingWhiteSpaces =
+ !removingLastCharOfStartNode && precedingCharPoint.IsSet() &&
+ !precedingCharPoint.IsEndOfContainer() &&
+ precedingCharPoint.ContainerAs<Text>() ==
+ aStartToDelete.ContainerAs<Text>() &&
+ precedingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP();
+ const bool maybeNormalizeFollowingWhiteSpaces =
+ followingCharPoint.IsSet() && !followingCharPoint.IsEndOfContainer() &&
+ (followingCharPoint.ContainerAs<Text>() ==
+ aEndToDelete.ContainerAs<Text>() ||
+ removingLastCharOfStartNode) &&
+ followingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP();
+
+ if (!maybeNormalizePrecedingWhiteSpaces &&
+ !maybeNormalizeFollowingWhiteSpaces) {
+ return; // There are no white-spaces.
+ }
+
+ // Next, consider the range to normalize.
+ EditorDOMPointInText startToNormalize, endToNormalize;
+ if (maybeNormalizePrecedingWhiteSpaces) {
+ Maybe<uint32_t> previousCharOffsetOfWhiteSpaces =
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
+ precedingCharPoint, {WalkTextOption::TreatNBSPsCollapsible});
+ startToNormalize.Set(precedingCharPoint.ContainerAs<Text>(),
+ previousCharOffsetOfWhiteSpaces.isSome()
+ ? previousCharOffsetOfWhiteSpaces.value() + 1
+ : 0);
+ MOZ_ASSERT(!startToNormalize.IsEndOfContainer());
+ }
+ if (maybeNormalizeFollowingWhiteSpaces) {
+ Maybe<uint32_t> nextCharOffsetOfWhiteSpaces =
+ HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
+ followingCharPoint, {WalkTextOption::TreatNBSPsCollapsible});
+ if (nextCharOffsetOfWhiteSpaces.isSome()) {
+ endToNormalize.Set(followingCharPoint.ContainerAs<Text>(),
+ nextCharOffsetOfWhiteSpaces.value());
+ } else {
+ endToNormalize.SetToEndOf(followingCharPoint.ContainerAs<Text>());
+ }
+ MOZ_ASSERT(!endToNormalize.IsStartOfContainer());
+ }
+
+ // Next, retrieve surrounding information of white-space sequence.
+ // If we're removing first text node's last character, we need to
+ // normalize white-spaces starts from another text node. In this case,
+ // we need to lie for avoiding assertion in GenerateWhiteSpaceSequence().
+ CharPointData previousCharPointData =
+ removingLastCharOfStartNode
+ ? CharPointData::InDifferentTextNode(CharPointType::TextEnd)
+ : GetPreviousCharPointDataForNormalizingWhiteSpaces(
+ startToNormalize.IsSet() ? startToNormalize : aStartToDelete);
+ CharPointData nextCharPointData =
+ GetInclusiveNextCharPointDataForNormalizingWhiteSpaces(
+ endToNormalize.IsSet() ? endToNormalize : aEndToDelete);
+
+ // Next, compute number of white-spaces in start/end node.
+ uint32_t lengthInStartNode = 0, lengthInEndNode = 0;
+ if (startToNormalize.IsSet()) {
+ MOZ_ASSERT(startToNormalize.ContainerAs<Text>() ==
+ aStartToDelete.ContainerAs<Text>());
+ lengthInStartNode = aStartToDelete.Offset() - startToNormalize.Offset();
+ MOZ_ASSERT(lengthInStartNode);
+ }
+ if (endToNormalize.IsSet()) {
+ lengthInEndNode =
+ endToNormalize.ContainerAs<Text>() == aEndToDelete.ContainerAs<Text>()
+ ? endToNormalize.Offset() - aEndToDelete.Offset()
+ : endToNormalize.Offset();
+ MOZ_ASSERT(lengthInEndNode);
+ // If we normalize white-spaces in a text node, we can replace all of them
+ // with one ReplaceTextTransaction.
+ if (endToNormalize.ContainerAs<Text>() ==
+ aStartToDelete.ContainerAs<Text>()) {
+ lengthInStartNode += lengthInEndNode;
+ lengthInEndNode = 0;
+ }
+ }
+
+ MOZ_ASSERT(lengthInStartNode + lengthInEndNode);
+
+ // Next, generate normalized white-spaces.
+ if (!lengthInEndNode) {
+ HTMLEditor::GenerateWhiteSpaceSequence(
+ aNormalizedWhiteSpacesInStartNode, lengthInStartNode,
+ previousCharPointData, nextCharPointData);
+ } else if (!lengthInStartNode) {
+ HTMLEditor::GenerateWhiteSpaceSequence(
+ aNormalizedWhiteSpacesInEndNode, lengthInEndNode, previousCharPointData,
+ nextCharPointData);
+ } else {
+ // For making `GenerateWhiteSpaceSequence()` simpler, we should create
+ // whole white-space sequence first, then, copy to the out params.
+ nsAutoString whiteSpaces;
+ HTMLEditor::GenerateWhiteSpaceSequence(
+ whiteSpaces, lengthInStartNode + lengthInEndNode, previousCharPointData,
+ nextCharPointData);
+ aNormalizedWhiteSpacesInStartNode =
+ Substring(whiteSpaces, 0, lengthInStartNode);
+ aNormalizedWhiteSpacesInEndNode = Substring(whiteSpaces, lengthInStartNode);
+ MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.Length() == lengthInEndNode);
+ }
+
+ // TODO: Shrink the replacing range and string as far as possible because
+ // this may run a lot, i.e., HTMLEditor creates ReplaceTextTransaction
+ // a lot for normalizing white-spaces. Then, each transaction shouldn't
+ // have all white-spaces every time because once it's normalized, we
+ // don't need to normalize all of the sequence again, but currently
+ // we do.
+
+ // Finally, extend the range.
+ if (startToNormalize.IsSet()) {
+ aStartToDelete = startToNormalize;
+ }
+ if (endToNormalize.IsSet()) {
+ aEndToDelete = endToNormalize;
+ }
+}
+
+Result<CaretPoint, nsresult>
+HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces(
+ const EditorDOMPointInText& aStartToDelete,
+ const EditorDOMPointInText& aEndToDelete,
+ TreatEmptyTextNodes aTreatEmptyTextNodes,
+ DeleteDirection aDeleteDirection) {
+ MOZ_ASSERT(aStartToDelete.IsSetAndValid());
+ MOZ_ASSERT(aEndToDelete.IsSetAndValid());
+ MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete));
+
+ // Use nsString for these replacing string because we should avoid to copy
+ // the buffer from auto storange to ReplaceTextTransaction.
+ nsString normalizedWhiteSpacesInFirstNode, normalizedWhiteSpacesInLastNode;
+
+ // First, check whether we need to normalize white-spaces after deleting
+ // the given range.
+ EditorDOMPointInText startToDelete(aStartToDelete);
+ EditorDOMPointInText endToDelete(aEndToDelete);
+ ExtendRangeToDeleteWithNormalizingWhiteSpaces(
+ startToDelete, endToDelete, normalizedWhiteSpacesInFirstNode,
+ normalizedWhiteSpacesInLastNode);
+
+ // If extended range is still collapsed, i.e., the caller just wants to
+ // normalize white-space sequence, but there is no white-spaces which need to
+ // be replaced, we need to do nothing here.
+ if (startToDelete == endToDelete) {
+ return CaretPoint(aStartToDelete.To<EditorDOMPoint>());
+ }
+
+ // Note that the container text node of startToDelete may be removed from
+ // the tree if it becomes empty. Therefore, we need to track the point.
+ EditorDOMPoint newCaretPosition;
+ if (aStartToDelete.ContainerAs<Text>() == aEndToDelete.ContainerAs<Text>()) {
+ newCaretPosition = aEndToDelete.To<EditorDOMPoint>();
+ } else if (aDeleteDirection == DeleteDirection::Forward) {
+ newCaretPosition.SetToEndOf(aStartToDelete.ContainerAs<Text>());
+ } else {
+ newCaretPosition.Set(aEndToDelete.ContainerAs<Text>(), 0u);
+ }
+
+ // Then, modify the text nodes in the range.
+ while (true) {
+ AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(),
+ &newCaretPosition);
+ // Use ReplaceTextTransaction if we need to normalize white-spaces in
+ // the first text node.
+ if (!normalizedWhiteSpacesInFirstNode.IsEmpty()) {
+ EditorDOMPoint trackingEndToDelete(endToDelete.ContainerAs<Text>(),
+ endToDelete.Offset());
+ {
+ AutoTrackDOMPoint trackEndToDelete(RangeUpdaterRef(),
+ &trackingEndToDelete);
+ uint32_t lengthToReplaceInFirstTextNode =
+ startToDelete.ContainerAs<Text>() ==
+ trackingEndToDelete.ContainerAs<Text>()
+ ? trackingEndToDelete.Offset() - startToDelete.Offset()
+ : startToDelete.ContainerAs<Text>()->TextLength() -
+ startToDelete.Offset();
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ ReplaceTextWithTransaction(
+ MOZ_KnownLive(*startToDelete.ContainerAs<Text>()),
+ startToDelete.Offset(), lengthToReplaceInFirstTextNode,
+ normalizedWhiteSpacesInFirstNode);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // We'll return computed caret point, newCaretPosition, below.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ if (startToDelete.ContainerAs<Text>() ==
+ trackingEndToDelete.ContainerAs<Text>()) {
+ MOZ_ASSERT(normalizedWhiteSpacesInLastNode.IsEmpty());
+ break; // There is no more text which we need to delete.
+ }
+ }
+ if (MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) &&
+ (NS_WARN_IF(!trackingEndToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!trackingEndToDelete.IsInTextNode()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(trackingEndToDelete.IsInTextNode());
+ endToDelete.Set(trackingEndToDelete.ContainerAs<Text>(),
+ trackingEndToDelete.Offset());
+ // If the remaining range was modified by mutation event listener,
+ // we should stop handling the deletion.
+ startToDelete =
+ EditorDOMPointInText::AtEndOf(*startToDelete.ContainerAs<Text>());
+ if (MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) &&
+ NS_WARN_IF(!startToDelete.IsBefore(endToDelete))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+ // Delete ASCII whiteSpaces in the range simpley if there are some text
+ // nodes which we don't need to replace their text.
+ if (normalizedWhiteSpacesInLastNode.IsEmpty() ||
+ startToDelete.ContainerAs<Text>() != endToDelete.ContainerAs<Text>()) {
+ // If we need to replace text in the last text node, we should
+ // delete text before its previous text node.
+ EditorDOMPointInText endToDeleteExceptReplaceRange =
+ normalizedWhiteSpacesInLastNode.IsEmpty()
+ ? endToDelete
+ : EditorDOMPointInText(endToDelete.ContainerAs<Text>(), 0);
+ if (startToDelete != endToDeleteExceptReplaceRange) {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ DeleteTextAndTextNodesWithTransaction(startToDelete,
+ endToDeleteExceptReplaceRange,
+ aTreatEmptyTextNodes);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ if (normalizedWhiteSpacesInLastNode.IsEmpty()) {
+ break; // There is no more text which we need to delete.
+ }
+ if (MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
+ NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED) &&
+ (NS_WARN_IF(!endToDeleteExceptReplaceRange.IsSetAndValid()) ||
+ NS_WARN_IF(!endToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(endToDelete.IsStartOfContainer()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // Then, replace the text in the last text node.
+ startToDelete = endToDeleteExceptReplaceRange;
+ }
+ }
+
+ // Replace ASCII whiteSpaces in the range and following character in the
+ // last text node.
+ MOZ_ASSERT(!normalizedWhiteSpacesInLastNode.IsEmpty());
+ MOZ_ASSERT(startToDelete.ContainerAs<Text>() ==
+ endToDelete.ContainerAs<Text>());
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ ReplaceTextWithTransaction(
+ MOZ_KnownLive(*startToDelete.ContainerAs<Text>()),
+ startToDelete.Offset(),
+ endToDelete.Offset() - startToDelete.Offset(),
+ normalizedWhiteSpacesInLastNode);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // We'll return computed caret point, newCaretPosition, below.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ break;
+ }
+
+ if (NS_WARN_IF(!newCaretPosition.IsSetAndValid()) ||
+ NS_WARN_IF(!newCaretPosition.GetContainer()->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // Look for leaf node to put caret if we remove some empty inline ancestors
+ // at new caret position.
+ if (!newCaretPosition.IsInTextNode()) {
+ if (const Element* editableBlockElementOrInlineEditingHost =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *newCaretPosition.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ Element* editingHost = ComputeEditingHost();
+ // Try to put caret next to immediately after previous editable leaf.
+ nsIContent* previousContent =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ newCaretPosition, *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost);
+ if (previousContent &&
+ !HTMLEditUtils::IsBlockElement(
+ *previousContent, BlockInlineCheck::UseComputedDisplayStyle)) {
+ newCaretPosition =
+ previousContent->IsText() ||
+ HTMLEditUtils::IsContainerNode(*previousContent)
+ ? EditorDOMPoint::AtEndOf(*previousContent)
+ : EditorDOMPoint::After(*previousContent);
+ }
+ // But if the point is very first of a block element or immediately after
+ // a child block, look for next editable leaf instead.
+ else if (nsIContent* nextContent =
+ HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ newCaretPosition,
+ *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle,
+ editingHost)) {
+ newCaretPosition = nextContent->IsText() ||
+ HTMLEditUtils::IsContainerNode(*nextContent)
+ ? EditorDOMPoint(nextContent, 0)
+ : EditorDOMPoint(nextContent);
+ }
+ }
+ }
+
+ // For compatibility with Blink, we should move caret to end of previous
+ // text node if it's direct previous sibling of the first text node in the
+ // range.
+ if (newCaretPosition.IsStartOfContainer() &&
+ newCaretPosition.IsInTextNode() &&
+ newCaretPosition.GetContainer()->GetPreviousSibling() &&
+ newCaretPosition.GetContainer()->GetPreviousSibling()->IsText()) {
+ newCaretPosition.SetToEndOf(
+ newCaretPosition.GetContainer()->GetPreviousSibling()->AsText());
+ }
+
+ {
+ AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(),
+ &newCaretPosition);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ newCaretPosition);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed");
+ return caretPointOrError;
+ }
+ }
+ if (!newCaretPosition.IsSetAndValid()) {
+ NS_WARNING("Inserting <br> element caused unexpected DOM tree");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ return CaretPoint(std::move(newCaretPosition));
+}
+
+Result<CaretPoint, nsresult>
+HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ const EditorDOMPoint& aPointToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsert.IsSet());
+
+ if (!aPointToInsert.IsInContentNode()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ // If container of the point is not in a block, we don't need to put a
+ // `<br>` element here.
+ if (!HTMLEditUtils::IsBlockElement(
+ *aPointToInsert.ContainerAs<nsIContent>(),
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(
+ *aPointToInsert.ContainerAs<nsIContent>()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ WSRunScanner wsRunScanner(ComputeEditingHost(), aPointToInsert,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ // If the point is not start of a hard line, we don't need to put a `<br>`
+ // element here.
+ if (!wsRunScanner.StartsFromHardLineBreak()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+ // If the point is not end of a hard line or the hard line does not end with
+ // block boundary, we don't need to put a `<br>` element here.
+ if (!wsRunScanner.EndsByBlockBoundary()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ // If we cannot insert a `<br>` element here, do nothing.
+ if (!HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(),
+ *nsGkAtoms::br)) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult = InsertBRElement(
+ WithTransaction::Yes, aPointToInsert, nsIEditor::ePrevious);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes, ePrevious) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ return CaretPoint(insertBRElementResult.unwrap().UnwrapCaretPoint());
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::MakeOrChangeListAndListItemAsSubAction(
+ const nsStaticAtom& aListElementOrListItemElementTagName,
+ const nsAString& aBulletType,
+ SelectAllOfCurrentList aSelectAllOfCurrentList) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(&aListElementOrListItemElementTagName == nsGkAtoms::ul ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::ol ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dl ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dd ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dt);
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return EditActionResult::IgnoredResult();
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ // XXX EditSubAction::eCreateOrChangeDefinitionListItem and
+ // EditSubAction::eCreateOrChangeList are treated differently in
+ // HTMLEditor::MaybeSplitElementsAtEveryBRElement(). Only when
+ // EditSubAction::eCreateOrChangeList, it splits inline nodes.
+ // Currently, it shouldn't be done when we called for formatting
+ // `<dd>` or `<dt>` by
+ // HTMLEditor::MakeDefinitionListItemWithTransaction(). But this
+ // difference may be a bug. We should investigate this later.
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this,
+ &aListElementOrListItemElementTagName == nsGkAtoms::dd ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dt
+ ? EditSubAction::eCreateOrChangeDefinitionListItem
+ : EditSubAction::eCreateOrChangeList,
+ nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(error.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ const nsStaticAtom* listTagName = nullptr;
+ const nsStaticAtom* listItemTagName = nullptr;
+ if (&aListElementOrListItemElementTagName == nsGkAtoms::ul ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::ol) {
+ listTagName = &aListElementOrListItemElementTagName;
+ listItemTagName = nsGkAtoms::li;
+ } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dl) {
+ listTagName = &aListElementOrListItemElementTagName;
+ listItemTagName = nsGkAtoms::dd;
+ } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dd ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dt) {
+ listTagName = nsGkAtoms::dl;
+ listItemTagName = &aListElementOrListItemElementTagName;
+ } else {
+ NS_WARNING(
+ "aListElementOrListItemElementTagName was neither list element name "
+ "nor "
+ "definition listitem element name");
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (MOZ_UNLIKELY(!editingHost)) {
+ return EditActionResult::CanceledResult();
+ }
+
+ // Expands selection range to include the immediate block parent, and then
+ // further expands to include any ancestors whose children are all in the
+ // range.
+ // XXX Why do we do this only when there is only one selection range?
+ if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ SelectionRef().GetRangeAt(0u), *editingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use Selection::SetStartAndEndInLimit() here.
+ error.SuppressException();
+ SelectionRef().SetBaseAndExtentInLimiter(
+ extendedRange.inspect().StartRef().ToRawRangeBoundary(),
+ extendedRange.inspect().EndRef().ToRawRangeBoundary(), error);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return Err(error.StealNSResult());
+ }
+ }
+
+ AutoListElementCreator listCreator(*listTagName, *listItemTagName,
+ aBulletType);
+ AutoRangeArray selectionRanges(SelectionRef());
+ Result<EditActionResult, nsresult> result = listCreator.Run(
+ *this, selectionRanges, aSelectAllOfCurrentList, *editingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::ConvertContentAroundRangesToList() failed");
+ // XXX Should we try to restore selection ranges in this case?
+ return result;
+ }
+
+ rv = selectionRanges.ApplyTo(SelectionRef());
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::ApplyTo() failed");
+ return Err(rv);
+ }
+ return result.inspect().Ignored() ? EditActionResult::CanceledResult()
+ : EditActionResult::HandledResult();
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoListElementCreator::Run(
+ HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges,
+ SelectAllOfCurrentList aSelectAllOfCurrentList,
+ const Element& aEditingHost) const {
+ MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!aHTMLEditor.IsSelectionRangeContainerNotContent());
+
+ if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(aHTMLEditor))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ AutoContentNodeArray arrayOfContents;
+ nsresult rv = SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList(
+ aHTMLEditor, aRanges, aSelectAllOfCurrentList, aEditingHost,
+ arrayOfContents);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoListElementCreator::"
+ "SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList() failed");
+ return Err(rv);
+ }
+
+ // check if all our nodes are <br>s, or empty inlines
+ // if no nodes, we make empty list. Ditto if the user tried to make a list
+ // of some # of breaks.
+ if (AutoListElementCreator::
+ IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements(
+ arrayOfContents)) {
+ Result<RefPtr<Element>, nsresult> newListItemElementOrError =
+ ReplaceContentNodesWithEmptyNewList(aHTMLEditor, aRanges,
+ arrayOfContents, aEditingHost);
+ if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) {
+ NS_WARNING(
+ "AutoListElementCreator::ReplaceContentNodesWithEmptyNewList() "
+ "failed");
+ return newListItemElementOrError.propagateErr();
+ }
+ if (MOZ_UNLIKELY(!newListItemElementOrError.inspect())) {
+ aRanges.RestoreFromSavedRanges();
+ return EditActionResult::CanceledResult();
+ }
+ aRanges.ClearSavedRanges();
+ nsresult rv = aRanges.Collapse(
+ EditorRawDOMPoint(newListItemElementOrError.inspect(), 0u));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return Err(rv);
+ }
+ return EditActionResult::IgnoredResult();
+ }
+
+ Result<RefPtr<Element>, nsresult> listItemOrListToPutCaretOrError =
+ WrapContentNodesIntoNewListElements(aHTMLEditor, aRanges, arrayOfContents,
+ aEditingHost);
+ if (MOZ_UNLIKELY(listItemOrListToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "AutoListElementCreator::WrapContentNodesIntoNewListElements() failed");
+ return listItemOrListToPutCaretOrError.propagateErr();
+ }
+
+ MOZ_ASSERT(aRanges.HasSavedRanges());
+ aRanges.RestoreFromSavedRanges();
+
+ // If selection will be collapsed but not in listItemOrListToPutCaret, we need
+ // to adjust the caret position into it.
+ if (listItemOrListToPutCaretOrError.inspect()) {
+ DebugOnly<nsresult> rvIgnored =
+ EnsureCollapsedRangeIsInListItemOrListElement(
+ *listItemOrListToPutCaretOrError.inspect(), aRanges);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "AutoListElementCreator::"
+ "EnsureCollapsedRangeIsInListItemOrListElement() failed, but ignored");
+ }
+
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::AutoListElementCreator::
+ SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList(
+ HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges,
+ SelectAllOfCurrentList aSelectAllOfCurrentList,
+ const Element& aEditingHost,
+ ContentNodeArray& aOutArrayOfContents) const {
+ MOZ_ASSERT(aOutArrayOfContents.IsEmpty());
+
+ if (aSelectAllOfCurrentList == SelectAllOfCurrentList::Yes) {
+ if (Element* parentListElementOfRanges =
+ aRanges.GetClosestAncestorAnyListElementOfRange()) {
+ aOutArrayOfContents.AppendElement(
+ OwningNonNull<nsIContent>(*parentListElementOfRanges));
+ return NS_OK;
+ }
+ }
+
+ AutoRangeArray extendedRanges(aRanges);
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the
+ // normal cases, but removing this may cause the behavior with the
+ // legacy mutation event listeners. We should try to delete this in
+ // a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+
+ extendedRanges.ExtendRangesToWrapLines(EditSubAction::eCreateOrChangeList,
+ BlockInlineCheck::UseHTMLDefaultStyle,
+ aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges.SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ aHTMLEditor, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed");
+ return splitResult.unwrapErr();
+ }
+ nsresult rv = extendedRanges.CollectEditTargetNodes(
+ aHTMLEditor, aOutArrayOfContents, EditSubAction::eCreateOrChangeList,
+ AutoRangeArray::CollectNonEditableNodes::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
+ return rv;
+ }
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ aHTMLEditor.MaybeSplitElementsAtEveryBRElement(
+ aOutArrayOfContents, EditSubAction::eCreateOrChangeList);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eCreateOrChangeList) failed");
+ return splitAtBRElementsResult.unwrapErr();
+ }
+ return NS_OK;
+}
+
+// static
+bool HTMLEditor::AutoListElementCreator::
+ IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements(
+ const ContentNodeArray& aArrayOfContents) {
+ for (const OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ // if content is not a <br> or empty inline, we're done
+ // XXX Should we handle line breaks in preformatted text node?
+ if (!content->IsHTMLElement(nsGkAtoms::br) &&
+ !HTMLEditUtils::IsEmptyInlineContainer(
+ content,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+Result<RefPtr<Element>, nsresult>
+HTMLEditor::AutoListElementCreator::ReplaceContentNodesWithEmptyNewList(
+ HTMLEditor& aHTMLEditor, const AutoRangeArray& aRanges,
+ const AutoContentNodeArray& aArrayOfContents,
+ const Element& aEditingHost) const {
+ // if only breaks, delete them
+ for (const OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ // MOZ_KnownLive because of bug 1620312
+ nsresult rv =
+ aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(*content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ const auto firstRangeStartPoint =
+ aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!firstRangeStartPoint.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Make sure we can put a list here.
+ if (!HTMLEditUtils::CanNodeContain(*firstRangeStartPoint.GetContainer(),
+ mListTagName)) {
+ return RefPtr<Element>();
+ }
+
+ RefPtr<Element> newListItemElement;
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ aHTMLEditor.InsertElementWithSplittingAncestorsWithTransaction(
+ mListTagName, firstRangeStartPoint, BRElementNextToSplitPoint::Keep,
+ aEditingHost,
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [&](HTMLEditor& aHTMLEditor, Element& aListElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ AutoHandlingState dummyState;
+ Result<CreateElementResult, nsresult> createListItemElementResult =
+ AppendListItemElement(aHTMLEditor, aListElement, dummyState);
+ if (MOZ_UNLIKELY(createListItemElementResult.isErr())) {
+ NS_WARNING(
+ "AutoListElementCreator::AppendListItemElement() failed");
+ return createListItemElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedResult =
+ createListItemElementResult.unwrap();
+ // There is AutoSelectionRestorer in this method so that it'll
+ // be restored or updated with making it abort. Therefore,
+ // we don't need to update selection here.
+ // XXX I'd like to check aRanges.HasSavedRanges() here, but it
+ // requires ifdefs to avoid bustage of opt builds caused
+ // by unused warning...
+ unwrappedResult.IgnoreCaretPointSuggestion();
+ newListItemElement = unwrappedResult.UnwrapNewNode();
+ MOZ_ASSERT(newListItemElement);
+ return NS_OK;
+ });
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "%s) failed",
+ nsAtomCString(&mListTagName).get())
+ .get());
+ return createNewListElementResult.propagateErr();
+ }
+ MOZ_ASSERT(createNewListElementResult.inspect().GetNewNode());
+
+ // Put selection in new list item and don't restore the Selection.
+ createNewListElementResult.inspect().IgnoreCaretPointSuggestion();
+ return newListItemElement;
+}
+
+Result<RefPtr<Element>, nsresult>
+HTMLEditor::AutoListElementCreator::WrapContentNodesIntoNewListElements(
+ HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges,
+ AutoContentNodeArray& aArrayOfContents, const Element& aEditingHost) const {
+ // if there is only one node in the array, and it is a list, div, or
+ // blockquote, then look inside of it until we find inner list or content.
+ if (aArrayOfContents.Length() == 1) {
+ if (Element* deepestDivBlockquoteOrListElement =
+ HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild(
+ aArrayOfContents[0], {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseHTMLDefaultStyle, nsGkAtoms::div,
+ nsGkAtoms::blockquote, nsGkAtoms::ul, nsGkAtoms::ol,
+ nsGkAtoms::dl)) {
+ if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements(
+ nsGkAtoms::div, nsGkAtoms::blockquote)) {
+ aArrayOfContents.Clear();
+ HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement,
+ aArrayOfContents, 0, {});
+ } else {
+ aArrayOfContents.ReplaceElementAt(
+ 0, OwningNonNull<nsIContent>(*deepestDivBlockquoteOrListElement));
+ }
+ }
+ }
+
+ // Ok, now go through all the nodes and put then in the list,
+ // or whatever is appropriate. Wohoo!
+ AutoHandlingState handlingState;
+ for (const OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ // MOZ_KnownLive because of bug 1620312
+ nsresult rv = HandleChildContent(aHTMLEditor, MOZ_KnownLive(content),
+ handlingState, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoListElementCreator::HandleChildContent() failed");
+ return Err(rv);
+ }
+ }
+
+ return std::move(handlingState.mListOrListItemElementToPutCaret);
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildContent(
+ HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
+ AutoHandlingState& aState, const Element& aEditingHost) const {
+ // make sure we don't assemble content that is in different table cells
+ // into the same list. respect table cell boundaries when listifying.
+ if (aState.mCurrentListElement &&
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ *aState.mCurrentListElement) !=
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ aHandlingContent)) {
+ aState.mCurrentListElement = nullptr;
+ }
+
+ // If current node is a `<br>` element, delete it and forget previous
+ // list item element.
+ // If current node is an empty inline node, just delete it.
+ if (EditorUtils::IsEditableContent(aHandlingContent, EditorType::HTML) &&
+ (aHandlingContent.IsHTMLElement(nsGkAtoms::br) ||
+ HTMLEditUtils::IsEmptyInlineContainer(
+ aHandlingContent,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ BlockInlineCheck::UseHTMLDefaultStyle))) {
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aHandlingContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ if (aHandlingContent.IsHTMLElement(nsGkAtoms::br)) {
+ aState.mPreviousListItemElement = nullptr;
+ }
+ return NS_OK;
+ }
+
+ // If we meet a list, we can reuse it or convert it to the expected type list.
+ if (HTMLEditUtils::IsAnyListElement(&aHandlingContent)) {
+ nsresult rv = HandleChildListElement(
+ aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::HandleChildListElement() failed");
+ return rv;
+ }
+
+ // We cannot handle nodes if not in element node.
+ if (NS_WARN_IF(!aHandlingContent.GetParentElement())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If we meet a list item, we can just move it to current list element or new
+ // list element.
+ if (HTMLEditUtils::IsListItem(&aHandlingContent)) {
+ nsresult rv = HandleChildListItemElement(
+ aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::HandleChildListItemElement() failed");
+ return rv;
+ }
+
+ // If we meet a <div> or a <p>, we want only its children to wrapping into
+ // list element. Therefore, this call will call this recursively.
+ if (aHandlingContent.IsAnyOfHTMLElements(nsGkAtoms::div, nsGkAtoms::p)) {
+ nsresult rv = HandleChildDivOrParagraphElement(
+ aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState,
+ aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::HandleChildDivOrParagraphElement() failed");
+ return rv;
+ }
+
+ // If we've not met a list element, create a list element and make it
+ // current list element.
+ if (!aState.mCurrentListElement) {
+ nsresult rv = CreateAndUpdateCurrentListElement(
+ aHTMLEditor, EditorDOMPoint(&aHandlingContent),
+ EmptyListItem::NotCreate, aState, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoListElementCreator::HandleChildInlineElement() failed");
+ return rv;
+ }
+ }
+
+ // If we meet an inline content, we want to move it to previously used list
+ // item element or new list item element.
+ if (HTMLEditUtils::IsInlineContent(aHandlingContent,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ nsresult rv =
+ HandleChildInlineContent(aHTMLEditor, aHandlingContent, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::HandleChildInlineElement() failed");
+ return rv;
+ }
+
+ // Otherwise, we should wrap it into new list item element.
+ nsresult rv =
+ WrapContentIntoNewListItemElement(aHTMLEditor, aHandlingContent, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::WrapContentIntoNewListItemElement() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildListElement(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListElement,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aHandlingListElement));
+
+ // If we met a list element and current list element is not a descendant
+ // of the list, append current node to end of the current list element.
+ // Then, wrap it with list item element and delete the old container.
+ if (aState.mCurrentListElement &&
+ !EditorUtils::IsDescendantOf(aHandlingListElement,
+ *aState.mCurrentListElement)) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ aHandlingListElement, MOZ_KnownLive(*aState.mCurrentListElement));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+
+ Result<CreateElementResult, nsresult> convertListTypeResult =
+ aHTMLEditor.ChangeListElementType(aHandlingListElement, mListTagName,
+ mListItemTagName);
+ if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
+ NS_WARNING("HTMLEditor::ChangeListElementType() failed");
+ return convertListTypeResult.propagateErr();
+ }
+ convertListTypeResult.inspect().IgnoreCaretPointSuggestion();
+
+ Result<EditorDOMPoint, nsresult> unwrapNewListElementResult =
+ aHTMLEditor.RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*convertListTypeResult.inspect().GetNewNode()));
+ if (MOZ_UNLIKELY(unwrapNewListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapNewListElementResult.propagateErr();
+ }
+ aState.mPreviousListItemElement = nullptr;
+ return NS_OK;
+ }
+
+ // If current list element is in found list element or we've not met a
+ // list element, convert current list element to proper type.
+ Result<CreateElementResult, nsresult> convertListTypeResult =
+ aHTMLEditor.ChangeListElementType(aHandlingListElement, mListTagName,
+ mListItemTagName);
+ if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
+ NS_WARNING("HTMLEditor::ChangeListElementType() failed");
+ return convertListTypeResult.propagateErr();
+ }
+ CreateElementResult unwrappedConvertListTypeResult =
+ convertListTypeResult.unwrap();
+ unwrappedConvertListTypeResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedConvertListTypeResult.GetNewNode());
+ aState.mCurrentListElement = unwrappedConvertListTypeResult.UnwrapNewNode();
+ aState.mPreviousListItemElement = nullptr;
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::AutoListElementCreator::HandleChildListItemInDifferentTypeList(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(&aHandlingListItemElement));
+ MOZ_ASSERT(
+ !aHandlingListItemElement.GetParent()->IsHTMLElement(&mListTagName));
+
+ // If we've not met a list element or current node is not in current list
+ // element, insert a list element at current node and set current list element
+ // to the new one.
+ if (!aState.mCurrentListElement ||
+ aHandlingListItemElement.IsInclusiveDescendantOf(
+ aState.mCurrentListElement)) {
+ EditorDOMPoint atListItem(&aHandlingListItemElement);
+ MOZ_ASSERT(atListItem.IsInContentNode());
+
+ Result<SplitNodeResult, nsresult> splitListItemParentResult =
+ aHTMLEditor.SplitNodeWithTransaction(atListItem);
+ if (MOZ_UNLIKELY(splitListItemParentResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitListItemParentResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitListItemParentResult =
+ splitListItemParentResult.unwrap();
+ MOZ_ASSERT(unwrappedSplitListItemParentResult.DidSplit());
+ unwrappedSplitListItemParentResult.IgnoreCaretPointSuggestion();
+
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ aHTMLEditor.CreateAndInsertElement(
+ WithTransaction::Yes, mListTagName,
+ unwrappedSplitListItemParentResult.AtNextContent<EditorDOMPoint>());
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) "
+ "failed");
+ return createNewListElementResult.propagateErr();
+ }
+ CreateElementResult unwrapCreateNewListElementResult =
+ createNewListElementResult.unwrap();
+ unwrapCreateNewListElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrapCreateNewListElementResult.GetNewNode());
+ aState.mCurrentListElement =
+ unwrapCreateNewListElementResult.UnwrapNewNode();
+ }
+
+ // Then, move current node into current list element.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ aHandlingListItemElement, MOZ_KnownLive(*aState.mCurrentListElement));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+
+ // Convert list item type if current node is different list item type.
+ if (aHandlingListItemElement.IsHTMLElement(&mListItemTagName)) {
+ return NS_OK;
+ }
+ Result<CreateElementResult, nsresult> newListItemElementOrError =
+ aHTMLEditor.ReplaceContainerAndCloneAttributesWithTransaction(
+ aHandlingListItemElement, mListItemTagName);
+ if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed");
+ return newListItemElementOrError.propagateErr();
+ }
+ newListItemElementOrError.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildListItemElement(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(aHandlingListItemElement.GetParentNode());
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(&aHandlingListItemElement));
+
+ // If current list item element is not in proper list element, we need
+ // to convert the list element.
+ // XXX This check is not enough,
+ if (!aHandlingListItemElement.GetParentNode()->IsHTMLElement(&mListTagName)) {
+ nsresult rv = HandleChildListItemInDifferentTypeList(
+ aHTMLEditor, aHandlingListItemElement, aState);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoListElementCreator::HandleChildListItemInDifferentTypeList() "
+ "failed");
+ return rv;
+ }
+ } else {
+ nsresult rv = HandleChildListItemInSameTypeList(
+ aHTMLEditor, aHandlingListItemElement, aState);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoListElementCreator::HandleChildListItemInSameTypeList() failed");
+ return rv;
+ }
+ }
+
+ // If bullet type is specified, set list type attribute.
+ // XXX Cannot we set type attribute before inserting the list item
+ // element into the DOM tree?
+ if (!mBulletType.IsEmpty()) {
+ nsresult rv = aHTMLEditor.SetAttributeWithTransaction(
+ aHandlingListItemElement, *nsGkAtoms::type, mBulletType);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::type) failed");
+ return rv;
+ }
+
+ // Otherwise, remove list type attribute if there is.
+ if (!aHandlingListItemElement.HasAttr(nsGkAtoms::type)) {
+ return NS_OK;
+ }
+ nsresult rv = aHTMLEditor.RemoveAttributeWithTransaction(
+ aHandlingListItemElement, *nsGkAtoms::type);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::type) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildListItemInSameTypeList(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(&aHandlingListItemElement));
+ MOZ_ASSERT(
+ aHandlingListItemElement.GetParent()->IsHTMLElement(&mListTagName));
+
+ EditorDOMPoint atListItem(&aHandlingListItemElement);
+ MOZ_ASSERT(atListItem.IsInContentNode());
+
+ // If we've not met a list element, set current list element to the
+ // parent of current list item element.
+ if (!aState.mCurrentListElement) {
+ aState.mCurrentListElement = atListItem.GetContainerAs<Element>();
+ NS_WARNING_ASSERTION(
+ HTMLEditUtils::IsAnyListElement(aState.mCurrentListElement),
+ "Current list item parent is not a list element");
+ }
+ // If current list item element is not a child of current list element,
+ // move it into current list item.
+ else if (atListItem.GetContainer() != aState.mCurrentListElement) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ aHandlingListItemElement,
+ MOZ_KnownLive(*aState.mCurrentListElement));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ // Then, if current list item element is not proper type for current
+ // list element, convert list item element to proper element.
+ if (aHandlingListItemElement.IsHTMLElement(&mListItemTagName)) {
+ return NS_OK;
+ }
+ // FIXME: Manage attribute cloning
+ Result<CreateElementResult, nsresult> newListItemElementOrError =
+ aHTMLEditor.ReplaceContainerAndCloneAttributesWithTransaction(
+ aHandlingListItemElement, mListItemTagName);
+ if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction() "
+ "failed");
+ return newListItemElementOrError.propagateErr();
+ }
+ newListItemElementOrError.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildDivOrParagraphElement(
+ HTMLEditor& aHTMLEditor, Element& aHandlingDivOrParagraphElement,
+ AutoHandlingState& aState, const Element& aEditingHost) const {
+ MOZ_ASSERT(aHandlingDivOrParagraphElement.IsAnyOfHTMLElements(nsGkAtoms::div,
+ nsGkAtoms::p));
+
+ AutoRestore<RefPtr<Element>> previouslyReplacingBlockElement(
+ aState.mReplacingBlockElement);
+ aState.mReplacingBlockElement = &aHandlingDivOrParagraphElement;
+ AutoRestore<bool> previouslyReplacingBlockElementIdCopied(
+ aState.mMaybeCopiedReplacingBlockElementId);
+ aState.mMaybeCopiedReplacingBlockElementId = false;
+
+ // If the <div> or <p> is empty, we should replace it with a list element
+ // and/or a list item element.
+ if (HTMLEditUtils::IsEmptyNode(aHandlingDivOrParagraphElement,
+ {EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible})) {
+ if (!aState.mCurrentListElement) {
+ nsresult rv = CreateAndUpdateCurrentListElement(
+ aHTMLEditor, EditorDOMPoint(&aHandlingDivOrParagraphElement),
+ EmptyListItem::Create, aState, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoListElementCreator::CreateAndUpdateCurrentListElement("
+ "EmptyListItem::Create) failed");
+ return rv;
+ }
+ } else {
+ Result<CreateElementResult, nsresult> createListItemElementResult =
+ AppendListItemElement(
+ aHTMLEditor, MOZ_KnownLive(*aState.mCurrentListElement), aState);
+ if (MOZ_UNLIKELY(createListItemElementResult.isErr())) {
+ NS_WARNING("AutoListElementCreator::AppendListItemElement() failed");
+ return createListItemElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedResult =
+ createListItemElementResult.unwrap();
+ unwrappedResult.IgnoreCaretPointSuggestion();
+ aState.mListOrListItemElementToPutCaret = unwrappedResult.UnwrapNewNode();
+ }
+ nsresult rv =
+ aHTMLEditor.DeleteNodeWithTransaction(aHandlingDivOrParagraphElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+
+ // We don't want new inline contents inserted into the new list item element
+ // because we want to keep the line break at end of
+ // aHandlingDivOrParagraphElement.
+ aState.mPreviousListItemElement = nullptr;
+
+ return NS_OK;
+ }
+
+ // If current node is a <div> element, replace it with its children and handle
+ // them as same as topmost children in the range.
+ AutoContentNodeArray arrayOfContentsInDiv;
+ HTMLEditUtils::CollectChildren(aHandlingDivOrParagraphElement,
+ arrayOfContentsInDiv, 0,
+ {CollectChildrenOption::CollectListChildren,
+ CollectChildrenOption::CollectTableChildren});
+
+ Result<EditorDOMPoint, nsresult> unwrapDivElementResult =
+ aHTMLEditor.RemoveContainerWithTransaction(
+ aHandlingDivOrParagraphElement);
+ if (MOZ_UNLIKELY(unwrapDivElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapDivElementResult.unwrapErr();
+ }
+
+ for (const OwningNonNull<nsIContent>& content : arrayOfContentsInDiv) {
+ // MOZ_KnownLive because of bug 1620312
+ nsresult rv = HandleChildContent(aHTMLEditor, MOZ_KnownLive(content),
+ aState, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoListElementCreator::HandleChildContent() failed");
+ return rv;
+ }
+ }
+
+ // We don't want new inline contents inserted into the new list item element
+ // because we want to keep the line break at end of
+ // aHandlingDivOrParagraphElement.
+ aState.mPreviousListItemElement = nullptr;
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::CreateAndUpdateCurrentListElement(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert,
+ EmptyListItem aEmptyListItem, AutoHandlingState& aState,
+ const Element& aEditingHost) const {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ aState.mPreviousListItemElement = nullptr;
+ RefPtr<Element> newListItemElement;
+ auto initializer =
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [&](HTMLEditor&, Element& aListElement, const EditorDOMPoint&)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ // If the replacing element has `dir` attribute, the new list
+ // element should take it to correct its list marker position.
+ if (aState.mReplacingBlockElement) {
+ nsString dirValue;
+ if (aState.mReplacingBlockElement->GetAttr(nsGkAtoms::dir,
+ dirValue) &&
+ !dirValue.IsEmpty()) {
+ // We don't need to use transaction to set `dir` attribute here
+ // because the element will be stored with the `dir` attribute
+ // in InsertNodeTransaction. Therefore, undo should work.
+ IgnoredErrorResult ignoredError;
+ aListElement.SetAttr(nsGkAtoms::dir, dirValue, ignoredError);
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "Element::SetAttr(nsGkAtoms::dir) failed, but ignored");
+ }
+ }
+ if (aEmptyListItem == EmptyListItem::Create) {
+ Result<CreateElementResult, nsresult> createNewListItemResult =
+ AppendListItemElement(aHTMLEditor, aListElement, aState);
+ if (MOZ_UNLIKELY(createNewListItemResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::AppendNewElementToInsertingElement()"
+ " failed");
+ return createNewListItemResult.unwrapErr();
+ }
+ CreateElementResult unwrappedResult =
+ createNewListItemResult.unwrap();
+ unwrappedResult.IgnoreCaretPointSuggestion();
+ newListItemElement = unwrappedResult.UnwrapNewNode();
+ }
+ return NS_OK;
+ };
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ aHTMLEditor.InsertElementWithSplittingAncestorsWithTransaction(
+ mListTagName, aPointToInsert, BRElementNextToSplitPoint::Keep,
+ aEditingHost, initializer);
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction(%s) failed",
+ nsAtomCString(&mListTagName).get())
+ .get());
+ return createNewListElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewListElementResult =
+ createNewListElementResult.unwrap();
+ unwrappedCreateNewListElementResult.IgnoreCaretPointSuggestion();
+
+ MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode());
+ aState.mListOrListItemElementToPutCaret =
+ newListItemElement ? newListItemElement.get()
+ : unwrappedCreateNewListElementResult.GetNewNode();
+ aState.mCurrentListElement =
+ unwrappedCreateNewListElementResult.UnwrapNewNode();
+ aState.mPreviousListItemElement = std::move(newListItemElement);
+ return NS_OK;
+}
+
+// static
+nsresult HTMLEditor::AutoListElementCreator::MaybeCloneAttributesToNewListItem(
+ HTMLEditor& aHTMLEditor, Element& aListItemElement,
+ AutoHandlingState& aState) {
+ if (!aState.mReplacingBlockElement) {
+ return NS_OK;
+ }
+ // If we're replacing a block element, the list items should have attributes
+ // of the replacing element. However, we don't want to copy `dir` attribute
+ // because it does not affect content in list item element and setting
+ // opposite direction from the parent list causes the marker invisible.
+ // Therefore, we don't want to take it. Finally, we don't need to use
+ // transaction to copy the attributes here because the element will be stored
+ // with the attributes in InsertNodeTransaction. Therefore, undo should work.
+ nsresult rv = aHTMLEditor.CopyAttributes(
+ WithTransaction::No, aListItemElement,
+ MOZ_KnownLive(*aState.mReplacingBlockElement),
+ aState.mMaybeCopiedReplacingBlockElementId
+ ? HTMLEditor::CopyAllAttributesExceptIdAndDir
+ : HTMLEditor::CopyAllAttributesExceptDir);
+ aState.mMaybeCopiedReplacingBlockElementId = true;
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::CopyAttributes(WithTransaction::No) failed");
+ return rv;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::AutoListElementCreator::AppendListItemElement(
+ HTMLEditor& aHTMLEditor, const Element& aListElement,
+ AutoHandlingState& aState) const {
+ const WithTransaction withTransaction = aListElement.IsInComposedDoc()
+ ? WithTransaction::Yes
+ : WithTransaction::No;
+ Result<CreateElementResult, nsresult> createNewListItemResult =
+ aHTMLEditor.CreateAndInsertElement(
+ withTransaction, mListItemTagName,
+ EditorDOMPoint::AtEndOf(aListElement),
+ !aState.mReplacingBlockElement
+ ? HTMLEditor::DoNothingForNewElement
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ : [&aState](HTMLEditor& aHTMLEditor, Element& aListItemElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ nsresult rv =
+ AutoListElementCreator::MaybeCloneAttributesToNewListItem(
+ aHTMLEditor, aListItemElement, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::"
+ "MaybeCloneAttributesToNewListItem() failed");
+ return rv;
+ });
+ NS_WARNING_ASSERTION(createNewListItemResult.isOk(),
+ "HTMLEditor::CreateAndInsertElement() failed");
+ return createNewListItemResult;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildInlineContent(
+ HTMLEditor& aHTMLEditor, nsIContent& aHandlingInlineContent,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(HTMLEditUtils::IsInlineContent(
+ aHandlingInlineContent, BlockInlineCheck::UseHTMLDefaultStyle));
+
+ // If we're currently handling contents of a list item and current node
+ // is not a block element, move current node into the list item.
+ if (!aState.mPreviousListItemElement) {
+ nsresult rv = WrapContentIntoNewListItemElement(
+ aHTMLEditor, aHandlingInlineContent, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::WrapContentIntoNewListItemElement() failed");
+ return rv;
+ }
+
+ Result<MoveNodeResult, nsresult> moveInlineElementResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ aHandlingInlineContent,
+ MOZ_KnownLive(*aState.mPreviousListItemElement));
+ if (MOZ_UNLIKELY(moveInlineElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveInlineElementResult.propagateErr();
+ }
+ moveInlineElementResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::WrapContentIntoNewListItemElement(
+ HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
+ AutoHandlingState& aState) const {
+ // If current node is not a paragraph, wrap current node with new list
+ // item element and move it into current list element.
+ Result<CreateElementResult, nsresult> wrapContentInListItemElementResult =
+ aHTMLEditor.InsertContainerWithTransaction(
+ aHandlingContent, mListItemTagName,
+ !aState.mReplacingBlockElement
+ ? HTMLEditor::DoNothingForNewElement
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ : [&aState](HTMLEditor& aHTMLEditor, Element& aListItemElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ nsresult rv =
+ AutoListElementCreator::MaybeCloneAttributesToNewListItem(
+ aHTMLEditor, aListItemElement, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::"
+ "MaybeCloneAttributesToNewListItem() failed");
+ return rv;
+ });
+ if (MOZ_UNLIKELY(wrapContentInListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
+ return wrapContentInListItemElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedWrapContentInListItemElementResult =
+ wrapContentInListItemElementResult.unwrap();
+ unwrappedWrapContentInListItemElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedWrapContentInListItemElementResult.GetNewNode());
+
+ // MOZ_KnownLive(unwrappedWrapContentInListItemElementResult.GetNewNode()):
+ // The result is grabbed by unwrappedWrapContentInListItemElementResult.
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ MOZ_KnownLive(
+ *unwrappedWrapContentInListItemElementResult.GetNewNode()),
+ MOZ_KnownLive(*aState.mCurrentListElement));
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveListItemElementResult.unwrapErr();
+ }
+ moveListItemElementResult.inspect().IgnoreCaretPointSuggestion();
+
+ // If current node is not a block element, new list item should have
+ // following inline nodes too.
+ if (HTMLEditUtils::IsInlineContent(aHandlingContent,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ aState.mPreviousListItemElement =
+ unwrappedWrapContentInListItemElementResult.UnwrapNewNode();
+ } else {
+ aState.mPreviousListItemElement = nullptr;
+ }
+
+ // XXX Why don't we set `type` attribute here??
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::
+ EnsureCollapsedRangeIsInListItemOrListElement(
+ Element& aListItemOrListToPutCaret, AutoRangeArray& aRanges) const {
+ if (!aRanges.IsCollapsed() || aRanges.Ranges().IsEmpty()) {
+ return NS_OK;
+ }
+
+ const auto firstRangeStartPoint =
+ aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!firstRangeStartPoint.IsSet())) {
+ return NS_OK;
+ }
+ Result<EditorRawDOMPoint, nsresult> pointToPutCaretOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(aListItemOrListToPutCaret, firstRangeStartPoint);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditUtils::ComputePointToPutCaretInElementIfOutside()");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ nsresult rv = aRanges.Collapse(pointToPutCaretOrError.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::RemoveListAtSelectionAsSubAction(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eRemoveList, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return error.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ SelectionRef().GetRangeAt(0u), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.unwrapErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use Selection::SetStartAndEndInLimit() here.
+ error.SuppressException();
+ SelectionRef().SetBaseAndExtentInLimiter(
+ extendedRange.inspect().StartRef().ToRawRangeBoundary(),
+ extendedRange.inspect().EndRef().ToRawRangeBoundary(), error);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (error.Failed()) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return error.StealNSResult();
+ }
+ }
+
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ {
+ AutoRangeArray extendedSelectionRanges(SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ EditSubAction::eCreateOrChangeList,
+ BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedSelectionRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eCreateOrChangeList,
+ AutoRangeArray::CollectNonEditableNodes::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
+ return rv;
+ }
+ }
+
+ const Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eCreateOrChangeList);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eCreateOrChangeList) failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ }
+
+ // Remove all non-editable nodes. Leave them be.
+ // XXX CollectEditTargetNodes() should return only editable contents when it's
+ // called with CollectNonEditableNodes::No, but checking it here, looks
+ // like just wasting the runtime cost.
+ for (int32_t i = arrayOfContents.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsIContent>& content = arrayOfContents[i];
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ arrayOfContents.RemoveElementAt(i);
+ }
+ }
+
+ // Only act on lists or list items in the array
+ for (auto& content : arrayOfContents) {
+ // here's where we actually figure out what to do
+ if (HTMLEditUtils::IsListItem(content)) {
+ // unlist this listitem
+ nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()),
+ LiftUpFromAllParentListElements::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:"
+ ":Yes) failed");
+ return rv;
+ }
+ continue;
+ }
+ if (HTMLEditUtils::IsAnyListElement(content)) {
+ // node is a list, move list items out
+ nsresult rv =
+ DestroyListStructureRecursively(MOZ_KnownLive(*content->AsElement()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DestroyListStructureRecursively() failed");
+ return rv;
+ }
+ continue;
+ }
+ }
+ return NS_OK;
+}
+
+Result<RefPtr<Element>, nsresult>
+HTMLEditor::FormatBlockContainerWithTransaction(
+ AutoRangeArray& aSelectionRanges, const nsStaticAtom& aNewFormatTagName,
+ FormatBlockMode aFormatBlockMode, const Element& aEditingHost) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!aSelectionRanges.IsCollapsed() &&
+ aSelectionRanges.Ranges().Length() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ aSelectionRanges.FirstRangeRef(), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use AutoRangeArray::SetStartAndEnd() here.
+ if (NS_FAILED(aSelectionRanges.SetBaseAndExtent(
+ extendedRange.inspect().StartRef(),
+ extendedRange.inspect().EndRef()))) {
+ NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ MOZ_ALWAYS_TRUE(aSelectionRanges.SaveAndTrackRanges(*this));
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ aSelectionRanges.ExtendRangesToWrapLines(
+ aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
+ ? EditSubAction::eFormatBlockForHTMLCommand
+ : EditSubAction::eCreateOrRemoveBlock,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ aSelectionRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed");
+ return splitResult.propagateErr();
+ }
+ nsresult rv = aSelectionRanges.CollectEditTargetNodes(
+ *this, arrayOfContents,
+ aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
+ ? EditSubAction::eFormatBlockForHTMLCommand
+ : EditSubAction::eCreateOrRemoveBlock,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(CollectNonEditableNodes::No) "
+ "failed");
+ return Err(rv);
+ }
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(
+ arrayOfContents,
+ aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
+ ? EditSubAction::eFormatBlockForHTMLCommand
+ : EditSubAction::eCreateOrRemoveBlock);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING("HTMLEditor::MaybeSplitElementsAtEveryBRElement() failed");
+ return splitAtBRElementsResult.propagateErr();
+ }
+
+ // If there is no visible and editable nodes in the edit targets, make an
+ // empty block.
+ // XXX Isn't this odd if there are only non-editable visible nodes?
+ if (HTMLEditUtils::IsEmptyOneHardLine(
+ arrayOfContents, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ if (NS_WARN_IF(aSelectionRanges.Ranges().IsEmpty())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ auto pointToInsertBlock =
+ aSelectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand &&
+ (&aNewFormatTagName == nsGkAtoms::normal ||
+ &aNewFormatTagName == nsGkAtoms::_empty)) {
+ if (!pointToInsertBlock.IsInContentNode()) {
+ NS_WARNING(
+ "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find "
+ "block parent because container of the point is not content");
+ return Err(NS_ERROR_FAILURE);
+ }
+ // We are removing blocks (going to "body text")
+ const RefPtr<Element> editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *pointToInsertBlock.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (!editableBlockElement) {
+ NS_WARNING(
+ "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find "
+ "block parent");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (editableBlockElement->IsAnyOfHTMLElements(
+ nsGkAtoms::dd, nsGkAtoms::dl, nsGkAtoms::dt) ||
+ !HTMLEditUtils::IsFormatElementForParagraphStateCommand(
+ *editableBlockElement)) {
+ return RefPtr<Element>();
+ }
+
+ // If the first editable node after selection is a br, consume it.
+ // Otherwise it gets pushed into a following block after the split,
+ // which is visually bad.
+ if (nsCOMPtr<nsIContent> brContent = HTMLEditUtils::GetNextContent(
+ pointToInsertBlock, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ &aEditingHost)) {
+ if (brContent && brContent->IsHTMLElement(nsGkAtoms::br)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
+ nsresult rv = DeleteNodeWithTransaction(*brContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ }
+ // Do the splits!
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitNodeDeepWithTransaction(
+ *editableBlockElement, pointToInsertBlock,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ unwrappedSplitNodeResult.IgnoreCaretPointSuggestion();
+ // Put a <br> element at the split point
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(
+ WithTransaction::Yes,
+ unwrappedSplitNodeResult.AtSplitPoint<EditorDOMPoint>());
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ aSelectionRanges.ClearSavedRanges();
+ nsresult rv = aSelectionRanges.Collapse(
+ EditorRawDOMPoint(insertBRElementResult.inspect().GetNewNode()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return Err(rv);
+ }
+ return RefPtr<Element>();
+ }
+
+ // We are making a block. Consume a br, if needed.
+ if (nsCOMPtr<nsIContent> maybeBRContent = HTMLEditUtils::GetNextContent(
+ pointToInsertBlock,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost)) {
+ if (maybeBRContent->IsHTMLElement(nsGkAtoms::br)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
+ nsresult rv = DeleteNodeWithTransaction(*maybeBRContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ // We don't need to act on this node any more
+ arrayOfContents.RemoveElement(maybeBRContent);
+ }
+ }
+ // Make sure we can put a block here.
+ Result<CreateElementResult, nsresult> createNewBlockElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ aNewFormatTagName, pointToInsertBlock,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "%s) failed",
+ nsAtomCString(&aNewFormatTagName).get())
+ .get());
+ return createNewBlockElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewBlockElementResult =
+ createNewBlockElementResult.unwrap();
+ unwrappedCreateNewBlockElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode());
+
+ // Delete anything that was in the list of nodes
+ while (!arrayOfContents.IsEmpty()) {
+ OwningNonNull<nsIContent>& content = arrayOfContents[0];
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ arrayOfContents.RemoveElementAt(0);
+ }
+ // Put selection in new block
+ aSelectionRanges.ClearSavedRanges();
+ nsresult rv = aSelectionRanges.Collapse(EditorRawDOMPoint(
+ unwrappedCreateNewBlockElementResult.GetNewNode(), 0u));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return Err(rv);
+ }
+ return unwrappedCreateNewBlockElementResult.UnwrapNewNode();
+ }
+
+ if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand) {
+ // Okay, now go through all the nodes and make the right kind of blocks, or
+ // whatever is appropriate.
+ // Note: blockquote is handled a little differently.
+ if (&aNewFormatTagName == nsGkAtoms::blockquote) {
+ Result<CreateElementResult, nsresult>
+ wrapContentsInBlockquoteElementsResult =
+ WrapContentsInBlockquoteElementsWithTransaction(arrayOfContents,
+ aEditingHost);
+ if (MOZ_UNLIKELY(wrapContentsInBlockquoteElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction() "
+ "failed");
+ return wrapContentsInBlockquoteElementsResult.propagateErr();
+ }
+ wrapContentsInBlockquoteElementsResult.inspect()
+ .IgnoreCaretPointSuggestion();
+ return wrapContentsInBlockquoteElementsResult.unwrap().UnwrapNewNode();
+ }
+ if (&aNewFormatTagName == nsGkAtoms::normal ||
+ &aNewFormatTagName == nsGkAtoms::_empty) {
+ Result<EditorDOMPoint, nsresult> removeBlockContainerElementsResult =
+ RemoveBlockContainerElementsWithTransaction(
+ arrayOfContents, FormatBlockMode::XULParagraphStateCommand,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed");
+ return removeBlockContainerElementsResult.propagateErr();
+ }
+ return RefPtr<Element>();
+ }
+ }
+
+ Result<CreateElementResult, nsresult> wrapContentsInBlockElementResult =
+ CreateOrChangeFormatContainerElement(arrayOfContents, aNewFormatTagName,
+ aFormatBlockMode, aEditingHost);
+ if (MOZ_UNLIKELY(wrapContentsInBlockElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::CreateOrChangeFormatContainerElement() failed");
+ return wrapContentsInBlockElementResult.propagateErr();
+ }
+ wrapContentsInBlockElementResult.inspect().IgnoreCaretPointSuggestion();
+ return wrapContentsInBlockElementResult.unwrap().UnwrapNewNode();
+}
+
+nsresult HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+
+ if (!SelectionRef().IsCollapsed()) {
+ return NS_OK;
+ }
+
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+ const RangeBoundary& atStartOfSelection = firstRange->StartRef();
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!atStartOfSelection.Container()->IsElement()) {
+ return NS_OK;
+ }
+ OwningNonNull<Element> startContainerElement =
+ *atStartOfSelection.Container()->AsElement();
+ nsresult rv =
+ InsertPaddingBRElementForEmptyLastLineIfNeeded(startContainerElement);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineIfNeeded() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::IndentAsSubAction(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eIndent, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return EditActionResult::IgnoredResult();
+ }
+
+ Result<EditActionResult, nsresult> result =
+ HandleIndentAtSelection(aEditingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::HandleIndentAtSelection() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Mutation event listener might have changed selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // TODO: Investigate when we need to put a `<br>` element after indenting
+ // ranges. Then, we could stop calling this here, or maybe we need to
+ // do it while moving content nodes.
+ nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() failed");
+ return Err(rv);
+ }
+ return result;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::IndentListChildWithTransaction(
+ RefPtr<Element>* aSubListElement, const EditorDOMPoint& aPointInListElement,
+ nsIContent& aContentMovingToSubList, const Element& aEditingHost) {
+ MOZ_ASSERT(
+ HTMLEditUtils::IsAnyListElement(aPointInListElement.GetContainer()),
+ "unexpected container");
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // some logic for putting list items into nested lists...
+
+ // If aContentMovingToSubList is followed by a sub-list element whose tag is
+ // same as the parent list element's tag, we can move it to start of the
+ // sub-list.
+ if (nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling(
+ aContentMovingToSubList, {WalkTreeOption::IgnoreWhiteSpaceOnlyText,
+ WalkTreeOption::IgnoreNonEditableNode})) {
+ if (HTMLEditUtils::IsAnyListElement(nextEditableSibling) &&
+ aPointInListElement.GetContainer()->NodeInfo()->NameAtom() ==
+ nextEditableSibling->NodeInfo()->NameAtom() &&
+ aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() ==
+ nextEditableSibling->NodeInfo()->NamespaceID()) {
+ Result<MoveNodeResult, nsresult> moveListElementResult =
+ MoveNodeWithTransaction(aContentMovingToSubList,
+ EditorDOMPoint(nextEditableSibling, 0u));
+ if (MOZ_UNLIKELY(moveListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveListElementResult.propagateErr();
+ }
+ return moveListElementResult.unwrap().UnwrapCaretPoint();
+ }
+ }
+
+ // If aContentMovingToSubList follows a sub-list element whose tag is same
+ // as the parent list element's tag, we can move it to end of the sub-list.
+ if (nsCOMPtr<nsIContent> previousEditableSibling =
+ HTMLEditUtils::GetPreviousSibling(
+ aContentMovingToSubList,
+ {WalkTreeOption::IgnoreWhiteSpaceOnlyText,
+ WalkTreeOption::IgnoreNonEditableNode})) {
+ if (HTMLEditUtils::IsAnyListElement(previousEditableSibling) &&
+ aPointInListElement.GetContainer()->NodeInfo()->NameAtom() ==
+ previousEditableSibling->NodeInfo()->NameAtom() &&
+ aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() ==
+ previousEditableSibling->NodeInfo()->NamespaceID()) {
+ Result<MoveNodeResult, nsresult> moveListElementResult =
+ MoveNodeToEndWithTransaction(aContentMovingToSubList,
+ *previousEditableSibling);
+ if (MOZ_UNLIKELY(moveListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveListElementResult.propagateErr();
+ }
+ return moveListElementResult.unwrap().UnwrapCaretPoint();
+ }
+ }
+
+ // If aContentMovingToSubList does not follow aSubListElement, we need
+ // to create new sub-list element.
+ EditorDOMPoint pointToPutCaret;
+ nsIContent* previousEditableSibling =
+ *aSubListElement ? HTMLEditUtils::GetPreviousSibling(
+ aContentMovingToSubList,
+ {WalkTreeOption::IgnoreWhiteSpaceOnlyText,
+ WalkTreeOption::IgnoreNonEditableNode})
+ : nullptr;
+ if (!*aSubListElement || (previousEditableSibling &&
+ previousEditableSibling != *aSubListElement)) {
+ nsAtom* containerName =
+ aPointInListElement.GetContainer()->NodeInfo()->NameAtom();
+ // Create a new nested list of correct type.
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ MOZ_KnownLive(*containerName), aPointInListElement,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "%s) failed",
+ nsAtomCString(containerName).get())
+ .get());
+ return createNewListElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewListElementResult =
+ createNewListElementResult.unwrap();
+ MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode());
+ pointToPutCaret = unwrappedCreateNewListElementResult.UnwrapCaretPoint();
+ *aSubListElement = unwrappedCreateNewListElementResult.UnwrapNewNode();
+ }
+
+ // Finally, we should move aContentMovingToSubList into aSubListElement.
+ const RefPtr<Element> subListElement = *aSubListElement;
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(aContentMovingToSubList, *subListElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ return pointToPutCaret;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::HandleIndentAtSelection(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ AutoRangeArray selectionRanges(SelectionRef());
+
+ if (MOZ_UNLIKELY(!selectionRanges.IsInContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (IsCSSEnabled()) {
+ nsresult rv = HandleCSSIndentAroundRanges(selectionRanges, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::HandleCSSIndentAroundRanges() failed");
+ return Err(rv);
+ }
+ } else {
+ nsresult rv = HandleHTMLIndentAroundRanges(selectionRanges, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::HandleHTMLIndentAroundRanges() failed");
+ return Err(rv);
+ }
+ }
+ rv = selectionRanges.ApplyTo(SelectionRef());
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING("AutoRangeArray::ApplyTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::ApplyTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::HandleCSSIndentAroundRanges(AutoRangeArray& aRanges,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!aRanges.Ranges().IsEmpty());
+ MOZ_ASSERT(aRanges.IsInContent());
+
+ if (aRanges.Ranges().IsEmpty()) {
+ NS_WARNING("There is no selection range");
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!aRanges.IsCollapsed() && aRanges.Ranges().Length() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ aRanges.FirstRangeRef(), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.unwrapErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use SetStartAndEnd() here.
+ nsresult rv = aRanges.SetBaseAndExtent(extendedRange.inspect().StartRef(),
+ extendedRange.inspect().EndRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed");
+ return rv;
+ }
+ }
+
+ if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+
+ // short circuit: detect case of collapsed selection inside an <li>.
+ // just sublist that <li>. This prevents bug 97797.
+
+ if (aRanges.IsCollapsed()) {
+ const auto atCaret = aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!atCaret.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atCaret.IsInContentNode());
+ Element* const editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *atCaret.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseHTMLDefaultStyle);
+ if (editableBlockElement &&
+ HTMLEditUtils::IsListItem(editableBlockElement)) {
+ arrayOfContents.AppendElement(*editableBlockElement);
+ }
+ }
+
+ EditorDOMPoint pointToPutCaret;
+ if (arrayOfContents.IsEmpty()) {
+ {
+ AutoRangeArray extendedRanges(aRanges);
+ extendedRanges.ExtendRangesToWrapLines(
+ EditSubAction::eIndent, BlockInlineCheck::UseHTMLDefaultStyle,
+ aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ nsresult rv = extendedRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eIndent,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eIndent, "
+ "CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eIndent);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eIndent) failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+ }
+
+ // If there is no visible and editable nodes in the edit targets, make an
+ // empty block.
+ // XXX Isn't this odd if there are only non-editable visible nodes?
+ if (HTMLEditUtils::IsEmptyOneHardLine(
+ arrayOfContents, BlockInlineCheck::UseHTMLDefaultStyle)) {
+ const EditorDOMPoint pointToInsertDivElement =
+ pointToPutCaret.IsSet()
+ ? std::move(pointToPutCaret)
+ : aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsertDivElement.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // make sure we can put a block here
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, pointToInsertDivElement,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ // We'll collapse ranges below, so we don't need to touch the ranges here.
+ unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion();
+ const RefPtr<Element> newDivElement =
+ unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newDivElement);
+ const Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(*newDivElement, ChangeMargin::Increase, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but "
+ "ignored");
+ }
+ // delete anything that was in the list of nodes
+ // XXX We don't need to remove the nodes from the array for performance.
+ for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // MOZ_KnownLive(content) due to bug 1622253
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+ aRanges.ClearSavedRanges();
+ nsresult rv = aRanges.Collapse(EditorDOMPoint(newDivElement, 0u));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+
+ RefPtr<Element> latestNewBlockElement;
+ auto RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside =
+ [&]() -> nsresult {
+ MOZ_ASSERT(aRanges.HasSavedRanges());
+ aRanges.RestoreFromSavedRanges();
+
+ if (!latestNewBlockElement || !aRanges.IsCollapsed() ||
+ aRanges.Ranges().IsEmpty()) {
+ return NS_OK;
+ }
+
+ const auto firstRangeStartRawPoint =
+ aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) {
+ return NS_OK;
+ }
+ Result<EditorRawDOMPoint, nsresult> pointInNewBlockElementOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(*latestNewBlockElement, firstRangeStartRawPoint);
+ if (MOZ_UNLIKELY(pointInNewBlockElementOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, "
+ "but ignored");
+ return NS_OK;
+ }
+ if (!pointInNewBlockElementOrError.inspect().IsSet()) {
+ return NS_OK;
+ }
+ return aRanges.Collapse(pointInNewBlockElementOrError.unwrap());
+ };
+
+ // Ok, now go through all the nodes and put them into sub-list element
+ // elements and new <div> elements which have start margin.
+ RefPtr<Element> subListElement, divElement;
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // Here's where we actually figure out what to do.
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ continue;
+ }
+
+ // Ignore all non-editable nodes. Leave them be.
+ // XXX We ignore non-editable nodes here, but not so in the above block.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ const RefPtr<Element> oldSubListElement = subListElement;
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ IndentListChildWithTransaction(&subListElement, atContent,
+ MOZ_KnownLive(content), aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::IndentListChildWithTransaction() failed");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ if (subListElement != oldSubListElement) {
+ // New list element is created, so we should put caret into the new list
+ // element.
+ latestNewBlockElement = subListElement;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ continue;
+ }
+
+ // Not a list item.
+
+ if (HTMLEditUtils::IsBlockElement(content,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(MOZ_KnownLive(*content->AsElement()),
+ ChangeMargin::Increase, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but "
+ "ignored");
+ } else if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ divElement = nullptr;
+ continue;
+ }
+
+ if (!divElement) {
+ // First, check that our element can contain a div.
+ if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(),
+ *nsGkAtoms::div)) {
+ // XXX This is odd, why do we stop indenting remaining content nodes?
+ // Perhaps, `continue` is better.
+ nsresult rv =
+ RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "RestoreSavedRangesAndCollapseInLatestBlockElement"
+ "IfOutside() failed");
+ return rv;
+ }
+
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ pointToPutCaret = unwrappedCreateNewDivElementResult.UnwrapCaretPoint();
+
+ MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode());
+ divElement = unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(*divElement, ChangeMargin::Increase, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but "
+ "ignored");
+ } else if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+
+ latestNewBlockElement = divElement;
+ }
+
+ // Move the content into the <div> which has start margin.
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *divElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.unwrapErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ }
+
+ nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::HandleHTMLIndentAroundRanges(AutoRangeArray& aRanges,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!aRanges.Ranges().IsEmpty());
+ MOZ_ASSERT(aRanges.IsInContent());
+
+ // XXX Why do we do this only when there is only one range?
+ if (!aRanges.IsCollapsed() && aRanges.Ranges().Length() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ aRanges.FirstRangeRef(), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.unwrapErr();
+ }
+ // Note that end point may be prior to start point. So, we cannot use
+ // SetStartAndEnd() here.
+ nsresult rv = aRanges.SetBaseAndExtent(extendedRange.inspect().StartRef(),
+ extendedRange.inspect().EndRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed");
+ return rv;
+ }
+ }
+
+ if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorDOMPoint pointToPutCaret;
+
+ // convert the selection ranges into "promoted" selection ranges:
+ // this basically just expands the range to include the immediate
+ // block parent, and then further expands to include any ancestors
+ // whose children are all in the range
+
+ // use these ranges to construct a list of nodes to act on.
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedRanges(aRanges);
+ extendedRanges.ExtendRangesToWrapLines(
+ EditSubAction::eIndent, BlockInlineCheck::UseHTMLDefaultStyle,
+ aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ nsresult rv = extendedRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eIndent,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eIndent, "
+ "CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+
+ // FIXME: Split ancestors when we consider to indent the range.
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eIndent);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::eIndent)"
+ " failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+
+ // If there is no visible and editable nodes in the edit targets, make an
+ // empty block.
+ // XXX Isn't this odd if there are only non-editable visible nodes?
+ if (HTMLEditUtils::IsEmptyOneHardLine(
+ arrayOfContents, BlockInlineCheck::UseHTMLDefaultStyle)) {
+ const EditorDOMPoint pointToInsertBlockquoteElement =
+ pointToPutCaret.IsSet()
+ ? std::move(pointToPutCaret)
+ : EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsertBlockquoteElement.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If there is no element which can have <blockquote>, abort.
+ if (NS_WARN_IF(!HTMLEditUtils::GetInsertionPointInInclusiveAncestor(
+ *nsGkAtoms::blockquote, pointToInsertBlockquoteElement,
+ &aEditingHost)
+ .IsSet())) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+
+ // Make sure we can put a block here.
+ // XXX Unfortunately, this calls
+ // MaybeSplitAncestorsForInsertWithTransaction() then,
+ // HTMLEditUtils::GetInsertionPointInInclusiveAncestor() is called again.
+ Result<CreateElementResult, nsresult> createNewBlockquoteElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::blockquote, pointToInsertBlockquoteElement,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::blockquote) failed");
+ return createNewBlockquoteElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewBlockquoteElementResult =
+ createNewBlockquoteElementResult.unwrap();
+ unwrappedCreateNewBlockquoteElementResult.IgnoreCaretPointSuggestion();
+ RefPtr<Element> newBlockquoteElement =
+ unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newBlockquoteElement);
+ // delete anything that was in the list of nodes
+ // XXX We don't need to remove the nodes from the array for performance.
+ for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+ aRanges.ClearSavedRanges();
+ nsresult rv = aRanges.Collapse(EditorRawDOMPoint(newBlockquoteElement, 0u));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ return rv;
+ }
+
+ RefPtr<Element> latestNewBlockElement;
+ auto RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside =
+ [&]() -> nsresult {
+ MOZ_ASSERT(aRanges.HasSavedRanges());
+ aRanges.RestoreFromSavedRanges();
+
+ if (!latestNewBlockElement || !aRanges.IsCollapsed() ||
+ aRanges.Ranges().IsEmpty()) {
+ return NS_OK;
+ }
+
+ const auto firstRangeStartRawPoint =
+ aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) {
+ return NS_OK;
+ }
+ Result<EditorRawDOMPoint, nsresult> pointInNewBlockElementOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(*latestNewBlockElement, firstRangeStartRawPoint);
+ if (MOZ_UNLIKELY(pointInNewBlockElementOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, "
+ "but ignored");
+ return NS_OK;
+ }
+ if (!pointInNewBlockElementOrError.inspect().IsSet()) {
+ return NS_OK;
+ }
+ return aRanges.Collapse(pointInNewBlockElementOrError.unwrap());
+ };
+
+ // Ok, now go through all the nodes and put them in a blockquote,
+ // or whatever is appropriate. Wohoo!
+ RefPtr<Element> subListElement, blockquoteElement, indentedListItemElement;
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // Here's where we actually figure out what to do.
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ continue;
+ }
+
+ // Ignore all non-editable nodes. Leave them be.
+ // XXX We ignore non-editable nodes here, but not so in the above block.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML) ||
+ !HTMLEditUtils::IsRemovableNode(content)) {
+ continue;
+ }
+
+ // If the content has been moved to different place, ignore it.
+ if (MOZ_UNLIKELY(!content->IsInclusiveDescendantOf(&aEditingHost))) {
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ const RefPtr<Element> oldSubListElement = subListElement;
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ IndentListChildWithTransaction(&subListElement, atContent,
+ MOZ_KnownLive(content), aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::IndentListChildWithTransaction() failed");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ if (oldSubListElement != subListElement) {
+ // New list element is created, so we should put caret into the new list
+ // element.
+ latestNewBlockElement = subListElement;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ blockquoteElement = nullptr;
+ continue;
+ }
+
+ // Not a list item, use blockquote?
+
+ // if we are inside a list item, we don't want to blockquote, we want
+ // to sublist the list item. We may have several nodes listed in the
+ // array of nodes to act on, that are in the same list item. Since
+ // we only want to indent that li once, we must keep track of the most
+ // recent indented list item, and not indent it if we find another node
+ // to act on that is still inside the same li.
+ if (RefPtr<Element> listItem =
+ HTMLEditUtils::GetClosestAncestorListItemElement(content,
+ &aEditingHost)) {
+ if (indentedListItemElement == listItem) {
+ // already indented this list item
+ continue;
+ }
+ // check to see if subListElement is still appropriate. Which it is if
+ // content is still right after it in the same list.
+ nsIContent* previousEditableSibling =
+ subListElement
+ ? HTMLEditUtils::GetPreviousSibling(
+ *listItem, {WalkTreeOption::IgnoreNonEditableNode})
+ : nullptr;
+ if (!subListElement || (previousEditableSibling &&
+ previousEditableSibling != subListElement)) {
+ EditorDOMPoint atListItem(listItem);
+ if (NS_WARN_IF(!listItem)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAtom* containerName =
+ atListItem.GetContainer()->NodeInfo()->NameAtom();
+ // Create a new nested list of correct type.
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ MOZ_KnownLive(*containerName), atListItem,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(nsPrintfCString("HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTr"
+ "ansaction(%s) failed",
+ nsAtomCString(containerName).get())
+ .get());
+ return createNewListElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewListElementResult =
+ createNewListElementResult.unwrap();
+ if (unwrappedCreateNewListElementResult.HasCaretPointSuggestion()) {
+ pointToPutCaret =
+ unwrappedCreateNewListElementResult.UnwrapCaretPoint();
+ }
+ MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode());
+ subListElement = unwrappedCreateNewListElementResult.UnwrapNewNode();
+ }
+
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ MoveNodeToEndWithTransaction(*listItem, *subListElement);
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveListItemElementResult.unwrapErr();
+ }
+ MoveNodeResult unwrappedMoveListItemElementResult =
+ moveListItemElementResult.unwrap();
+ if (unwrappedMoveListItemElementResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveListItemElementResult.UnwrapCaretPoint();
+ }
+
+ // Remember the list item element which we indented now for ignoring its
+ // children to avoid using <blockquote> in it.
+ indentedListItemElement = std::move(listItem);
+
+ continue;
+ }
+
+ // need to make a blockquote to put things in if we haven't already,
+ // or if this node doesn't go in blockquote we used earlier.
+ // One reason it might not go in prio blockquote is if we are now
+ // in a different table cell.
+ if (blockquoteElement &&
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ *blockquoteElement) !=
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(content)) {
+ blockquoteElement = nullptr;
+ }
+
+ if (!blockquoteElement) {
+ // First, check that our element can contain a blockquote.
+ if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(),
+ *nsGkAtoms::blockquote)) {
+ // XXX This is odd, why do we stop indenting remaining content nodes?
+ // Perhaps, `continue` is better.
+ nsresult rv =
+ RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "RestoreSavedRangesAndCollapseInLatestBlockElement"
+ "IfOutside() failed");
+ return rv;
+ }
+
+ Result<CreateElementResult, nsresult> createNewBlockquoteElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::blockquote, atContent,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::blockquote) failed");
+ return createNewBlockquoteElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewBlockquoteElementResult =
+ createNewBlockquoteElementResult.unwrap();
+ if (unwrappedCreateNewBlockquoteElementResult.HasCaretPointSuggestion()) {
+ pointToPutCaret =
+ unwrappedCreateNewBlockquoteElementResult.UnwrapCaretPoint();
+ }
+
+ MOZ_ASSERT(unwrappedCreateNewBlockquoteElementResult.GetNewNode());
+ blockquoteElement =
+ unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode();
+ latestNewBlockElement = blockquoteElement;
+ }
+
+ // tuck the node into the end of the active blockquote
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
+ *blockquoteElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.unwrapErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ subListElement = nullptr;
+ }
+
+ nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::OutdentAsSubAction(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eOutdent, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return EditActionResult::IgnoredResult();
+ }
+
+ Result<EditActionResult, nsresult> result =
+ HandleOutdentAtSelection(aEditingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::HandleOutdentAtSelection() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() "
+ "failed");
+ return Err(rv);
+ }
+ return result;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::HandleOutdentAtSelection(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ SelectionRef().GetRangeAt(0u), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use Selection::SetStartAndEndInLimit() here.
+ IgnoredErrorResult error;
+ SelectionRef().SetBaseAndExtentInLimiter(
+ extendedRange.inspect().StartRef().ToRawRangeBoundary(),
+ extendedRange.inspect().EndRef().ToRawRangeBoundary(), error);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return Err(error.StealNSResult());
+ }
+ }
+
+ // HandleOutdentAtSelectionInternal() creates AutoSelectionRestorer.
+ // Therefore, even if it returns NS_OK, the editor might have been destroyed
+ // at restoring Selection.
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ HandleOutdentAtSelectionInternal(aEditingHost);
+ MOZ_ASSERT_IF(outdentResult.isOk(),
+ !outdentResult.inspect().HasCaretPointSuggestion());
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleOutdentAtSelectionInternal() failed");
+ return outdentResult.propagateErr();
+ }
+ SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap();
+
+ // Make sure selection didn't stick to last piece of content in old bq (only
+ // a problem for collapsed selections)
+ if (!unwrappedOutdentResult.GetLeftContent() &&
+ !unwrappedOutdentResult.GetRightContent()) {
+ return EditActionResult::HandledResult();
+ }
+
+ if (!SelectionRef().IsCollapsed()) {
+ return EditActionResult::HandledResult();
+ }
+
+ // Push selection past end of left element of last split indented element.
+ if (unwrappedOutdentResult.GetLeftContent()) {
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return EditActionResult::HandledResult();
+ }
+ const RangeBoundary& atStartOfSelection = firstRange->StartRef();
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (atStartOfSelection.Container() ==
+ unwrappedOutdentResult.GetLeftContent() ||
+ EditorUtils::IsDescendantOf(*atStartOfSelection.Container(),
+ *unwrappedOutdentResult.GetLeftContent())) {
+ // Selection is inside the left node - push it past it.
+ EditorRawDOMPoint afterRememberedLeftBQ(
+ EditorRawDOMPoint::After(*unwrappedOutdentResult.GetLeftContent()));
+ NS_WARNING_ASSERTION(
+ afterRememberedLeftBQ.IsSet(),
+ "Failed to set after remembered left blockquote element");
+ nsresult rv = CollapseSelectionTo(afterRememberedLeftBQ);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+ // And pull selection before beginning of right element of last split
+ // indented element.
+ if (unwrappedOutdentResult.GetRightContent()) {
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return EditActionResult::HandledResult();
+ }
+ const RangeBoundary& atStartOfSelection = firstRange->StartRef();
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (atStartOfSelection.Container() ==
+ unwrappedOutdentResult.GetRightContent() ||
+ EditorUtils::IsDescendantOf(
+ *atStartOfSelection.Container(),
+ *unwrappedOutdentResult.GetRightContent())) {
+ // Selection is inside the right element - push it before it.
+ EditorRawDOMPoint atRememberedRightBQ(
+ unwrappedOutdentResult.GetRightContent());
+ nsresult rv = CollapseSelectionTo(atRememberedRightBQ);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+ return EditActionResult::HandledResult();
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult>
+HTMLEditor::HandleOutdentAtSelectionInternal(const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ bool useCSS = IsCSSEnabled();
+
+ // Convert the selection ranges into "promoted" selection ranges: this
+ // basically just expands the range to include the immediate block parent,
+ // and then further expands to include any ancestors whose children are all
+ // in the range
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedSelectionRanges(SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ EditSubAction::eOutdent, BlockInlineCheck::UseHTMLDefaultStyle,
+ aEditingHost);
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eOutdent,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eOutdent, "
+ "CollectNonEditableNodes::Yes) failed");
+ return Err(rv);
+ }
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eOutdent);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eOutdent) failed");
+ return splitAtBRElementsResult.propagateErr();
+ }
+ if (AllowsTransactionsToChangeSelection() &&
+ splitAtBRElementsResult.inspect().IsSet()) {
+ nsresult rv = CollapseSelectionTo(splitAtBRElementsResult.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ }
+
+ nsCOMPtr<nsIContent> leftContentOfLastOutdented;
+ nsCOMPtr<nsIContent> middleContentOfLastOutdented;
+ nsCOMPtr<nsIContent> rightContentOfLastOutdented;
+ RefPtr<Element> indentedParentElement;
+ nsCOMPtr<nsIContent> firstContentToBeOutdented, lastContentToBeOutdented;
+ BlockIndentedWith indentedParentIndentedWith = BlockIndentedWith::HTML;
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // Here's where we actually figure out what to do
+ EditorDOMPoint atContent(content);
+ if (!atContent.IsSet()) {
+ continue;
+ }
+
+ // If it's a `<blockquote>`, remove it to outdent its children.
+ if (content->IsHTMLElement(nsGkAtoms::blockquote)) {
+ // If we've already found an ancestor block element indented, we need to
+ // split it and remove the block element first.
+ if (indentedParentElement) {
+ NS_WARNING_ASSERTION(indentedParentElement == content,
+ "Indented parent element is not the <blockquote>");
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ OutdentPartOfBlock(*indentedParentElement,
+ *firstContentToBeOutdented,
+ *lastContentToBeOutdented,
+ indentedParentIndentedWith, aEditingHost);
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed");
+ return outdentResult;
+ }
+ SplitRangeOffFromNodeResult unwrappedOutdentResult =
+ outdentResult.unwrap();
+ unwrappedOutdentResult.IgnoreCaretPointSuggestion();
+ leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent();
+ middleContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapMiddleContent();
+ rightContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapRightContent();
+ indentedParentElement = nullptr;
+ firstContentToBeOutdented = nullptr;
+ lastContentToBeOutdented = nullptr;
+ indentedParentIndentedWith = BlockIndentedWith::HTML;
+ }
+ Result<EditorDOMPoint, nsresult> unwrapBlockquoteElementResult =
+ RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*content->AsElement()));
+ if (MOZ_UNLIKELY(unwrapBlockquoteElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapBlockquoteElementResult.propagateErr();
+ }
+ const EditorDOMPoint& pointToPutCaret =
+ unwrapBlockquoteElementResult.inspect();
+ if (AllowsTransactionsToChangeSelection() && pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ continue;
+ }
+
+ // If we're using CSS and the node is a block element, check its start
+ // margin whether it's indented with CSS.
+ if (useCSS && HTMLEditUtils::IsBlockElement(
+ content, BlockInlineCheck::UseHTMLDefaultStyle)) {
+ nsStaticAtom& marginProperty =
+ MarginPropertyAtomForIndent(MOZ_KnownLive(content));
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ nsAutoString value;
+ DebugOnly<nsresult> rvIgnored =
+ CSSEditUtils::GetSpecifiedProperty(content, marginProperty, value);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetSpecifiedProperty() failed, but ignored");
+ float startMargin = 0;
+ RefPtr<nsAtom> unit;
+ CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit));
+ // If indented with CSS, we should decrease the start margin.
+ if (startMargin > 0) {
+ const Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(MOZ_KnownLive(*content->AsElement()),
+ ChangeMargin::Decrease, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Decrease) failed, "
+ "but ignored");
+ } else if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaretOrError.inspect().IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ continue;
+ }
+ }
+
+ // If it's a list item, we should treat as that it "indents" its children.
+ if (HTMLEditUtils::IsListItem(content)) {
+ // If it is a list item, that means we are not outdenting whole list.
+ // XXX I don't understand this sentence... We may meet parent list
+ // element, no?
+ if (indentedParentElement) {
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ OutdentPartOfBlock(*indentedParentElement,
+ *firstContentToBeOutdented,
+ *lastContentToBeOutdented,
+ indentedParentIndentedWith, aEditingHost);
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed");
+ return outdentResult;
+ }
+ SplitRangeOffFromNodeResult unwrappedOutdentResult =
+ outdentResult.unwrap();
+ unwrappedOutdentResult.IgnoreCaretPointSuggestion();
+ leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent();
+ middleContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapMiddleContent();
+ rightContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapRightContent();
+ indentedParentElement = nullptr;
+ firstContentToBeOutdented = nullptr;
+ lastContentToBeOutdented = nullptr;
+ indentedParentIndentedWith = BlockIndentedWith::HTML;
+ }
+ // XXX `content` could become different element since
+ // `OutdentPartOfBlock()` may run mutation event listeners.
+ nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()),
+ LiftUpFromAllParentListElements::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:"
+ ":No) failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ // If we've found an ancestor block element which indents its children
+ // and the current node is NOT a descendant of it, we should remove it to
+ // outdent its children. Otherwise, i.e., current node is a descendant of
+ // it, we meet new node which should be outdented when the indented parent
+ // is removed.
+ if (indentedParentElement) {
+ if (EditorUtils::IsDescendantOf(*content, *indentedParentElement)) {
+ // Extend the range to be outdented at removing the
+ // indentedParentElement.
+ lastContentToBeOutdented = content;
+ continue;
+ }
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented,
+ *lastContentToBeOutdented,
+ indentedParentIndentedWith, aEditingHost);
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed");
+ return outdentResult;
+ }
+ SplitRangeOffFromNodeResult unwrappedOutdentResult =
+ outdentResult.unwrap();
+ unwrappedOutdentResult.IgnoreCaretPointSuggestion();
+ leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent();
+ middleContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapMiddleContent();
+ rightContentOfLastOutdented = unwrappedOutdentResult.UnwrapRightContent();
+ indentedParentElement = nullptr;
+ firstContentToBeOutdented = nullptr;
+ lastContentToBeOutdented = nullptr;
+ // curBlockIndentedWith = HTMLEditor::BlockIndentedWith::HTML;
+
+ // Then, we need to look for next indentedParentElement.
+ }
+
+ indentedParentIndentedWith = BlockIndentedWith::HTML;
+ for (nsCOMPtr<nsIContent> parentContent = content->GetParent();
+ parentContent && !parentContent->IsHTMLElement(nsGkAtoms::body) &&
+ parentContent != &aEditingHost &&
+ (parentContent->IsHTMLElement(nsGkAtoms::table) ||
+ !HTMLEditUtils::IsAnyTableElement(parentContent));
+ parentContent = parentContent->GetParent()) {
+ if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(*parentContent))) {
+ continue;
+ }
+ // If we reach a `<blockquote>` ancestor, it should be split at next
+ // time at least for outdenting current node.
+ if (parentContent->IsHTMLElement(nsGkAtoms::blockquote)) {
+ indentedParentElement = parentContent->AsElement();
+ firstContentToBeOutdented = content;
+ lastContentToBeOutdented = content;
+ break;
+ }
+
+ if (!useCSS) {
+ continue;
+ }
+
+ nsCOMPtr<nsINode> grandParentNode = parentContent->GetParentNode();
+ nsStaticAtom& marginProperty =
+ MarginPropertyAtomForIndent(MOZ_KnownLive(content));
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_WARN_IF(grandParentNode != parentContent->GetParentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ nsAutoString value;
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetSpecifiedProperty(
+ *parentContent, marginProperty, value);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetSpecifiedProperty() failed, but ignored");
+ // XXX Now, editing host may become different element. If so, shouldn't
+ // we stop this handling?
+ float startMargin;
+ RefPtr<nsAtom> unit;
+ CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit));
+ // If we reach a block element which indents its children with start
+ // margin, we should remove it at next time.
+ if (startMargin > 0 &&
+ !(HTMLEditUtils::IsAnyListElement(atContent.GetContainer()) &&
+ HTMLEditUtils::IsAnyListElement(content))) {
+ indentedParentElement = parentContent->AsElement();
+ firstContentToBeOutdented = content;
+ lastContentToBeOutdented = content;
+ indentedParentIndentedWith = BlockIndentedWith::CSS;
+ break;
+ }
+ }
+
+ if (indentedParentElement) {
+ continue;
+ }
+
+ // If we don't have any block elements which indents current node and
+ // both current node and its parent are list element, remove current
+ // node to move all its children to the parent list.
+ // XXX This is buggy. When both lists' item types are different,
+ // we create invalid tree. E.g., `<ul>` may have `<dd>` as its
+ // list item element.
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ if (!HTMLEditUtils::IsAnyListElement(content)) {
+ continue;
+ }
+ // Just unwrap this sublist
+ Result<EditorDOMPoint, nsresult> unwrapSubListElementResult =
+ RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*content->AsElement()));
+ if (MOZ_UNLIKELY(unwrapSubListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapSubListElementResult.propagateErr();
+ }
+ const EditorDOMPoint& pointToPutCaret =
+ unwrapSubListElementResult.inspect();
+ if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
+ continue;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ // If current content is a list element but its parent is not a list
+ // element, move children to where it is and remove it from the tree.
+ if (HTMLEditUtils::IsAnyListElement(content)) {
+ // XXX If mutation event listener appends new children forever, this
+ // becomes an infinite loop so that we should set limitation from
+ // first child count.
+ for (nsCOMPtr<nsIContent> lastChildContent = content->GetLastChild();
+ lastChildContent; lastChildContent = content->GetLastChild()) {
+ if (HTMLEditUtils::IsListItem(lastChildContent)) {
+ nsresult rv = LiftUpListItemElement(
+ MOZ_KnownLive(*lastChildContent->AsElement()),
+ LiftUpFromAllParentListElements::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::LiftUpListItemElement("
+ "LiftUpFromAllParentListElements::No) failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(lastChildContent)) {
+ // We have an embedded list, so move it out from under the parent
+ // list. Be sure to put it after the parent list because this
+ // loop iterates backwards through the parent's list of children.
+ EditorDOMPoint afterCurrentList(EditorDOMPoint::After(atContent));
+ NS_WARNING_ASSERTION(
+ afterCurrentList.IsSet(),
+ "Failed to set it to after current list element");
+ Result<MoveNodeResult, nsresult> moveListElementResult =
+ MoveNodeWithTransaction(*lastChildContent, afterCurrentList);
+ if (MOZ_UNLIKELY(moveListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveListElementResult.propagateErr();
+ }
+ nsresult rv = moveListElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ continue;
+ }
+
+ // Delete any non-list items for now
+ // XXX Chrome moves it from the list element. We should follow it.
+ nsresult rv = DeleteNodeWithTransaction(*lastChildContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ // Delete the now-empty list
+ Result<EditorDOMPoint, nsresult> unwrapListElementResult =
+ RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*content->AsElement()));
+ if (MOZ_UNLIKELY(unwrapListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapListElementResult.propagateErr();
+ }
+ const EditorDOMPoint& pointToPutCaret = unwrapListElementResult.inspect();
+ if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
+ continue;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ if (useCSS) {
+ if (RefPtr<Element> element = content->GetAsElementOrParentElement()) {
+ const Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(*element, ChangeMargin::Decrease, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Decrease) failed, "
+ "but ignored");
+ } else if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaretOrError.inspect().IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ }
+ continue;
+ }
+ }
+
+ if (!indentedParentElement) {
+ return SplitRangeOffFromNodeResult(leftContentOfLastOutdented,
+ middleContentOfLastOutdented,
+ rightContentOfLastOutdented);
+ }
+
+ // We have a <blockquote> we haven't finished handling.
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented,
+ *lastContentToBeOutdented, indentedParentIndentedWith,
+ aEditingHost);
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed");
+ return outdentResult;
+ }
+ // We will restore selection soon. Therefore, callers do not need to restore
+ // the selection.
+ SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap();
+ unwrappedOutdentResult.ForgetCaretPointSuggestion();
+ return unwrappedOutdentResult;
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult>
+HTMLEditor::RemoveBlockContainerElementWithTransactionBetween(
+ Element& aBlockContainerElement, nsIContent& aStartOfRange,
+ nsIContent& aEndOfRange, BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ EditorDOMPoint pointToPutCaret;
+ Result<SplitRangeOffFromNodeResult, nsresult> splitResult =
+ SplitRangeOffFromElement(aBlockContainerElement, aStartOfRange,
+ aEndOfRange);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ if (splitResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::SplitRangeOffFromElement() failed");
+ return splitResult;
+ }
+ NS_WARNING(
+ "HTMLEditor::SplitRangeOffFromElement() failed, but might be ignored");
+ return SplitRangeOffFromNodeResult(nullptr, nullptr, nullptr);
+ }
+ SplitRangeOffFromNodeResult unwrappedSplitResult = splitResult.unwrap();
+ unwrappedSplitResult.MoveCaretPointTo(pointToPutCaret,
+ {SuggestCaret::OnlyIfHasSuggestion});
+
+ // Even if either split aBlockContainerElement or did not split it, we should
+ // unwrap the right most element which is split from aBlockContainerElement
+ // (or aBlockContainerElement itself if it was not split without errors).
+ Element* rightmostElement =
+ unwrappedSplitResult.GetRightmostContentAs<Element>();
+ MOZ_ASSERT(rightmostElement);
+ if (NS_WARN_IF(!rightmostElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ {
+ // MOZ_KnownLive(rightmostElement) because it's grabbed by
+ // unwrappedSplitResult.
+ Result<EditorDOMPoint, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerWithTransaction(MOZ_KnownLive(*rightmostElement));
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ if (unwrapBlockElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapBlockElementResult.unwrap();
+ }
+ }
+
+ return SplitRangeOffFromNodeResult(
+ unwrappedSplitResult.GetLeftContent(), nullptr,
+ unwrappedSplitResult.GetRightContent(), std::move(pointToPutCaret));
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult>
+HTMLEditor::SplitRangeOffFromElement(Element& aElementToSplit,
+ nsIContent& aStartOfMiddleElement,
+ nsIContent& aEndOfMiddleElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // aStartOfMiddleElement and aEndOfMiddleElement must be exclusive
+ // descendants of aElementToSplit.
+ MOZ_ASSERT(
+ EditorUtils::IsDescendantOf(aStartOfMiddleElement, aElementToSplit));
+ MOZ_ASSERT(EditorUtils::IsDescendantOf(aEndOfMiddleElement, aElementToSplit));
+
+ EditorDOMPoint pointToPutCaret;
+ // Split at the start.
+ Result<SplitNodeResult, nsresult> splitAtStartResult =
+ SplitNodeDeepWithTransaction(aElementToSplit,
+ EditorDOMPoint(&aStartOfMiddleElement),
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitAtStartResult.isErr())) {
+ if (splitAtStartResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed (at left)");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) at start of middle element failed");
+ } else {
+ splitAtStartResult.inspect().CopyCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ // Split at after the end
+ auto atAfterEnd = EditorDOMPoint::After(aEndOfMiddleElement);
+ Element* rightElement =
+ splitAtStartResult.isOk() && splitAtStartResult.inspect().DidSplit()
+ ? splitAtStartResult.inspect().GetNextContentAs<Element>()
+ : &aElementToSplit;
+ // MOZ_KnownLive(rightElement) because it's grabbed by splitAtStartResult or
+ // aElementToSplit whose lifetime is guaranteed by the caller.
+ Result<SplitNodeResult, nsresult> splitAtEndResult =
+ SplitNodeDeepWithTransaction(MOZ_KnownLive(*rightElement), atAfterEnd,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
+ if (splitAtEndResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction() failed (at right)");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) after end of middle element failed");
+ } else {
+ splitAtEndResult.inspect().CopyCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ if (splitAtStartResult.isOk() && splitAtStartResult.inspect().DidSplit() &&
+ splitAtEndResult.isOk() && splitAtEndResult.inspect().DidSplit()) {
+ // Note that the middle node can be computed only with the latter split
+ // result.
+ return SplitRangeOffFromNodeResult(
+ splitAtStartResult.inspect().GetPreviousContent(),
+ splitAtEndResult.inspect().GetPreviousContent(),
+ splitAtEndResult.inspect().GetNextContent(),
+ std::move(pointToPutCaret));
+ }
+ if (splitAtStartResult.isOk() && splitAtStartResult.inspect().DidSplit()) {
+ return SplitRangeOffFromNodeResult(
+ splitAtStartResult.inspect().GetPreviousContent(),
+ splitAtStartResult.inspect().GetNextContent(), nullptr,
+ std::move(pointToPutCaret));
+ }
+ if (splitAtEndResult.isOk() && splitAtEndResult.inspect().DidSplit()) {
+ return SplitRangeOffFromNodeResult(
+ nullptr, splitAtEndResult.inspect().GetPreviousContent(),
+ splitAtEndResult.inspect().GetNextContent(),
+ std::move(pointToPutCaret));
+ }
+ return SplitRangeOffFromNodeResult(nullptr, &aElementToSplit, nullptr,
+ std::move(pointToPutCaret));
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult> HTMLEditor::OutdentPartOfBlock(
+ Element& aBlockElement, nsIContent& aStartOfOutdent,
+ nsIContent& aEndOfOutdent, BlockIndentedWith aBlockIndentedWith,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Result<SplitRangeOffFromNodeResult, nsresult> splitResult =
+ SplitRangeOffFromElement(aBlockElement, aStartOfOutdent, aEndOfOutdent);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitRangeOffFromElement() failed");
+ return splitResult;
+ }
+
+ SplitRangeOffFromNodeResult unwrappedSplitResult = splitResult.unwrap();
+ Element* middleElement = unwrappedSplitResult.GetMiddleContentAs<Element>();
+ if (MOZ_UNLIKELY(!middleElement)) {
+ NS_WARNING(
+ "HTMLEditor::SplitRangeOffFromElement() didn't return middle content");
+ unwrappedSplitResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*middleElement))) {
+ unwrappedSplitResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ nsresult rv = unwrappedSplitResult.SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitRangeOffFromNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "SplitRangeOffFromNodeResult::SuggestCaretPointTo() "
+ "failed, but ignored");
+
+ if (aBlockIndentedWith == BlockIndentedWith::HTML) {
+ // MOZ_KnownLive(middleElement) because of grabbed by unwrappedSplitResult.
+ Result<EditorDOMPoint, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerWithTransaction(MOZ_KnownLive(*middleElement));
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ const EditorDOMPoint& pointToPutCaret = unwrapBlockElementResult.inspect();
+ if (AllowsTransactionsToChangeSelection() && pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ return SplitRangeOffFromNodeResult(unwrappedSplitResult.GetLeftContent(),
+ nullptr,
+ unwrappedSplitResult.GetRightContent());
+ }
+
+ // MOZ_KnownLive(middleElement) because of grabbed by unwrappedSplitResult.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError = ChangeMarginStart(
+ MOZ_KnownLive(*middleElement), ChangeMargin::Decrease, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::ChangeMarginStart(ChangeMargin::Decrease) failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaretOrError.inspect().IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ return unwrappedSplitResult;
+}
+
+Result<CreateElementResult, nsresult> HTMLEditor::ChangeListElementType(
+ Element& aListElement, nsAtom& aNewListTag, nsAtom& aNewListItemTag) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ EditorDOMPoint pointToPutCaret;
+
+ AutoTArray<OwningNonNull<nsIContent>, 32> listElementChildren;
+ HTMLEditUtils::CollectAllChildren(aListElement, listElementChildren);
+
+ for (const OwningNonNull<nsIContent>& childContent : listElementChildren) {
+ if (!childContent->IsElement()) {
+ continue;
+ }
+ Element* childElement = childContent->AsElement();
+ if (HTMLEditUtils::IsListItem(childElement) &&
+ !childContent->IsHTMLElement(&aNewListItemTag)) {
+ // MOZ_KnownLive(childElement) because its lifetime is guaranteed by
+ // listElementChildren.
+ Result<CreateElementResult, nsresult>
+ replaceWithNewListItemElementResult =
+ ReplaceContainerAndCloneAttributesWithTransaction(
+ MOZ_KnownLive(*childElement), aNewListItemTag);
+ if (MOZ_UNLIKELY(replaceWithNewListItemElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction() "
+ "failed");
+ return replaceWithNewListItemElementResult;
+ }
+ CreateElementResult unwrappedReplaceWithNewListItemElementResult =
+ replaceWithNewListItemElementResult.unwrap();
+ unwrappedReplaceWithNewListItemElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ continue;
+ }
+ if (HTMLEditUtils::IsAnyListElement(childElement) &&
+ !childElement->IsHTMLElement(&aNewListTag)) {
+ // XXX List elements shouldn't have other list elements as their
+ // child. Why do we handle such invalid tree?
+ // -> Maybe, for bug 525888.
+ // MOZ_KnownLive(childElement) because its lifetime is guaranteed by
+ // listElementChildren.
+ Result<CreateElementResult, nsresult> convertListTypeResult =
+ ChangeListElementType(MOZ_KnownLive(*childElement), aNewListTag,
+ aNewListItemTag);
+ if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
+ NS_WARNING("HTMLEditor::ChangeListElementType() failed");
+ return convertListTypeResult;
+ }
+ CreateElementResult unwrappedConvertListTypeResult =
+ convertListTypeResult.unwrap();
+ unwrappedConvertListTypeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ continue;
+ }
+ }
+
+ if (aListElement.IsHTMLElement(&aNewListTag)) {
+ return CreateElementResult(&aListElement, std::move(pointToPutCaret));
+ }
+
+ // XXX If we replace the list element, shouldn't we create it first and then,
+ // move children into it before inserting the new list element into the
+ // DOM tree? Then, we could reduce the cost of dispatching DOM mutation
+ // events.
+ Result<CreateElementResult, nsresult> replaceWithNewListElementResult =
+ ReplaceContainerWithTransaction(aListElement, aNewListTag);
+ if (MOZ_UNLIKELY(replaceWithNewListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed");
+ return replaceWithNewListElementResult;
+ }
+ CreateElementResult unwrappedReplaceWithNewListElementResult =
+ replaceWithNewListElementResult.unwrap();
+ unwrappedReplaceWithNewListElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return CreateElementResult(
+ unwrappedReplaceWithNewListElementResult.UnwrapNewNode(),
+ std::move(pointToPutCaret));
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::CreateStyleForInsertText(
+ const EditorDOMPoint& aPointToInsertText, const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsertText.IsSetAndValid());
+ MOZ_ASSERT(mPendingStylesToApplyToNewContent);
+
+ const RefPtr<Element> documentRootElement = GetDocument()->GetRootElement();
+ if (NS_WARN_IF(!documentRootElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // process clearing any styles first
+ UniquePtr<PendingStyle> pendingStyle =
+ mPendingStylesToApplyToNewContent->TakeClearingStyle();
+
+ EditorDOMPoint pointToPutCaret(aPointToInsertText);
+ {
+ // Transactions may set selection, but we will set selection if necessary.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ while (pendingStyle &&
+ pointToPutCaret.GetContainer() != documentRootElement) {
+ // MOZ_KnownLive because we own pendingStyle which guarantees the lifetime
+ // of its members.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ClearStyleAt(pointToPutCaret, pendingStyle->ToInlineStyle(),
+ pendingStyle->GetSpecifiedStyle());
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::ClearStyleAt() failed");
+ return pointToPutCaretOrError;
+ }
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ if (NS_WARN_IF(!pointToPutCaret.IsSetAndValid())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ pendingStyle = mPendingStylesToApplyToNewContent->TakeClearingStyle();
+ }
+ }
+
+ // then process setting any styles
+ const int32_t relFontSize =
+ mPendingStylesToApplyToNewContent->TakeRelativeFontSize();
+ AutoTArray<EditorInlineStyleAndValue, 32> stylesToSet;
+ mPendingStylesToApplyToNewContent->TakeAllPreservedStyles(stylesToSet);
+ if (stylesToSet.IsEmpty() && !relFontSize) {
+ return pointToPutCaret;
+ }
+
+ // We're in chrome, e.g., the email composer of Thunderbird, and there is
+ // relative font size changes, we need to keep using legacy path until we port
+ // IncrementOrDecrementFontSizeAsSubAction() to work with
+ // AutoInlineStyleSetter.
+ if (relFontSize) {
+ // we have at least one style to add; make a new text node to insert style
+ // nodes above.
+ EditorDOMPoint pointToInsertTextNode(pointToPutCaret);
+ if (pointToInsertTextNode.IsInTextNode()) {
+ // if we are in a text node, split it
+ Result<SplitNodeResult, nsresult> splitTextNodeResult =
+ SplitNodeDeepWithTransaction(
+ MOZ_KnownLive(*pointToInsertTextNode.ContainerAs<Text>()),
+ pointToInsertTextNode,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eAllowToCreateEmptyContainer) failed");
+ return splitTextNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitTextNodeResult =
+ splitTextNodeResult.unwrap();
+ unwrappedSplitTextNodeResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ pointToInsertTextNode =
+ unwrappedSplitTextNodeResult.AtSplitPoint<EditorDOMPoint>();
+ }
+ if (!pointToInsertTextNode.IsInContentNode() ||
+ !HTMLEditUtils::IsContainerNode(
+ *pointToInsertTextNode.ContainerAs<nsIContent>())) {
+ return pointToPutCaret;
+ }
+ RefPtr<Text> newEmptyTextNode = CreateTextNode(u""_ns);
+ if (!newEmptyTextNode) {
+ NS_WARNING("EditorBase::CreateTextNode() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<CreateTextResult, nsresult> insertNewTextNodeResult =
+ InsertNodeWithTransaction<Text>(*newEmptyTextNode,
+ pointToInsertTextNode);
+ if (MOZ_UNLIKELY(insertNewTextNodeResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertNewTextNodeResult.propagateErr();
+ }
+ insertNewTextNodeResult.inspect().IgnoreCaretPointSuggestion();
+ pointToPutCaret.Set(newEmptyTextNode, 0u);
+
+ // FIXME: If the stylesToSet have background-color style, it may
+ // be applied shorter because outer <span> element height is not
+ // computed with inner element's height.
+ HTMLEditor::FontSize incrementOrDecrement =
+ relFontSize > 0 ? HTMLEditor::FontSize::incr
+ : HTMLEditor::FontSize::decr;
+ for ([[maybe_unused]] uint32_t j : IntegerRange(Abs(relFontSize))) {
+ Result<CreateElementResult, nsresult> wrapTextInBigOrSmallElementResult =
+ SetFontSizeOnTextNode(*newEmptyTextNode, 0, UINT32_MAX,
+ incrementOrDecrement);
+ if (MOZ_UNLIKELY(wrapTextInBigOrSmallElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed");
+ return wrapTextInBigOrSmallElementResult.propagateErr();
+ }
+ // We don't need to update here because we'll suggest caret position
+ // which is computed above.
+ MOZ_ASSERT(pointToPutCaret.IsSet());
+ wrapTextInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ for (const EditorInlineStyleAndValue& styleToSet : stylesToSet) {
+ AutoInlineStyleSetter inlineStyleSetter(styleToSet);
+ // MOZ_KnownLive(...ContainerAs<nsIContent>()) because pointToPutCaret
+ // grabs the result.
+ Result<CaretPoint, nsresult> setStyleResult =
+ inlineStyleSetter.ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
+ *this, MOZ_KnownLive(*pointToPutCaret.ContainerAs<nsIContent>()));
+ if (MOZ_UNLIKELY(setStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetInlinePropertyOnNode() failed");
+ return setStyleResult.propagateErr();
+ }
+ // We don't need to update here because we'll suggest caret position which
+ // is computed above.
+ MOZ_ASSERT(pointToPutCaret.IsSet());
+ setStyleResult.unwrap().IgnoreCaretPointSuggestion();
+ }
+ return pointToPutCaret;
+ }
+
+ // If we have preserved commands except relative font style changes, we can
+ // use inline style setting code which reuse ancestors better.
+ AutoRangeArray ranges(pointToPutCaret);
+ if (MOZ_UNLIKELY(ranges.Ranges().IsEmpty())) {
+ NS_WARNING("AutoRangeArray::AutoRangeArray() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsresult rv =
+ SetInlinePropertiesAroundRanges(ranges, stylesToSet, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetInlinePropertiesAroundRanges() failed");
+ return Err(rv);
+ }
+ if (NS_WARN_IF(ranges.Ranges().IsEmpty())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // Now `ranges` selects new styled contents and the range may not be
+ // collapsed. We should use the deepest editable start point of the range
+ // to insert text.
+ nsINode* container = ranges.FirstRangeRef()->GetStartContainer();
+ if (MOZ_UNLIKELY(!container->IsContent())) {
+ container = ranges.FirstRangeRef()->GetChildAtStartOffset();
+ if (MOZ_UNLIKELY(!container)) {
+ NS_WARNING("How did we get lost insertion point?");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ pointToPutCaret =
+ HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
+ *container->AsContent());
+ if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return pointToPutCaret;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AlignAsSubAction(
+ const nsAString& aAlignType, const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSetOrClearAlignment, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return EditActionResult::IgnoredResult();
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ AutoRangeArray selectionRanges(SelectionRef());
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!selectionRanges.IsCollapsed() &&
+ selectionRanges.Ranges().Length() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ selectionRanges.FirstRangeRef(), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use etStartAndEnd() here.
+ nsresult rv = selectionRanges.SetBaseAndExtent(
+ extendedRange.inspect().StartRef(), extendedRange.inspect().EndRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return Err(rv);
+ }
+ }
+
+ rv = AlignContentsAtRanges(selectionRanges, aAlignType, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::AlignContentsAtSelection() failed");
+ return Err(rv);
+ }
+ rv = selectionRanges.ApplyTo(SelectionRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::ApplyTo() failed");
+ return Err(rv);
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() "
+ "failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::AlignContentsAtRanges(AutoRangeArray& aRanges,
+ const nsAString& aAlignType,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(aRanges.IsInContent());
+
+ if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorDOMPoint pointToPutCaret;
+
+ // Convert the selection ranges into "promoted" selection ranges: This
+ // basically just expands the range to include the immediate block parent,
+ // and then further expands to include any ancestors whose children are all
+ // in the range
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedRanges(aRanges);
+ extendedRanges.ExtendRangesToWrapLines(
+ EditSubAction::eSetOrClearAlignment,
+ BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ nsresult rv = extendedRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eSetOrClearAlignment,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eSetOrClearAlignment, CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eSetOrClearAlignment);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eSetOrClearAlignment) failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+
+ // If we don't have any nodes, or we have only a single br, then we are
+ // creating an empty alignment div. We have to do some different things for
+ // these.
+ bool createEmptyDivElement = arrayOfContents.IsEmpty();
+ if (arrayOfContents.Length() == 1) {
+ OwningNonNull<nsIContent>& content = arrayOfContents[0];
+
+ if (HTMLEditUtils::SupportsAlignAttr(content) &&
+ HTMLEditUtils::IsBlockElement(content,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ // The node is a table element, an hr, a paragraph, a div or a section
+ // header; in HTML 4, it can directly carry the ALIGN attribute and we
+ // don't need to make a div! If we are in CSS mode, all the work is done
+ // in SetBlockElementAlign().
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ SetBlockElementAlign(MOZ_KnownLive(*content->AsElement()), aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::SetBlockElementAlign() failed");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+
+ if (content->IsHTMLElement(nsGkAtoms::br)) {
+ // The special case createEmptyDivElement code (below) that consumes
+ // `<br>` elements can cause tables to split if the start node of the
+ // selection is not in a table cell or caption, for example parent is a
+ // `<tr>`. Avoid this unnecessary splitting if possible by leaving
+ // createEmptyDivElement false so that we fall through to the normal case
+ // alignment code.
+ //
+ // XXX: It seems a little error prone for the createEmptyDivElement
+ // special case code to assume that the start node of the selection
+ // is the parent of the single node in the arrayOfContents, as the
+ // paragraph above points out. Do we rely on the selection start
+ // node because of the fact that arrayOfContents can be empty? We
+ // should probably revisit this issue. - kin
+
+ const EditorDOMPoint firstRangeStartPoint =
+ pointToPutCaret.IsSet()
+ ? pointToPutCaret
+ : aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!firstRangeStartPoint.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ nsINode* parent = firstRangeStartPoint.GetContainer();
+ createEmptyDivElement = !HTMLEditUtils::IsAnyTableElement(parent) ||
+ HTMLEditUtils::IsTableCellOrCaption(*parent);
+ }
+ }
+
+ if (createEmptyDivElement) {
+ if (MOZ_UNLIKELY(!pointToPutCaret.IsSet() && !aRanges.IsInContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ const EditorDOMPoint pointToInsertDivElement =
+ pointToPutCaret.IsSet()
+ ? pointToPutCaret
+ : aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ Result<CreateElementResult, nsresult> insertNewDivElementResult =
+ InsertDivElementToAlignContents(pointToInsertDivElement, aAlignType,
+ aEditingHost);
+ if (insertNewDivElementResult.isErr()) {
+ NS_WARNING("HTMLEditor::InsertDivElementToAlignContents() failed");
+ return insertNewDivElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInsertNewDivElementResult =
+ insertNewDivElementResult.unwrap();
+ aRanges.ClearSavedRanges();
+ EditorDOMPoint pointToPutCaret =
+ unwrappedInsertNewDivElementResult.UnwrapCaretPoint();
+ nsresult rv = aRanges.Collapse(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+
+ Result<CreateElementResult, nsresult> maybeCreateDivElementResult =
+ AlignNodesAndDescendants(arrayOfContents, aAlignType, aEditingHost);
+ if (MOZ_UNLIKELY(maybeCreateDivElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::AlignNodesAndDescendants() failed");
+ return maybeCreateDivElementResult.unwrapErr();
+ }
+ maybeCreateDivElementResult.inspect().IgnoreCaretPointSuggestion();
+
+ MOZ_ASSERT(aRanges.HasSavedRanges());
+ aRanges.RestoreFromSavedRanges();
+ // If restored range is collapsed outside the latest cased <div> element,
+ // we should move caret into the <div>.
+ if (maybeCreateDivElementResult.inspect().GetNewNode() &&
+ aRanges.IsCollapsed() && !aRanges.Ranges().IsEmpty()) {
+ const auto firstRangeStartRawPoint =
+ aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (MOZ_LIKELY(firstRangeStartRawPoint.IsSet())) {
+ Result<EditorRawDOMPoint, nsresult> pointInNewDivOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(
+ *maybeCreateDivElementResult.inspect().GetNewNode(),
+ firstRangeStartRawPoint);
+ if (MOZ_UNLIKELY(pointInNewDivOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, "
+ "but ignored");
+ } else if (pointInNewDivOrError.inspect().IsSet()) {
+ nsresult rv = aRanges.Collapse(pointInNewDivOrError.unwrap());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::InsertDivElementToAlignContents(
+ const EditorDOMPoint& aPointToInsert, const nsAString& aAlignType,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ if (NS_WARN_IF(!aPointToInsert.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, aPointToInsert, BRElementNextToSplitPoint::Delete,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div, BRElementNextToSplitPoint::Delete) failed");
+ return createNewDivElementResult;
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ // We'll suggest start of the new <div>, so we don't need the suggested
+ // position.
+ unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion();
+
+ MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode());
+ RefPtr<Element> newDivElement =
+ unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ // Set up the alignment on the div, using HTML or CSS
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ SetBlockElementAlign(*newDivElement, aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SetBlockElementAlign(EditTarget::"
+ "OnlyDescendantsExceptTable) failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ // We don't need the new suggested position too.
+
+ // Put in a padding <br> element for empty last line so that it won't get
+ // deleted.
+ {
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint(newDivElement, 0u));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
+ "failed");
+ return insertPaddingBRElementResult;
+ }
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ return CreateElementResult(std::move(newDivElement),
+ EditorDOMPoint(newDivElement, 0u));
+}
+
+Result<CreateElementResult, nsresult> HTMLEditor::AlignNodesAndDescendants(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ const nsAString& aAlignType, const Element& aEditingHost) {
+ // Detect all the transitions in the array, where a transition means that
+ // adjacent nodes in the array don't have the same parent.
+ AutoTArray<bool, 64> transitionList;
+ HTMLEditor::MakeTransitionList(aArrayOfContents, transitionList);
+
+ RefPtr<Element> latestCreatedDivElement;
+ EditorDOMPoint pointToPutCaret;
+
+ // Okay, now go through all the nodes and give them an align attrib or put
+ // them in a div, or whatever is appropriate. Woohoo!
+
+ RefPtr<Element> createdDivElement;
+ const bool useCSS = IsCSSEnabled();
+ int32_t indexOfTransitionList = -1;
+ for (OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ ++indexOfTransitionList;
+
+ // Ignore all non-editable nodes. Leave them be.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+
+ // The node is a table element, an hr, a paragraph, a div or a section
+ // header; in HTML 4, it can directly carry the ALIGN attribute and we
+ // don't need to nest it, just set the alignment. In CSS, assign the
+ // corresponding CSS styles in SetBlockElementAlign().
+ if (HTMLEditUtils::SupportsAlignAttr(content)) {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ SetBlockElementAlign(MOZ_KnownLive(*content->AsElement()), aAlignType,
+ EditTarget::NodeAndDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SetBlockElementAlign(EditTarget::"
+ "NodeAndDescendantsExceptTable) failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ // Clear out createdDivElement so that we don't put nodes after this one
+ // into it
+ createdDivElement = nullptr;
+ continue;
+ }
+
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ continue;
+ }
+
+ // Skip insignificant formatting text nodes to prevent unnecessary
+ // structure splitting!
+ if (content->IsText() &&
+ ((HTMLEditUtils::IsAnyTableElement(atContent.GetContainer()) &&
+ !HTMLEditUtils::IsTableCellOrCaption(*atContent.GetContainer())) ||
+ HTMLEditUtils::IsAnyListElement(atContent.GetContainer()) ||
+ HTMLEditUtils::IsEmptyNode(
+ *content,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible}))) {
+ continue;
+ }
+
+ // If it's a list item, or a list inside a list, forget any "current" div,
+ // and instead put divs inside the appropriate block (td, li, etc.)
+ if (HTMLEditUtils::IsListItem(content) ||
+ HTMLEditUtils::IsAnyListElement(content)) {
+ Element* listOrListItemElement = content->AsElement();
+ {
+ AutoEditorDOMPointOffsetInvalidator lockChild(atContent);
+ // MOZ_KnownLive(*listOrListItemElement): An element of aArrayOfContents
+ // which is array of OwningNonNull.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ RemoveAlignFromDescendants(MOZ_KnownLive(*listOrListItemElement),
+ aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveAlignFromDescendants(EditTarget::"
+ "OnlyDescendantsExceptTable) failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ if (NS_WARN_IF(!atContent.IsSetAndValid())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (useCSS) {
+ nsStyledElement* styledListOrListItemElement =
+ nsStyledElement::FromNode(listOrListItemElement);
+ if (styledListOrListItemElement &&
+ EditorElementStyle::Align().IsCSSSettable(
+ *styledListOrListItemElement)) {
+ // MOZ_KnownLive(*styledListOrListItemElement): An element of
+ // aArrayOfContents which is array of OwningNonNull.
+ Result<size_t, nsresult> result =
+ CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::Yes, *this,
+ MOZ_KnownLive(*styledListOrListItemElement),
+ EditorElementStyle::Align(), &aAlignType);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return result.propagateErr();
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::"
+ "Align()) failed, but ignored");
+ }
+ }
+ createdDivElement = nullptr;
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ // If we don't use CSS, add a content to list element: they have to
+ // be inside another list, i.e., >= second level of nesting.
+ // XXX AlignContentsInAllTableCellsAndListItems() handles only list
+ // item elements and table cells. Is it intentional? Why don't
+ // we need to align contents in other type blocks?
+ // MOZ_KnownLive(*listOrListItemElement): An element of aArrayOfContents
+ // which is array of OwningNonNull.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ AlignContentsInAllTableCellsAndListItems(
+ MOZ_KnownLive(*listOrListItemElement), aAlignType);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::AlignContentsInAllTableCellsAndListItems() failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ createdDivElement = nullptr;
+ continue;
+ }
+
+ // Clear out createdDivElement so that we don't put nodes after this one
+ // into it
+ }
+
+ // Need to make a div to put things in if we haven't already, or if this
+ // node doesn't go in div we used earlier.
+ if (!createdDivElement || transitionList[indexOfTransitionList]) {
+ // First, check that our element can contain a div.
+ if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(),
+ *nsGkAtoms::div)) {
+ // XXX Why do we return "OK" here rather than returning error or
+ // doing continue?
+ return latestCreatedDivElement
+ ? CreateElementResult(std::move(latestCreatedDivElement),
+ std::move(pointToPutCaret))
+ : CreateElementResult::NotHandled(
+ std::move(pointToPutCaret));
+ }
+
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult;
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ if (unwrappedCreateNewDivElementResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedCreateNewDivElementResult.UnwrapCaretPoint();
+ }
+
+ MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode());
+ createdDivElement = unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ // Set up the alignment on the div
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ SetBlockElementAlign(*createdDivElement, aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return pointToPutCaretOrError.propagateErr();
+ }
+ NS_WARNING(
+ "HTMLEditor::SetBlockElementAlign(EditTarget::"
+ "OnlyDescendantsExceptTable) failed, but ignored");
+ } else if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ latestCreatedDivElement = createdDivElement;
+ }
+
+ // Tuck the node into the end of the active div
+ //
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
+ *createdDivElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ }
+
+ return latestCreatedDivElement
+ ? CreateElementResult(std::move(latestCreatedDivElement),
+ std::move(pointToPutCaret))
+ : CreateElementResult::NotHandled(std::move(pointToPutCaret));
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::AlignContentsInAllTableCellsAndListItems(
+ Element& aElement, const nsAString& aAlignType) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Gather list of table cells or list items
+ AutoTArray<OwningNonNull<Element>, 64> arrayOfTableCellsAndListItems;
+ DOMIterator iter(aElement);
+ iter.AppendNodesToArray(
+ +[](nsINode& aNode, void*) -> bool {
+ MOZ_ASSERT(Element::FromNode(&aNode));
+ return HTMLEditUtils::IsTableCell(&aNode) ||
+ HTMLEditUtils::IsListItem(&aNode);
+ },
+ arrayOfTableCellsAndListItems);
+
+ // Now that we have the list, align their contents as requested
+ EditorDOMPoint pointToPutCaret;
+ for (auto& tableCellOrListItemElement : arrayOfTableCellsAndListItems) {
+ // MOZ_KnownLive because 'arrayOfTableCellsAndListItems' is guaranteed to
+ // keep it alive.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ AlignBlockContentsWithDivElement(
+ MOZ_KnownLive(tableCellOrListItemElement), aAlignType);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::AlignBlockContentsWithDivElement() failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::AlignBlockContentsWithDivElement(
+ Element& aBlockElement, const nsAString& aAlignType) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // XXX I don't understand why we should NOT align non-editable children
+ // with modifying EDITABLE `<div>` element.
+ nsCOMPtr<nsIContent> firstEditableContent = HTMLEditUtils::GetFirstChild(
+ aBlockElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!firstEditableContent) {
+ // This block has no editable content, nothing to align.
+ return EditorDOMPoint();
+ }
+
+ // If there is only one editable content and it's a `<div>` element,
+ // just set `align` attribute of it.
+ nsCOMPtr<nsIContent> lastEditableContent = HTMLEditUtils::GetLastChild(
+ aBlockElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (firstEditableContent == lastEditableContent &&
+ firstEditableContent->IsHTMLElement(nsGkAtoms::div)) {
+ nsresult rv = SetAttributeOrEquivalent(
+ MOZ_KnownLive(firstEditableContent->AsElement()), nsGkAtoms::align,
+ aAlignType, false);
+ if (NS_WARN_IF(Destroyed())) {
+ NS_WARNING(
+ "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align) caused "
+ "destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align) failed");
+ return Err(rv);
+ }
+ return EditorDOMPoint();
+ }
+
+ // Otherwise, we need to insert a `<div>` element to set `align` attribute.
+ // XXX Don't insert the new `<div>` element until we set `align` attribute
+ // for avoiding running mutation event listeners.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ CreateAndInsertElement(
+ WithTransaction::Yes, *nsGkAtoms::div,
+ EditorDOMPoint(&aBlockElement, 0u),
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [&aAlignType](HTMLEditor& aHTMLEditor, Element& aDivElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ MOZ_ASSERT(!aDivElement.IsInComposedDoc());
+ // If aDivElement has not been connected yet, we do not need
+ // transaction of setting align attribute here.
+ nsresult rv = aHTMLEditor.SetAttributeOrEquivalent(
+ &aDivElement, nsGkAtoms::align, aAlignType, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeOrEquivalent("
+ "nsGkAtoms::align, \"...\", false) failed");
+ return rv;
+ });
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes, "
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ EditorDOMPoint pointToPutCaret =
+ unwrappedCreateNewDivElementResult.UnwrapCaretPoint();
+ RefPtr<Element> newDivElement =
+ unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newDivElement);
+ // XXX This is tricky and does not work with mutation event listeners.
+ // But I'm not sure what we should do if new content is inserted.
+ // Anyway, I don't think that we should move editable contents
+ // over non-editable contents. Chrome does no do that.
+ while (lastEditableContent && (lastEditableContent != newDivElement)) {
+ Result<MoveNodeResult, nsresult> moveNodeResult = MoveNodeWithTransaction(
+ *lastEditableContent, EditorDOMPoint(newDivElement, 0u));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ lastEditableContent = HTMLEditUtils::GetLastChild(
+ aBlockElement, {WalkTreeOption::IgnoreNonEditableNode});
+ }
+ return pointToPutCaret;
+}
+
+Result<EditorRawDOMRange, nsresult>
+HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ const nsRange* aRange, const Element& aEditingHost) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // This tweaks selections to be more "natural".
+ // Idea here is to adjust edges of selection ranges so that they do not cross
+ // breaks or block boundaries unless something editable beyond that boundary
+ // is also selected. This adjustment makes it much easier for the various
+ // block operations to determine what nodes to act on.
+ if (NS_WARN_IF(!aRange) || NS_WARN_IF(!aRange->IsPositioned())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ const EditorRawDOMPoint startPoint(aRange->StartRef());
+ if (NS_WARN_IF(!startPoint.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorRawDOMPoint endPoint(aRange->EndRef());
+ if (NS_WARN_IF(!endPoint.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // adjusted values default to original values
+ EditorRawDOMRange newRange(startPoint, endPoint);
+
+ // Is there any intervening visible white-space? If so we can't push
+ // selection past that, it would visibly change meaning of users selection.
+ WSRunScanner wsScannerAtEnd(
+ &aEditingHost, endPoint,
+ // We should refer only the default style of HTML because we need to wrap
+ // any elements with a specific HTML element. So we should not refer
+ // actual style. For example, we want to reformat parent HTML block
+ // element even if selected in a blocked phrase element or
+ // non-HTMLelement.
+ BlockInlineCheck::UseHTMLDefaultStyle);
+ WSScanResult scanResultAtEnd =
+ wsScannerAtEnd.ScanPreviousVisibleNodeOrBlockBoundaryFrom(endPoint);
+ if (scanResultAtEnd.Failed()) {
+ NS_WARNING(
+ "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (scanResultAtEnd.ReachedSomethingNonTextContent()) {
+ // eThisBlock and eOtherBlock conveniently distinguish cases
+ // of going "down" into a block and "up" out of a block.
+ if (wsScannerAtEnd.StartsFromOtherBlockElement()) {
+ // endpoint is just after the close of a block.
+ if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
+ *wsScannerAtEnd.StartReasonOtherBlockElementPtr(),
+ {LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ newRange.SetEnd(EditorRawDOMPoint::After(*child));
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsScannerAtEnd.StartsFromCurrentBlockBoundary()) {
+ // endpoint is just after start of this block
+ if (nsIContent* child = HTMLEditUtils::GetPreviousContent(
+ endPoint, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseHTMLDefaultStyle, &aEditingHost)) {
+ newRange.SetEnd(EditorRawDOMPoint::After(*child));
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsScannerAtEnd.StartsFromBRElement()) {
+ // endpoint is just after break. lets adjust it to before it.
+ newRange.SetEnd(
+ EditorRawDOMPoint(wsScannerAtEnd.StartReasonBRElementPtr()));
+ }
+ }
+
+ // Is there any intervening visible white-space? If so we can't push
+ // selection past that, it would visibly change meaning of users selection.
+ WSRunScanner wsScannerAtStart(&aEditingHost, startPoint,
+ BlockInlineCheck::UseHTMLDefaultStyle);
+ WSScanResult scanResultAtStart =
+ wsScannerAtStart.ScanNextVisibleNodeOrBlockBoundaryFrom(startPoint);
+ if (scanResultAtStart.Failed()) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (scanResultAtStart.ReachedSomethingNonTextContent()) {
+ // eThisBlock and eOtherBlock conveniently distinguish cases
+ // of going "down" into a block and "up" out of a block.
+ if (wsScannerAtStart.EndsByOtherBlockElement()) {
+ // startpoint is just before the start of a block.
+ if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *wsScannerAtStart.EndReasonOtherBlockElementPtr(),
+ {LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ newRange.SetStart(EditorRawDOMPoint(child));
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsScannerAtStart.EndsByCurrentBlockBoundary()) {
+ // startpoint is just before end of this block
+ if (nsIContent* child = HTMLEditUtils::GetNextContent(
+ startPoint, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseHTMLDefaultStyle, &aEditingHost)) {
+ newRange.SetStart(EditorRawDOMPoint(child));
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsScannerAtStart.EndsByBRElement()) {
+ // startpoint is just before a break. lets adjust it to after it.
+ newRange.SetStart(
+ EditorRawDOMPoint::After(*wsScannerAtStart.EndReasonBRElementPtr()));
+ }
+ }
+
+ // There is a demented possibility we have to check for. We might have a very
+ // strange selection that is not collapsed and yet does not contain any
+ // editable content, and satisfies some of the above conditions that cause
+ // tweaking. In this case we don't want to tweak the selection into a block
+ // it was never in, etc. There are a variety of strategies one might use to
+ // try to detect these cases, but I think the most straightforward is to see
+ // if the adjusted locations "cross" the old values: i.e., new end before old
+ // start, or new start after old end. If so then just leave things alone.
+
+ Maybe<int32_t> comp = nsContentUtils::ComparePoints(
+ startPoint.ToRawRangeBoundary(), newRange.EndRef().ToRawRangeBoundary());
+
+ if (NS_WARN_IF(!comp)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (*comp == 1) {
+ return EditorRawDOMRange(); // New end before old start.
+ }
+
+ comp = nsContentUtils::ComparePoints(newRange.StartRef().ToRawRangeBoundary(),
+ endPoint.ToRawRangeBoundary());
+
+ if (NS_WARN_IF(!comp)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (*comp == 1) {
+ return EditorRawDOMRange(); // New start after old end.
+ }
+
+ return newRange;
+}
+
+template <typename EditorDOMRangeType>
+already_AddRefed<nsRange> HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMRangeType& aRange) {
+ MOZ_DIAGNOSTIC_ASSERT(aRange.IsPositioned());
+ return CreateRangeIncludingAdjuscentWhiteSpaces(aRange.StartRef(),
+ aRange.EndRef());
+}
+
+template <typename EditorDOMPointType1, typename EditorDOMPointType2>
+already_AddRefed<nsRange> HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMPointType1& aStartPoint,
+ const EditorDOMPointType2& aEndPoint) {
+ MOZ_DIAGNOSTIC_ASSERT(!aStartPoint.IsInNativeAnonymousSubtree());
+ MOZ_DIAGNOSTIC_ASSERT(!aEndPoint.IsInNativeAnonymousSubtree());
+
+ if (!aStartPoint.IsInContentNode() || !aEndPoint.IsInContentNode()) {
+ NS_WARNING_ASSERTION(aStartPoint.IsSet(), "aStartPoint was not set");
+ NS_WARNING_ASSERTION(aEndPoint.IsSet(), "aEndPoint was not set");
+ return nullptr;
+ }
+
+ const Element* const editingHost = ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost)) {
+ return nullptr;
+ }
+
+ EditorDOMPoint startPoint = aStartPoint.template To<EditorDOMPoint>();
+ EditorDOMPoint endPoint = aEndPoint.template To<EditorDOMPoint>();
+ AutoRangeArray::UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
+ startPoint, endPoint, *editingHost);
+
+ if (NS_WARN_IF(!startPoint.IsInContentNode()) ||
+ NS_WARN_IF(!endPoint.IsInContentNode())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement() "
+ "failed");
+ return nullptr;
+ }
+
+ // For text actions, we want to look backwards (or forwards, as
+ // appropriate) for additional white-space or nbsp's. We may have to act
+ // on these later even though they are outside of the initial selection.
+ // Even if they are in another node!
+ // XXX Those scanners do not treat siblings of the text nodes. Perhaps,
+ // we should use `WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo()`
+ // and `WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces()` instead.
+ if (startPoint.IsInTextNode()) {
+ while (!startPoint.IsStartOfContainer()) {
+ if (!startPoint.IsPreviousCharASCIISpaceOrNBSP()) {
+ break;
+ }
+ MOZ_ALWAYS_TRUE(startPoint.RewindOffset());
+ }
+ }
+ if (!startPoint.GetChildOrContainerIfDataNode() ||
+ !startPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
+ editingHost)) {
+ return nullptr;
+ }
+ if (endPoint.IsInTextNode()) {
+ while (!endPoint.IsEndOfContainer()) {
+ if (!endPoint.IsCharASCIISpaceOrNBSP()) {
+ break;
+ }
+ MOZ_ALWAYS_TRUE(endPoint.AdvanceOffset());
+ }
+ }
+ EditorDOMPoint lastRawPoint(endPoint);
+ if (!lastRawPoint.IsStartOfContainer()) {
+ lastRawPoint.RewindOffset();
+ }
+ if (!lastRawPoint.GetChildOrContainerIfDataNode() ||
+ !lastRawPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
+ editingHost)) {
+ return nullptr;
+ }
+
+ RefPtr<nsRange> range =
+ nsRange::Create(startPoint.ToRawRangeBoundary(),
+ endPoint.ToRawRangeBoundary(), IgnoreErrors());
+ NS_WARNING_ASSERTION(range, "nsRange::Create() failed");
+ return range.forget();
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::MaybeSplitElementsAtEveryBRElement(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ EditSubAction aEditSubAction) {
+ // Post-process the list to break up inline containers that contain br's, but
+ // only for operations that might care, like making lists or paragraphs
+ switch (aEditSubAction) {
+ case EditSubAction::eCreateOrRemoveBlock:
+ case EditSubAction::eFormatBlockForHTMLCommand:
+ case EditSubAction::eMergeBlockContents:
+ case EditSubAction::eCreateOrChangeList:
+ case EditSubAction::eSetOrClearAlignment:
+ case EditSubAction::eSetPositionToAbsolute:
+ case EditSubAction::eIndent:
+ case EditSubAction::eOutdent: {
+ EditorDOMPoint pointToPutCaret;
+ for (size_t index : Reversed(IntegerRange(aArrayOfContents.Length()))) {
+ OwningNonNull<nsIContent>& content = aArrayOfContents[index];
+ if (HTMLEditUtils::IsInlineContent(
+ content, BlockInlineCheck::UseHTMLDefaultStyle) &&
+ HTMLEditUtils::IsContainerNode(content) && !content->IsText()) {
+ AutoTArray<OwningNonNull<nsIContent>, 24> arrayOfInlineContents;
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
+ // alive.
+ Result<EditorDOMPoint, nsresult> splitResult =
+ SplitElementsAtEveryBRElement(MOZ_KnownLive(content),
+ arrayOfInlineContents);
+ if (splitResult.isErr()) {
+ NS_WARNING("HTMLEditor::SplitElementsAtEveryBRElement() failed");
+ return splitResult;
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ // Put these nodes in aArrayOfContents, replacing the current node
+ aArrayOfContents.RemoveElementAt(index);
+ aArrayOfContents.InsertElementsAt(index, arrayOfInlineContents);
+ }
+ }
+ return pointToPutCaret;
+ }
+ default:
+ return EditorDOMPoint();
+ }
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::SplitInlineAncestorsAtRangeBoundaries(
+ RangeItem& aRangeItem, BlockInlineCheck aBlockInlineCheck,
+ const Element& aEditingHost,
+ const nsIContent* aAncestorLimiter /* = nullptr */) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ EditorDOMPoint pointToPutCaret;
+ if (!aRangeItem.Collapsed() && aRangeItem.mEndContainer &&
+ aRangeItem.mEndContainer->IsContent()) {
+ nsCOMPtr<nsIContent> mostAncestorInlineContentAtEnd =
+ HTMLEditUtils::GetMostDistantAncestorInlineElement(
+ *aRangeItem.mEndContainer->AsContent(), aBlockInlineCheck,
+ &aEditingHost, aAncestorLimiter);
+
+ if (mostAncestorInlineContentAtEnd) {
+ Result<SplitNodeResult, nsresult> splitEndInlineResult =
+ SplitNodeDeepWithTransaction(
+ *mostAncestorInlineContentAtEnd, aRangeItem.EndPoint(),
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitEndInlineResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) failed");
+ return splitEndInlineResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitEndInlineResult =
+ splitEndInlineResult.unwrap();
+ unwrappedSplitEndInlineResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ if (pointToPutCaret.IsInContentNode() &&
+ MOZ_UNLIKELY(
+ &aEditingHost !=
+ ComputeEditingHost(*pointToPutCaret.ContainerAs<nsIContent>()))) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) caused changing editing host");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ const auto splitPointAtEnd =
+ unwrappedSplitEndInlineResult.AtSplitPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!splitPointAtEnd.IsSet())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) didn't return split point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ aRangeItem.mEndContainer = splitPointAtEnd.GetContainer();
+ aRangeItem.mEndOffset = splitPointAtEnd.Offset();
+ }
+ }
+
+ if (!aRangeItem.mStartContainer || !aRangeItem.mStartContainer->IsContent()) {
+ return pointToPutCaret;
+ }
+
+ nsCOMPtr<nsIContent> mostAncestorInlineContentAtStart =
+ HTMLEditUtils::GetMostDistantAncestorInlineElement(
+ *aRangeItem.mStartContainer->AsContent(), aBlockInlineCheck,
+ &aEditingHost, aAncestorLimiter);
+
+ if (mostAncestorInlineContentAtStart) {
+ Result<SplitNodeResult, nsresult> splitStartInlineResult =
+ SplitNodeDeepWithTransaction(*mostAncestorInlineContentAtStart,
+ aRangeItem.StartPoint(),
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitStartInlineResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) failed");
+ return splitStartInlineResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitStartInlineResult =
+ splitStartInlineResult.unwrap();
+ // XXX Why don't we check editing host like above??
+ unwrappedSplitStartInlineResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ // XXX If we split only here because of collapsed range, we're modifying
+ // only start point of aRangeItem. Shouldn't we modify end point here
+ // if it's collapsed?
+ const auto splitPointAtStart =
+ unwrappedSplitStartInlineResult.AtSplitPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!splitPointAtStart.IsSet())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) didn't return split point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ aRangeItem.mStartContainer = splitPointAtStart.GetContainer();
+ aRangeItem.mStartOffset = splitPointAtStart.Offset();
+ }
+
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::SplitElementsAtEveryBRElement(
+ nsIContent& aMostAncestorToBeSplit,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // First build up a list of all the break nodes inside the inline container.
+ AutoTArray<OwningNonNull<HTMLBRElement>, 24> arrayOfBRElements;
+ DOMIterator iter(aMostAncestorToBeSplit);
+ iter.AppendAllNodesToArray(arrayOfBRElements);
+
+ // If there aren't any breaks, just put inNode itself in the array
+ if (arrayOfBRElements.IsEmpty()) {
+ aOutArrayOfContents.AppendElement(aMostAncestorToBeSplit);
+ return EditorDOMPoint();
+ }
+
+ // Else we need to bust up aMostAncestorToBeSplit along all the breaks
+ nsCOMPtr<nsIContent> nextContent = &aMostAncestorToBeSplit;
+ EditorDOMPoint pointToPutCaret;
+ for (OwningNonNull<HTMLBRElement>& brElement : arrayOfBRElements) {
+ EditorDOMPoint atBRNode(brElement);
+ if (NS_WARN_IF(!atBRNode.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitNodeDeepWithTransaction(
+ *nextContent, atBRNode, SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ unwrappedSplitNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ // Put previous node at the split point.
+ if (nsIContent* previousContent =
+ unwrappedSplitNodeResult.GetPreviousContent()) {
+ // Might not be a left node. A break might have been at the very
+ // beginning of inline container, in which case
+ // SplitNodeDeepWithTransaction() would not actually split anything.
+ aOutArrayOfContents.AppendElement(*previousContent);
+ }
+
+ // Move break outside of container and also put in node list
+ // MOZ_KnownLive because 'arrayOfBRElements' is guaranteed to keep it alive.
+ Result<MoveNodeResult, nsresult> moveBRElementResult =
+ MoveNodeWithTransaction(
+ MOZ_KnownLive(brElement),
+ unwrappedSplitNodeResult.AtNextContent<EditorDOMPoint>());
+ if (MOZ_UNLIKELY(moveBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveBRElementResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveBRElementResult = moveBRElementResult.unwrap();
+ unwrappedMoveBRElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ aOutArrayOfContents.AppendElement(brElement);
+
+ nextContent = unwrappedSplitNodeResult.GetNextContent();
+ }
+
+ // Now tack on remaining next node.
+ aOutArrayOfContents.AppendElement(*nextContent);
+
+ return pointToPutCaret;
+}
+
+// static
+void HTMLEditor::MakeTransitionList(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ nsTArray<bool>& aTransitionArray) {
+ nsINode* prevParent = nullptr;
+ aTransitionArray.EnsureLengthAtLeast(aArrayOfContents.Length());
+ for (uint32_t i = 0; i < aArrayOfContents.Length(); i++) {
+ aTransitionArray[i] = aArrayOfContents[i]->GetParentNode() != prevParent;
+ prevParent = aArrayOfContents[i]->GetParentNode();
+ }
+}
+
+Result<InsertParagraphResult, nsresult>
+HTMLEditor::HandleInsertParagraphInHeadingElement(
+ Element& aHeadingElement, const EditorDOMPoint& aPointToSplit) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ auto splitHeadingResult =
+ [this, &aPointToSplit, &aHeadingElement]()
+ MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
+ // Normalize collapsible white-spaces around the split point to keep
+ // them visible after the split. Note that this does not touch
+ // selection because of using AutoTransactionsConserveSelection in
+ // WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes().
+ Result<EditorDOMPoint, nsresult> preparationResult =
+ WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ *this, aPointToSplit, aHeadingElement);
+ if (MOZ_UNLIKELY(preparationResult.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement() "
+ "failed");
+ return preparationResult.propagateErr();
+ }
+ EditorDOMPoint pointToSplit = preparationResult.unwrap();
+ MOZ_ASSERT(pointToSplit.IsInContentNode());
+
+ // Split the header
+ Result<SplitNodeResult, nsresult> splitResult =
+ SplitNodeDeepWithTransaction(
+ aHeadingElement, pointToSplit,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ NS_WARNING_ASSERTION(
+ splitResult.isOk(),
+ "HTMLEditor::SplitNodeDeepWithTransaction(aHeadingElement, "
+ "SplitAtEdges::eAllowToCreateEmptyContainer) failed");
+ return splitResult;
+ }();
+ if (MOZ_UNLIKELY(splitHeadingResult.isErr())) {
+ NS_WARNING("Failed to splitting aHeadingElement");
+ return splitHeadingResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitHeadingResult = splitHeadingResult.unwrap();
+ unwrappedSplitHeadingResult.IgnoreCaretPointSuggestion();
+ if (MOZ_UNLIKELY(!unwrappedSplitHeadingResult.DidSplit())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eAllowToCreateEmptyContainer) didn't split aHeadingElement");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // If the left heading element is empty, put a padding <br> element for empty
+ // last line into it.
+ // FYI: leftHeadingElement is grabbed by unwrappedSplitHeadingResult so that
+ // it's safe to access anytime.
+ auto* const leftHeadingElement =
+ unwrappedSplitHeadingResult.GetPreviousContentAs<Element>();
+ MOZ_ASSERT(leftHeadingElement,
+ "SplitNodeResult::GetPreviousContent() should return something if "
+ "DidSplit() returns true");
+ MOZ_DIAGNOSTIC_ASSERT(HTMLEditUtils::IsHeader(*leftHeadingElement));
+ if (HTMLEditUtils::IsEmptyNode(
+ *leftHeadingElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint(leftHeadingElement, 0u));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction("
+ ") failed");
+ return insertPaddingBRElementResult.propagateErr();
+ }
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ // Put caret at start of the right head element if it's not empty.
+ auto* const rightHeadingElement =
+ unwrappedSplitHeadingResult.GetNextContentAs<Element>();
+ MOZ_ASSERT(rightHeadingElement,
+ "SplitNodeResult::GetNextContent() should return something if "
+ "DidSplit() returns true");
+ if (!HTMLEditUtils::IsEmptyBlockElement(
+ *rightHeadingElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ return InsertParagraphResult(rightHeadingElement,
+ EditorDOMPoint(rightHeadingElement, 0u));
+ }
+
+ // If the right heading element is empty, delete it.
+ // TODO: If we know the new heading element becomes empty, we stop spliting
+ // the heading element.
+ // MOZ_KnownLive(rightHeadingElement) because it's grabbed by
+ // unwrappedSplitHeadingResult.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*rightHeadingElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+
+ // Layout tells the caret to blink in a weird place if we don't place a
+ // break after the header.
+ // XXX This block is dead code unless the removed right heading element is
+ // reconnected by a mutation event listener. This is a regression of
+ // bug 1405751:
+ // https://searchfox.org/mozilla-central/diff/879f3317d1331818718e18776caa47be7f426a22/editor/libeditor/HTMLEditRules.cpp#6389
+ // However, the traditional behavior is different from the other browsers.
+ // Chrome creates new paragraph in this case. Therefore, we should just
+ // drop this block in a follow up bug.
+ if (rightHeadingElement->GetNextSibling()) {
+ // XXX Ignoring non-editable <br> element here is odd because non-editable
+ // <br> elements also work as <br> from point of view of layout.
+ nsIContent* nextEditableSibling =
+ HTMLEditUtils::GetNextSibling(*rightHeadingElement->GetNextSibling(),
+ {WalkTreeOption::IgnoreNonEditableNode});
+ if (nextEditableSibling &&
+ nextEditableSibling->IsHTMLElement(nsGkAtoms::br)) {
+ auto afterEditableBRElement = EditorDOMPoint::After(*nextEditableSibling);
+ if (NS_WARN_IF(!afterEditableBRElement.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // Put caret at the <br> element.
+ return InsertParagraphResult::NotHandled(
+ std::move(afterEditableBRElement));
+ }
+ }
+
+ if (MOZ_UNLIKELY(!leftHeadingElement->IsInComposedDoc())) {
+ NS_WARNING("The left heading element was unexpectedly removed");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
+ mPendingStylesToApplyToNewContent->ClearAllStyles();
+
+ // Create a paragraph if the right heading element is not followed by an
+ // editable <br> element.
+ nsStaticAtom& newParagraphTagName =
+ &DefaultParagraphSeparatorTagName() == nsGkAtoms::br
+ ? *nsGkAtoms::p
+ : DefaultParagraphSeparatorTagName();
+ // We want a wrapper element even if we separate with a <br>.
+ // MOZ_KnownLive(newParagraphTagName) because it's available until shutdown.
+ Result<CreateElementResult, nsresult> createNewParagraphElementResult =
+ CreateAndInsertElement(WithTransaction::Yes,
+ MOZ_KnownLive(newParagraphTagName),
+ EditorDOMPoint::After(*leftHeadingElement),
+ HTMLEditor::InsertNewBRElement);
+ if (MOZ_UNLIKELY(createNewParagraphElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewParagraphElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewParagraphElementResult =
+ createNewParagraphElementResult.unwrap();
+ // Put caret at the <br> element in the following paragraph.
+ unwrappedCreateNewParagraphElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedCreateNewParagraphElementResult.GetNewNode());
+ EditorDOMPoint pointToPutCaret(
+ unwrappedCreateNewParagraphElementResult.GetNewNode(), 0u);
+ return InsertParagraphResult(
+ unwrappedCreateNewParagraphElementResult.UnwrapNewNode(),
+ std::move(pointToPutCaret));
+}
+
+Result<SplitNodeResult, nsresult> HTMLEditor::HandleInsertParagraphInParagraph(
+ Element& aParentDivOrP, const EditorDOMPoint& aCandidatePointToSplit,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aCandidatePointToSplit.IsSetAndValid());
+
+ // First, get a better split point to avoid to create a new empty link in the
+ // right paragraph.
+ EditorDOMPoint pointToSplit = [&]() {
+ // We shouldn't create new anchor element which has non-empty href unless
+ // splitting middle of it because we assume that users don't want to create
+ // *same* anchor element across two or more paragraphs in most cases.
+ // So, adjust selection start if it's edge of anchor element(s).
+ // XXX We don't support white-space collapsing in these cases since it needs
+ // some additional work with WhiteSpaceVisibilityKeeper but it's not
+ // usual case. E.g., |<a href="foo"><b>foo []</b> </a>|
+ if (aCandidatePointToSplit.IsStartOfContainer()) {
+ EditorDOMPoint candidatePoint(aCandidatePointToSplit);
+ for (nsIContent* container =
+ aCandidatePointToSplit.GetContainerAs<nsIContent>();
+ container && container != &aParentDivOrP;
+ container = container->GetParent()) {
+ if (HTMLEditUtils::IsLink(container)) {
+ // Found link should be only in right node. So, we shouldn't split
+ // it.
+ candidatePoint.Set(container);
+ // Even if we found an anchor element, don't break because DOM API
+ // allows to nest anchor elements.
+ }
+ // If the container is middle of its parent, stop adjusting split point.
+ if (container->GetPreviousSibling()) {
+ // XXX Should we check if previous sibling is visible content?
+ // E.g., should we ignore comment node, invisible <br> element?
+ break;
+ }
+ }
+ return candidatePoint;
+ }
+
+ // We also need to check if selection is at invisible <br> element at end
+ // of an <a href="foo"> element because editor inserts a <br> element when
+ // user types Enter key after a white-space which is at middle of
+ // <a href="foo"> element and when setting selection at end of the element,
+ // selection becomes referring the <br> element. We may need to change this
+ // behavior later if it'd be standardized.
+ if (aCandidatePointToSplit.IsEndOfContainer() ||
+ aCandidatePointToSplit.IsBRElementAtEndOfContainer()) {
+ // If there are 2 <br> elements, the first <br> element is visible. E.g.,
+ // |<a href="foo"><b>boo[]<br></b><br></a>|, we should split the <a>
+ // element. Otherwise, E.g., |<a href="foo"><b>boo[]<br></b></a>|,
+ // we should not split the <a> element and ignore inline elements in it.
+ bool foundBRElement =
+ aCandidatePointToSplit.IsBRElementAtEndOfContainer();
+ EditorDOMPoint candidatePoint(aCandidatePointToSplit);
+ for (nsIContent* container =
+ aCandidatePointToSplit.GetContainerAs<nsIContent>();
+ container && container != &aParentDivOrP;
+ container = container->GetParent()) {
+ if (HTMLEditUtils::IsLink(container)) {
+ // Found link should be only in left node. So, we shouldn't split it.
+ candidatePoint.SetAfter(container);
+ // Even if we found an anchor element, don't break because DOM API
+ // allows to nest anchor elements.
+ }
+ // If the container is middle of its parent, stop adjusting split point.
+ if (nsIContent* nextSibling = container->GetNextSibling()) {
+ if (foundBRElement) {
+ // If we've already found a <br> element, we assume found node is
+ // visible <br> or something other node.
+ // XXX Should we check if non-text data node like comment?
+ break;
+ }
+
+ // XXX Should we check if non-text data node like comment?
+ if (!nextSibling->IsHTMLElement(nsGkAtoms::br)) {
+ break;
+ }
+ foundBRElement = true;
+ }
+ }
+ return candidatePoint;
+ }
+ return aCandidatePointToSplit;
+ }();
+
+ const bool createNewParagraph = GetReturnInParagraphCreatesNewParagraph();
+ RefPtr<HTMLBRElement> brElement;
+ if (createNewParagraph && pointToSplit.GetContainer() == &aParentDivOrP) {
+ // We are try to split only the current paragraph. Therefore, we don't need
+ // to create new <br> elements around it (if left and/or right paragraph
+ // becomes empty, it'll be treated by SplitParagraphWithTransaction().
+ brElement = nullptr;
+ } else if (pointToSplit.IsInTextNode()) {
+ if (pointToSplit.IsStartOfContainer()) {
+ // If we're splitting the paragraph at start of a text node and there is
+ // no preceding visible <br> element, we need to create a <br> element to
+ // keep the inline elements containing this text node.
+ // TODO: If the parent of the text node is the splitting paragraph,
+ // obviously we don't need to do this because empty paragraphs will
+ // be treated by SplitParagraphWithTransaction(). In this case, we
+ // just need to update pointToSplit for using the same path as the
+ // previous `if` block.
+ brElement =
+ HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetPreviousSibling(
+ *pointToSplit.ContainerAs<Text>(),
+ {WalkTreeOption::IgnoreNonEditableNode}));
+ if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) ||
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ // If insertParagraph does not create a new paragraph, default to
+ // insertLineBreak.
+ if (!createNewParagraph) {
+ return SplitNodeResult::NotHandled(pointToSplit);
+ }
+ const EditorDOMPoint pointToInsertBR = pointToSplit.ParentPoint();
+ MOZ_ASSERT(pointToInsertBR.IsSet());
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToInsertBR);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ brElement = HTMLBRElement::FromNodeOrNull(
+ insertBRElementResult.inspect().GetNewNode());
+ }
+ } else if (pointToSplit.IsEndOfContainer()) {
+ // If we're splitting the paragraph at end of a text node and there is not
+ // following visible <br> element, we need to create a <br> element after
+ // the text node to make current style specified by parent inline elements
+ // keep in the right paragraph.
+ // TODO: Same as above, we don't need to do this if the text node is a
+ // direct child of the paragraph. For using the simplest path, we
+ // just need to update `pointToSplit` in the case.
+ brElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetNextSibling(
+ *pointToSplit.ContainerAs<Text>(),
+ {WalkTreeOption::IgnoreNonEditableNode}));
+ if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) ||
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ // If insertParagraph does not create a new paragraph, default to
+ // insertLineBreak.
+ if (!createNewParagraph) {
+ return SplitNodeResult::NotHandled(pointToSplit);
+ }
+ const auto pointToInsertBR =
+ EditorDOMPoint::After(*pointToSplit.ContainerAs<Text>());
+ MOZ_ASSERT(pointToInsertBR.IsSet());
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToInsertBR);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ brElement = HTMLBRElement::FromNodeOrNull(
+ insertBRElementResult.inspect().GetNewNode());
+ }
+ } else {
+ // If insertParagraph does not create a new paragraph, default to
+ // insertLineBreak.
+ if (!createNewParagraph) {
+ return SplitNodeResult::NotHandled(pointToSplit);
+ }
+
+ // If we're splitting the paragraph at middle of a text node, we should
+ // split the text node here and put a <br> element next to the left text
+ // node.
+ // XXX Why? I think that this should be handled in
+ // SplitParagraphWithTransaction() directly because I don't find
+ // the necessary case of the <br> element.
+
+ // XXX We split a text node here if caret is middle of it to insert
+ // <br> element **before** splitting aParentDivOrP. Then, if
+ // the <br> element becomes unnecessary, it'll be removed again.
+ // So this does much more complicated things than what we want to
+ // do here. We should handle this case separately to make the code
+ // much simpler.
+
+ // Normalize collapsible white-spaces around the split point to keep
+ // them visible after the split. Note that this does not touch
+ // selection because of using AutoTransactionsConserveSelection in
+ // WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes().
+ Result<EditorDOMPoint, nsresult> pointToSplitOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ *this, pointToSplit, aParentDivOrP);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(pointToSplitOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement() "
+ "failed");
+ return pointToSplitOrError.propagateErr();
+ }
+ MOZ_ASSERT(pointToSplitOrError.inspect().IsSetAndValid());
+ if (pointToSplitOrError.inspect().IsSet()) {
+ pointToSplit = pointToSplitOrError.unwrap();
+ }
+ Result<SplitNodeResult, nsresult> splitParentDivOrPResult =
+ SplitNodeWithTransaction(pointToSplit);
+ if (MOZ_UNLIKELY(splitParentDivOrPResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitParentDivOrPResult;
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ splitParentDivOrPResult.inspect().IgnoreCaretPointSuggestion();
+
+ pointToSplit.SetToEndOf(
+ splitParentDivOrPResult.inspect().GetPreviousContent());
+ if (NS_WARN_IF(!pointToSplit.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // We need to put new <br> after the left node if given node was split
+ // above.
+ const auto pointToInsertBR =
+ EditorDOMPoint::After(*pointToSplit.ContainerAs<nsIContent>());
+ MOZ_ASSERT(pointToInsertBR.IsSet());
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToInsertBR);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ brElement = HTMLBRElement::FromNodeOrNull(
+ insertBRElementResult.inspect().GetNewNode());
+ }
+ } else {
+ // If we're splitting in a child element of the paragraph, and there is no
+ // <br> element around it, we should insert a <br> element at the split
+ // point and keep splitting the paragraph after the new <br> element.
+ // XXX Why? We probably need to do this if we're splitting in an inline
+ // element which and whose parents provide some styles, we should put
+ // the <br> element for making a placeholder in the left paragraph for
+ // moving to the caret, but I think that this could be handled in fewer
+ // cases than this.
+ brElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetPreviousContent(
+ pointToSplit, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, &aEditingHost));
+ if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) ||
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ // is there a BR after it?
+ brElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetNextContent(
+ pointToSplit, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, &aEditingHost));
+ if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) ||
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ // If insertParagraph does not create a new paragraph, default to
+ // insertLineBreak.
+ if (!createNewParagraph) {
+ return SplitNodeResult::NotHandled(pointToSplit);
+ }
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToSplit);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ brElement = HTMLBRElement::FromNodeOrNull(
+ insertBRElementResult.inspect().GetNewNode());
+ // We split the parent after the <br>.
+ pointToSplit.SetAfter(brElement);
+ if (NS_WARN_IF(!pointToSplit.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+ }
+ }
+
+ Result<SplitNodeResult, nsresult> splitParagraphResult =
+ SplitParagraphWithTransaction(aParentDivOrP, pointToSplit, brElement);
+ if (MOZ_UNLIKELY(splitParagraphResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitParagraphWithTransaction() failed");
+ return splitParagraphResult;
+ }
+ if (MOZ_UNLIKELY(!splitParagraphResult.inspect().DidSplit())) {
+ NS_WARNING(
+ "HTMLEditor::SplitParagraphWithTransaction() didn't split the "
+ "paragraph");
+ splitParagraphResult.inspect().IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_FAILURE);
+ }
+ MOZ_ASSERT(splitParagraphResult.inspect().Handled());
+ return splitParagraphResult;
+}
+
+Result<SplitNodeResult, nsresult> HTMLEditor::SplitParagraphWithTransaction(
+ Element& aParentDivOrP, const EditorDOMPoint& aStartOfRightNode,
+ HTMLBRElement* aMayBecomeVisibleBRElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Result<EditorDOMPoint, nsresult> preparationResult =
+ WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ *this, aStartOfRightNode, aParentDivOrP);
+ if (MOZ_UNLIKELY(preparationResult.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement() failed");
+ return preparationResult.propagateErr();
+ }
+ EditorDOMPoint pointToSplit = preparationResult.unwrap();
+ MOZ_ASSERT(pointToSplit.IsInContentNode());
+
+ // Split the paragraph.
+ Result<SplitNodeResult, nsresult> splitDivOrPResult =
+ SplitNodeDeepWithTransaction(aParentDivOrP, pointToSplit,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitDivOrPResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitDivOrPResult;
+ }
+ SplitNodeResult unwrappedSplitDivOrPResult = splitDivOrPResult.unwrap();
+ if (MOZ_UNLIKELY(!unwrappedSplitDivOrPResult.DidSplit())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction() didn't split any nodes");
+ return unwrappedSplitDivOrPResult;
+ }
+
+ // We'll compute caret suggestion later. So the simple result is not needed.
+ unwrappedSplitDivOrPResult.IgnoreCaretPointSuggestion();
+
+ auto* const leftDivOrParagraphElement =
+ unwrappedSplitDivOrPResult.GetPreviousContentAs<Element>();
+ MOZ_ASSERT(leftDivOrParagraphElement,
+ "SplitNodeResult::GetPreviousContent() should return something if "
+ "DidSplit() returns true");
+ auto* const rightDivOrParagraphElement =
+ unwrappedSplitDivOrPResult.GetNextContentAs<Element>();
+ MOZ_ASSERT(rightDivOrParagraphElement,
+ "SplitNodeResult::GetNextContent() should return something if "
+ "DidSplit() returns true");
+
+ // Get rid of the break, if it is visible (otherwise it may be needed to
+ // prevent an empty p).
+ if (aMayBecomeVisibleBRElement &&
+ HTMLEditUtils::IsVisibleBRElement(*aMayBecomeVisibleBRElement)) {
+ nsresult rv = DeleteNodeWithTransaction(*aMayBecomeVisibleBRElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ // Remove ID attribute on the paragraph from the right node.
+ // MOZ_KnownLive(rightDivOrParagraphElement) because it's grabbed by
+ // unwrappedSplitDivOrPResult.
+ nsresult rv = RemoveAttributeWithTransaction(
+ MOZ_KnownLive(*rightDivOrParagraphElement), *nsGkAtoms::id);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::id) failed");
+ return Err(rv);
+ }
+
+ // We need to ensure to both paragraphs visible even if they are empty.
+ // However, padding <br> element for empty last line isn't useful in this
+ // case because it'll be ignored by PlaintextSerializer. Additionally,
+ // it'll be exposed as <br> with Element.innerHTML. Therefore, we can use
+ // normal <br> elements for placeholder in this case. Note that Chromium
+ // also behaves so.
+ auto InsertBRElementIfEmptyBlockElement =
+ [&](Element& aElement) MOZ_CAN_RUN_SCRIPT {
+ if (!HTMLEditUtils::IsBlockElement(
+ aElement, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return NS_OK;
+ }
+
+ if (!HTMLEditUtils::IsEmptyNode(
+ aElement, {EmptyCheckOption::TreatSingleBRElementAsVisible})) {
+ return NS_OK;
+ }
+
+ // XXX: Probably, we should use
+ // InsertPaddingBRElementForEmptyLastLineWithTransaction here, and
+ // if there are some empty inline container, we should put the <br>
+ // into the last one.
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes,
+ EditorDOMPoint(&aElement, 0u));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ // After this is called twice, we'll compute new caret position.
+ // Therefore, we don't need to update selection here.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+ };
+
+ // MOZ_KnownLive(leftDivOrParagraphElement) because it's grabbed by
+ // splitDivOrResult.
+ rv = InsertBRElementIfEmptyBlockElement(
+ MOZ_KnownLive(*leftDivOrParagraphElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "InsertBRElementIfEmptyBlockElement(leftDivOrParagraphElement) failed");
+ return Err(rv);
+ }
+
+ if (HTMLEditUtils::IsEmptyNode(*rightDivOrParagraphElement)) {
+ // If the right paragraph is empty, it might have an empty inline element
+ // (which may contain other empty inline containers) and optionally a <br>
+ // element which may not be in the deepest inline element.
+ const RefPtr<Element> deepestInlineContainerElement =
+ [](const Element& aBlockElement) {
+ Element* result = nullptr;
+ for (Element* maybeDeepestInlineContainer =
+ Element::FromNodeOrNull(aBlockElement.GetFirstChild());
+ maybeDeepestInlineContainer &&
+ HTMLEditUtils::IsInlineContent(
+ *maybeDeepestInlineContainer,
+ BlockInlineCheck::UseComputedDisplayStyle) &&
+ HTMLEditUtils::IsContainerNode(*maybeDeepestInlineContainer);
+ maybeDeepestInlineContainer =
+ maybeDeepestInlineContainer->GetFirstElementChild()) {
+ result = maybeDeepestInlineContainer;
+ }
+ return result;
+ }(*rightDivOrParagraphElement);
+ if (deepestInlineContainerElement) {
+ RefPtr<HTMLBRElement> brElement =
+ HTMLEditUtils::GetFirstBRElement(*rightDivOrParagraphElement);
+ // If there is a <br> element and it is in the deepest inline container,
+ // we need to do nothing anymore. Let's suggest caret position as at the
+ // <br>.
+ if (brElement &&
+ brElement->GetParentNode() == deepestInlineContainerElement) {
+ brElement->SetFlags(NS_PADDING_FOR_EMPTY_LAST_LINE);
+ return SplitNodeResult(std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(brElement));
+ }
+ // Otherwise, we should put a padding <br> element into the deepest inline
+ // container and then, existing <br> element (if there is) becomes
+ // unnecessary.
+ if (brElement) {
+ nsresult rv = DeleteNodeWithTransaction(*brElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint::AtEndOf(deepestInlineContainerElement));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertPaddingBRElementForEmptyLastLineWithTransaction() failed");
+ return insertPaddingBRElementResult.propagateErr();
+ }
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ return SplitNodeResult(
+ std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(insertPaddingBRElementResult.inspect().GetNewNode()));
+ }
+
+ // If there is no inline container elements, we just need to make the
+ // right paragraph visible.
+ nsresult rv = InsertBRElementIfEmptyBlockElement(
+ MOZ_KnownLive(*rightDivOrParagraphElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "InsertBRElementIfEmptyBlockElement(rightDivOrParagraphElement) "
+ "failed");
+ return Err(rv);
+ }
+ }
+
+ // Let's put caret at start of the first leaf container.
+ nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *rightDivOrParagraphElement, {LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(!child)) {
+ return SplitNodeResult(std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(rightDivOrParagraphElement, 0u));
+ }
+ return child->IsText() || HTMLEditUtils::IsContainerNode(*child)
+ ? SplitNodeResult(std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(child, 0u))
+ : SplitNodeResult(std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(child));
+}
+
+Result<InsertParagraphResult, nsresult>
+HTMLEditor::HandleInsertParagraphInListItemElement(
+ Element& aListItemElement, const EditorDOMPoint& aPointToSplit,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(&aListItemElement));
+
+ // If aListItemElement is empty, then we want to outdent its content.
+ if (&aEditingHost != aListItemElement.GetParentElement() &&
+ HTMLEditUtils::IsEmptyBlockElement(
+ aListItemElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ RefPtr<Element> leftListElement = aListItemElement.GetParentElement();
+ // If the given list item element is not the last list item element of
+ // its parent nor not followed by sub list elements, split the parent
+ // before it.
+ if (!HTMLEditUtils::IsLastChild(aListItemElement,
+ {WalkTreeOption::IgnoreNonEditableNode})) {
+ Result<SplitNodeResult, nsresult> splitListItemParentResult =
+ SplitNodeWithTransaction(EditorDOMPoint(&aListItemElement));
+ if (MOZ_UNLIKELY(splitListItemParentResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitListItemParentResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitListItemParentResult =
+ splitListItemParentResult.unwrap();
+ if (MOZ_UNLIKELY(!unwrappedSplitListItemParentResult.DidSplit())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeWithTransaction() didn't split the parent of "
+ "aListItemElement");
+ MOZ_ASSERT(
+ !unwrappedSplitListItemParentResult.HasCaretPointSuggestion());
+ return Err(NS_ERROR_FAILURE);
+ }
+ unwrappedSplitListItemParentResult.IgnoreCaretPointSuggestion();
+ leftListElement =
+ unwrappedSplitListItemParentResult.GetPreviousContentAs<Element>();
+ MOZ_DIAGNOSTIC_ASSERT(leftListElement);
+ }
+
+ auto afterLeftListElement = EditorDOMPoint::After(leftListElement);
+ if (MOZ_UNLIKELY(!afterLeftListElement.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // If aListItemElement is in an invalid sub-list element, move it into
+ // the grand parent list element in order to outdent.
+ if (HTMLEditUtils::IsAnyListElement(afterLeftListElement.GetContainer())) {
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ MoveNodeWithTransaction(aListItemElement, afterLeftListElement);
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveListItemElementResult.propagateErr();
+ }
+ moveListItemElementResult.inspect().IgnoreCaretPointSuggestion();
+ return InsertParagraphResult(&aListItemElement,
+ EditorDOMPoint(&aListItemElement, 0u));
+ }
+
+ // Otherwise, replace the empty aListItemElement with a new paragraph.
+ nsresult rv = DeleteNodeWithTransaction(aListItemElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ nsStaticAtom& newParagraphTagName =
+ &DefaultParagraphSeparatorTagName() == nsGkAtoms::br
+ ? *nsGkAtoms::p
+ : DefaultParagraphSeparatorTagName();
+ // MOZ_KnownLive(newParagraphTagName) because it's available until shutdown.
+ Result<CreateElementResult, nsresult> createNewParagraphElementResult =
+ CreateAndInsertElement(
+ WithTransaction::Yes, MOZ_KnownLive(newParagraphTagName),
+ afterLeftListElement, HTMLEditor::InsertNewBRElement);
+ if (MOZ_UNLIKELY(createNewParagraphElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewParagraphElementResult.propagateErr();
+ }
+ createNewParagraphElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(createNewParagraphElementResult.inspect().GetNewNode());
+ EditorDOMPoint pointToPutCaret(
+ createNewParagraphElementResult.inspect().GetNewNode(), 0u);
+ return InsertParagraphResult(
+ createNewParagraphElementResult.inspect().GetNewNode(),
+ std::move(pointToPutCaret));
+ }
+
+ // If aListItemElement has some content or aListItemElement is empty but it's
+ // a child of editing host, we want a new list item at the same list level.
+ // First, sort out white-spaces.
+ Result<EditorDOMPoint, nsresult> preparationResult =
+ WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ *this, aPointToSplit, aListItemElement);
+ if (preparationResult.isErr()) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement() failed");
+ return Err(preparationResult.unwrapErr());
+ }
+ EditorDOMPoint pointToSplit = preparationResult.unwrap();
+ MOZ_ASSERT(pointToSplit.IsInContentNode());
+
+ // Now split the list item.
+ Result<SplitNodeResult, nsresult> splitListItemResult =
+ SplitNodeDeepWithTransaction(aListItemElement, pointToSplit,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitListItemResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitListItemResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitListItemElement = splitListItemResult.unwrap();
+ unwrappedSplitListItemElement.IgnoreCaretPointSuggestion();
+ if (MOZ_UNLIKELY(!aListItemElement.GetParent())) {
+ NS_WARNING("Somebody disconnected the target listitem from the parent");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // If aListItemElement is not replaced, we should not do anything anymore.
+ if (MOZ_UNLIKELY(!unwrappedSplitListItemElement.DidSplit()) ||
+ NS_WARN_IF(!unwrappedSplitListItemElement.GetNewContentAs<Element>()) ||
+ NS_WARN_IF(
+ !unwrappedSplitListItemElement.GetOriginalContentAs<Element>())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() didn't split");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // FYI: They are grabbed by unwrappedSplitListItemElement so that they are
+ // known live
+ // things.
+ auto& leftListItemElement =
+ *unwrappedSplitListItemElement.GetPreviousContentAs<Element>();
+ auto& rightListItemElement =
+ *unwrappedSplitListItemElement.GetNextContentAs<Element>();
+
+ // Hack: until I can change the damaged doc range code back to being
+ // extra-inclusive, I have to manually detect certain list items that may be
+ // left empty.
+ if (HTMLEditUtils::IsEmptyNode(
+ leftListItemElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint(&leftListItemElement, 0u));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction("
+ ") failed");
+ return insertPaddingBRElementResult.propagateErr();
+ }
+ // We're returning a candidate point to put caret so that we don't need to
+ // update now.
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ return InsertParagraphResult(&rightListItemElement,
+ EditorDOMPoint(&rightListItemElement, 0u));
+ }
+
+ if (HTMLEditUtils::IsEmptyNode(
+ rightListItemElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ // If aListItemElement is a <dd> or a <dt> and the right list item is empty
+ // or a direct child of the editing host, replace it a new list item element
+ // whose type is the other one.
+ if (aListItemElement.IsAnyOfHTMLElements(nsGkAtoms::dd, nsGkAtoms::dt)) {
+ nsStaticAtom& nextDefinitionListItemTagName =
+ aListItemElement.IsHTMLElement(nsGkAtoms::dt) ? *nsGkAtoms::dd
+ : *nsGkAtoms::dt;
+ // MOZ_KnownLive(nextDefinitionListItemTagName) because it's available
+ // until shutdown.
+ Result<CreateElementResult, nsresult> createNewListItemElementResult =
+ CreateAndInsertElement(WithTransaction::Yes,
+ MOZ_KnownLive(nextDefinitionListItemTagName),
+ EditorDOMPoint::After(rightListItemElement));
+ if (MOZ_UNLIKELY(createNewListItemElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewListItemElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewListItemElementResult =
+ createNewListItemElementResult.unwrap();
+ unwrappedCreateNewListItemElementResult.IgnoreCaretPointSuggestion();
+ RefPtr<Element> newListItemElement =
+ unwrappedCreateNewListItemElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newListItemElement);
+ // MOZ_KnownLive(rightListItemElement) because it's grabbed by
+ // unwrappedSplitListItemElement.
+ nsresult rv =
+ DeleteNodeWithTransaction(MOZ_KnownLive(rightListItemElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ EditorDOMPoint pointToPutCaret(newListItemElement, 0u);
+ return InsertParagraphResult(std::move(newListItemElement),
+ std::move(pointToPutCaret));
+ }
+
+ // If aListItemElement is a <li> and the right list item becomes empty or a
+ // direct child of the editing host, copy all inline elements affecting to
+ // the style at end of the left list item element to the right list item
+ // element.
+ // MOZ_KnownLive(leftListItemElement) and
+ // MOZ_KnownLive(rightListItemElement) because they are grabbed by
+ // unwrappedSplitListItemElement.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ CopyLastEditableChildStylesWithTransaction(
+ MOZ_KnownLive(leftListItemElement),
+ MOZ_KnownLive(rightListItemElement), aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CopyLastEditableChildStylesWithTransaction() failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ return InsertParagraphResult(&rightListItemElement,
+ pointToPutCaretOrError.unwrap());
+ }
+
+ // If the right list item element is not empty, we need to consider where to
+ // put caret in it. If it has non-container inline elements, <br> or <hr>, at
+ // the element is proper position.
+ WSScanResult forwardScanFromStartOfListItemResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost, EditorRawDOMPoint(&rightListItemElement, 0u),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(forwardScanFromStartOfListItemResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (forwardScanFromStartOfListItemResult.ReachedSpecialContent() ||
+ forwardScanFromStartOfListItemResult.ReachedBRElement() ||
+ forwardScanFromStartOfListItemResult.ReachedHRElement()) {
+ auto atFoundElement =
+ forwardScanFromStartOfListItemResult.PointAtContent<EditorDOMPoint>();
+ if (NS_WARN_IF(!atFoundElement.IsSetAndValid())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return InsertParagraphResult(&rightListItemElement,
+ std::move(atFoundElement));
+ }
+
+ // If we reached a block boundary (end of the list item or a child block),
+ // let's put deepest start of the list item or the child block.
+ if (forwardScanFromStartOfListItemResult.ReachedBlockBoundary()) {
+ return InsertParagraphResult(
+ &rightListItemElement,
+ HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
+ forwardScanFromStartOfListItemResult.GetContent()
+ ? *forwardScanFromStartOfListItemResult.GetContent()
+ : rightListItemElement));
+ }
+
+ // Otherwise, return the point at first visible thing.
+ // XXX This may be not meaningful position if it reached block element
+ // in aListItemElement.
+ return InsertParagraphResult(
+ &rightListItemElement,
+ forwardScanFromStartOfListItemResult.Point<EditorDOMPoint>());
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // The idea here is to put the nodes into a minimal number of blockquotes.
+ // When the user blockquotes something, they expect one blockquote. That
+ // may not be possible (for instance, if they have two table cells selected,
+ // you need two blockquotes inside the cells).
+ RefPtr<Element> curBlock, blockElementToPutCaret;
+ nsCOMPtr<nsINode> prevParent;
+
+ EditorDOMPoint pointToPutCaret;
+ for (auto& content : aArrayOfContents) {
+ // If the node is a table element or list item, dive inside
+ if (HTMLEditUtils::IsAnyTableElementButNotTable(content) ||
+ HTMLEditUtils::IsListItem(content)) {
+ // Forget any previous block
+ curBlock = nullptr;
+ // Recursion time
+ AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
+ HTMLEditUtils::CollectAllChildren(*content, childContents);
+ Result<CreateElementResult, nsresult>
+ wrapChildrenInAnotherBlockquoteResult =
+ WrapContentsInBlockquoteElementsWithTransaction(childContents,
+ aEditingHost);
+ if (MOZ_UNLIKELY(wrapChildrenInAnotherBlockquoteResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction() "
+ "failed");
+ return wrapChildrenInAnotherBlockquoteResult;
+ }
+ CreateElementResult unwrappedWrapChildrenInAnotherBlockquoteResult =
+ wrapChildrenInAnotherBlockquoteResult.unwrap();
+ unwrappedWrapChildrenInAnotherBlockquoteResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ if (unwrappedWrapChildrenInAnotherBlockquoteResult.GetNewNode()) {
+ blockElementToPutCaret =
+ unwrappedWrapChildrenInAnotherBlockquoteResult.UnwrapNewNode();
+ }
+ }
+
+ // If the node has different parent than previous node, further nodes in a
+ // new parent
+ if (prevParent) {
+ if (prevParent != content->GetParentNode()) {
+ // Forget any previous blockquote node we were using
+ curBlock = nullptr;
+ prevParent = content->GetParentNode();
+ }
+ } else {
+ prevParent = content->GetParentNode();
+ }
+
+ // If no curBlock, make one
+ if (!curBlock) {
+ Result<CreateElementResult, nsresult> createNewBlockquoteElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::blockquote, EditorDOMPoint(content),
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::blockquote) failed");
+ return createNewBlockquoteElementResult;
+ }
+ CreateElementResult unwrappedCreateNewBlockquoteElementResult =
+ createNewBlockquoteElementResult.unwrap();
+ unwrappedCreateNewBlockquoteElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_ASSERT(unwrappedCreateNewBlockquoteElementResult.GetNewNode());
+ blockElementToPutCaret =
+ unwrappedCreateNewBlockquoteElementResult.GetNewNode();
+ curBlock = unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode();
+ }
+
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to/ keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *curBlock);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ unwrappedMoveNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return blockElementToPutCaret
+ ? CreateElementResult(std::move(blockElementToPutCaret),
+ std::move(pointToPutCaret))
+ : CreateElementResult::NotHandled(std::move(pointToPutCaret));
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::RemoveBlockContainerElementsWithTransaction(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ FormatBlockMode aFormatBlockMode, BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand);
+
+ // Intent of this routine is to be used for converting to/from headers,
+ // paragraphs, pre, and address. Those blocks that pretty much just contain
+ // inline things...
+ RefPtr<Element> blockElement;
+ nsCOMPtr<nsIContent> firstContent, lastContent;
+ EditorDOMPoint pointToPutCaret;
+ for (const auto& content : aArrayOfContents) {
+ // If the current node is a format element, remove it.
+ if (HTMLEditUtils::IsFormatElementForParagraphStateCommand(content)) {
+ // Process any partial progress saved
+ if (blockElement) {
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ }
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+ // Remove current block
+ Result<EditorDOMPoint, nsresult> unwrapFormatBlockResult =
+ RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*content->AsElement()));
+ if (MOZ_UNLIKELY(unwrapFormatBlockResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapFormatBlockResult;
+ }
+ if (unwrapFormatBlockResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapFormatBlockResult.unwrap();
+ }
+ continue;
+ }
+
+ // XXX How about, <th>, <thead>, <tfoot>, <dt>, <dl>?
+ if (content->IsAnyOfHTMLElements(
+ nsGkAtoms::table, nsGkAtoms::tr, nsGkAtoms::tbody, nsGkAtoms::td,
+ nsGkAtoms::li, nsGkAtoms::blockquote, nsGkAtoms::div) ||
+ HTMLEditUtils::IsAnyListElement(content)) {
+ // Process any partial progress saved
+ if (blockElement) {
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ }
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+ // Recursion time
+ AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
+ HTMLEditUtils::CollectAllChildren(*content, childContents);
+ Result<EditorDOMPoint, nsresult> removeBlockContainerElementsResult =
+ RemoveBlockContainerElementsWithTransaction(
+ childContents, aFormatBlockMode, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed");
+ return removeBlockContainerElementsResult;
+ }
+ if (removeBlockContainerElementsResult.inspect().IsSet()) {
+ pointToPutCaret = removeBlockContainerElementsResult.unwrap();
+ }
+ continue;
+ }
+
+ if (HTMLEditUtils::IsInlineContent(content, aBlockInlineCheck)) {
+ if (blockElement) {
+ // If so, is this node a descendant?
+ if (EditorUtils::IsDescendantOf(*content, *blockElement)) {
+ // Then we don't need to do anything different for this node
+ lastContent = content;
+ continue;
+ }
+ // Otherwise, we have progressed beyond end of blockElement, so let's
+ // handle it now. We need to remove the portion of blockElement that
+ // contains [firstContent - lastContent].
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ // Fall out and handle content
+ }
+ blockElement = HTMLEditUtils::GetAncestorElement(
+ content, HTMLEditUtils::ClosestEditableBlockElement,
+ aBlockInlineCheck);
+ if (!blockElement ||
+ !HTMLEditUtils::IsFormatElementForParagraphStateCommand(
+ *blockElement) ||
+ !HTMLEditUtils::IsRemovableNode(*blockElement)) {
+ // Not a block kind that we care about.
+ blockElement = nullptr;
+ } else {
+ firstContent = lastContent = content;
+ }
+ continue;
+ }
+
+ if (blockElement) {
+ // Some node that is already sans block style. Skip over it and process
+ // any partial progress saved.
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ continue;
+ }
+ }
+ // Process any partial progress saved
+ if (blockElement) {
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ }
+ return pointToPutCaret;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::CreateOrChangeFormatContainerElement(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ const nsStaticAtom& aNewFormatTagName, FormatBlockMode aFormatBlockMode,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // Intent of this routine is to be used for converting to/from headers,
+ // paragraphs, pre, and address. Those blocks that pretty much just contain
+ // inline things...
+ RefPtr<Element> newBlock, curBlock, blockElementToPutCaret;
+ // If we found a <br> element which should be moved into curBlock, this keeps
+ // storing the <br> element after removing it from the tree.
+ RefPtr<Element> pendingBRElementToMoveCurBlock;
+ EditorDOMPoint pointToPutCaret;
+ for (auto& content : aArrayOfContents) {
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsInContentNode())) {
+ // If given node has been removed from the document, let's ignore it
+ // since the following code may need its parent replace it with new
+ // block.
+ curBlock = nullptr;
+ newBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ continue;
+ }
+
+ // Is it already the right kind of block, or an uneditable block?
+ if (content->IsHTMLElement(&aNewFormatTagName) ||
+ (!EditorUtils::IsEditableContent(content, EditorType::HTML) &&
+ HTMLEditUtils::IsBlockElement(
+ content, BlockInlineCheck::UseHTMLDefaultStyle))) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ // Do nothing to this block
+ continue;
+ }
+
+ // If content is a format element, replace it with a new block of correct
+ // type.
+ // XXX: pre can't hold everything the others can
+ if (HTMLEditUtils::IsMozDiv(content) ||
+ HTMLEditor::IsFormatElement(aFormatBlockMode, content)) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ RefPtr<Element> expectedContainerOfNewBlock =
+ atContent.IsContainerHTMLElement(nsGkAtoms::dl) &&
+ HTMLEditUtils::IsSplittableNode(
+ *atContent.ContainerAs<Element>())
+ ? atContent.GetContainerParentAs<Element>()
+ : atContent.GetContainerAs<Element>();
+ Result<CreateElementResult, nsresult> replaceWithNewBlockElementResult =
+ ReplaceContainerAndCloneAttributesWithTransaction(
+ MOZ_KnownLive(*content->AsElement()), aNewFormatTagName);
+ if (MOZ_UNLIKELY(replaceWithNewBlockElementResult.isErr())) {
+ NS_WARNING(
+ "EditorBase::ReplaceContainerAndCloneAttributesWithTransaction() "
+ "failed");
+ return replaceWithNewBlockElementResult;
+ }
+ CreateElementResult unwrappedReplaceWithNewBlockElementResult =
+ replaceWithNewBlockElementResult.unwrap();
+ // If the new block element was moved to different element or removed by
+ // the web app via mutation event listener, we should stop handling this
+ // action since we cannot handle each of a lot of edge cases.
+ if (NS_WARN_IF(unwrappedReplaceWithNewBlockElementResult.GetNewNode()
+ ->GetParentNode() != expectedContainerOfNewBlock)) {
+ unwrappedReplaceWithNewBlockElementResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ unwrappedReplaceWithNewBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ newBlock = unwrappedReplaceWithNewBlockElementResult.UnwrapNewNode();
+ continue;
+ }
+
+ if (HTMLEditUtils::IsTable(content) ||
+ HTMLEditUtils::IsAnyListElement(content) ||
+ content->IsAnyOfHTMLElements(nsGkAtoms::tbody, nsGkAtoms::tr,
+ nsGkAtoms::td, nsGkAtoms::li,
+ nsGkAtoms::blockquote, nsGkAtoms::div)) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ // Recursion time
+ AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
+ HTMLEditUtils::CollectAllChildren(*content, childContents);
+ if (!childContents.IsEmpty()) {
+ Result<CreateElementResult, nsresult> wrapChildrenInBlockElementResult =
+ CreateOrChangeFormatContainerElement(
+ childContents, aNewFormatTagName, aFormatBlockMode,
+ aEditingHost);
+ if (MOZ_UNLIKELY(wrapChildrenInBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateOrChangeFormatContainerElement() failed");
+ return wrapChildrenInBlockElementResult;
+ }
+ CreateElementResult unwrappedWrapChildrenInBlockElementResult =
+ wrapChildrenInBlockElementResult.unwrap();
+ unwrappedWrapChildrenInBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ if (unwrappedWrapChildrenInBlockElementResult.GetNewNode()) {
+ blockElementToPutCaret =
+ unwrappedWrapChildrenInBlockElementResult.UnwrapNewNode();
+ }
+ continue;
+ }
+
+ // Make sure we can put a block here
+ Result<CreateElementResult, nsresult> createNewBlockElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ aNewFormatTagName, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction(%s) failed",
+ nsAtomCString(&aNewFormatTagName).get())
+ .get());
+ return createNewBlockElementResult;
+ }
+ CreateElementResult unwrappedCreateNewBlockElementResult =
+ createNewBlockElementResult.unwrap();
+ unwrappedCreateNewBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode());
+ blockElementToPutCaret =
+ unwrappedCreateNewBlockElementResult.UnwrapNewNode();
+ continue;
+ }
+
+ if (content->IsHTMLElement(nsGkAtoms::br)) {
+ if (curBlock) {
+ if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand) {
+ // If the node is a break, we honor it by putting further nodes in a
+ // new parent.
+
+ // Forget any previous block used for previous inline nodes.
+ curBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ } else {
+ // If the node is a break, we need to move it into end of the curBlock
+ // if we'll move following content into curBlock.
+ pendingBRElementToMoveCurBlock = content->AsElement();
+ }
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
+ // alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ // The break is the first (or even only) node we encountered. Create a
+ // block for it.
+ Result<CreateElementResult, nsresult> createNewBlockElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ aNewFormatTagName, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(nsPrintfCString("HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWith"
+ "Transaction(%s) failed",
+ nsAtomCString(&aNewFormatTagName).get())
+ .get());
+ return createNewBlockElementResult;
+ }
+ CreateElementResult unwrappedCreateNewBlockElementResult =
+ createNewBlockElementResult.unwrap();
+ unwrappedCreateNewBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ RefPtr<Element> newBlockElement =
+ unwrappedCreateNewBlockElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newBlockElement);
+ blockElementToPutCaret = newBlockElement;
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
+ // alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
+ *newBlockElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ unwrappedMoveNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ curBlock = std::move(newBlockElement);
+ continue;
+ }
+
+ if (HTMLEditUtils::IsInlineContent(content,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ // If content is inline, pull it into curBlock. Note: it's assumed that
+ // consecutive inline nodes in aNodeArray are actually members of the
+ // same block parent. This happens to be true now as a side effect of
+ // how aNodeArray is constructed, but some additional logic should be
+ // added here if that should change
+ //
+ // If content is a non editable, drop it if we are going to <pre>.
+ if (&aNewFormatTagName == nsGkAtoms::pre &&
+ !EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ // Do nothing to this block
+ continue;
+ }
+
+ // If no curBlock, make one
+ if (!curBlock) {
+ Result<CreateElementResult, nsresult> createNewBlockElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ aNewFormatTagName, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(nsPrintfCString("HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWith"
+ "Transaction(%s) failed",
+ nsAtomCString(&aNewFormatTagName).get())
+ .get());
+ return createNewBlockElementResult;
+ }
+ CreateElementResult unwrappedCreateNewBlockElementResult =
+ createNewBlockElementResult.unwrap();
+ unwrappedCreateNewBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode());
+ blockElementToPutCaret =
+ unwrappedCreateNewBlockElementResult.GetNewNode();
+ curBlock = unwrappedCreateNewBlockElementResult.UnwrapNewNode();
+
+ // Update container of content.
+ atContent.Set(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ // This is possible due to mutation events, let's not assert
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ } else if (pendingBRElementToMoveCurBlock) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertNodeWithTransaction<Element>(
+ *pendingBRElementToMoveCurBlock,
+ EditorDOMPoint::AtEndOf(*curBlock));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction<Element>() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ pendingBRElementToMoveCurBlock = nullptr;
+ }
+
+ // XXX If content is a br, replace it with a return if going to <pre>
+
+ // This is a continuation of some inline nodes that belong together in
+ // the same block item. Use curBlock.
+ //
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
+ // alive. We could try to make that a rvalue ref and create a const array
+ // on the stack here, but callers are passing in auto arrays, and we don't
+ // want to introduce copies..
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *curBlock);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ unwrappedMoveNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ }
+ return blockElementToPutCaret
+ ? CreateElementResult(std::move(blockElementToPutCaret),
+ std::move(pointToPutCaret))
+ : CreateElementResult::NotHandled(std::move(pointToPutCaret));
+}
+
+Result<SplitNodeResult, nsresult>
+HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction(
+ const nsAtom& aTag, const EditorDOMPoint& aStartOfDeepestRightNode,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aEditingHost.IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (NS_WARN_IF(!aStartOfDeepestRightNode.IsSet())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+ MOZ_ASSERT(aStartOfDeepestRightNode.IsSetAndValid());
+
+ // The point must be descendant of editing host.
+ // XXX Isn't it a valid case if it points a direct child of aEditingHost?
+ if (NS_WARN_IF(
+ !aStartOfDeepestRightNode.GetContainer()->IsInclusiveDescendantOf(
+ &aEditingHost))) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ // Look for a node that can legally contain the tag.
+ const EditorDOMPoint pointToInsert =
+ HTMLEditUtils::GetInsertionPointInInclusiveAncestor(
+ aTag, aStartOfDeepestRightNode, &aEditingHost);
+ if (MOZ_UNLIKELY(!pointToInsert.IsSet())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() reached "
+ "editing host");
+ return Err(NS_ERROR_FAILURE);
+ }
+ // If the point itself can contain the tag, we don't need to split any
+ // ancestor nodes. In this case, we should return the given split point
+ // as is.
+ if (pointToInsert.GetContainer() == aStartOfDeepestRightNode.GetContainer()) {
+ return SplitNodeResult::NotHandled(aStartOfDeepestRightNode);
+ }
+
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitNodeDeepWithTransaction(MOZ_KnownLive(*pointToInsert.GetChild()),
+ aStartOfDeepestRightNode,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ NS_WARNING_ASSERTION(splitNodeResult.isOk(),
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eAllowToCreateEmptyContainer) failed");
+ return splitNodeResult;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(
+ const nsAtom& aTagName, const EditorDOMPoint& aPointToInsert,
+ BRElementNextToSplitPoint aBRElementNextToSplitPoint,
+ const Element& aEditingHost,
+ const InitializeInsertingElement& aInitializer) {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ const nsCOMPtr<nsIContent> childAtPointToInsert = aPointToInsert.GetChild();
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ MaybeSplitAncestorsForInsertWithTransaction(aTagName, aPointToInsert,
+ aEditingHost);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed");
+ return splitNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ DebugOnly<bool> wasCaretPositionSuggestedAtSplit =
+ unwrappedSplitNodeResult.HasCaretPointSuggestion();
+ // We'll update selection below, and nobody touches selection until then.
+ // Therefore, we don't need to touch selection here.
+ unwrappedSplitNodeResult.IgnoreCaretPointSuggestion();
+
+ // If current handling node has been moved from the container by a
+ // mutation event listener when we need to do something more for it,
+ // we should stop handling this action since we cannot handle each
+ // edge case.
+ if (childAtPointToInsert &&
+ NS_WARN_IF(!childAtPointToInsert->IsInclusiveDescendantOf(
+ unwrappedSplitNodeResult.DidSplit()
+ ? unwrappedSplitNodeResult.GetNextContent()
+ : aPointToInsert.GetContainer()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ auto splitPoint = unwrappedSplitNodeResult.AtSplitPoint<EditorDOMPoint>();
+ if (aBRElementNextToSplitPoint == BRElementNextToSplitPoint::Delete) {
+ // Consume a trailing br, if any. This is to keep an alignment from
+ // creating extra lines, if possible.
+ if (nsCOMPtr<nsIContent> maybeBRContent = HTMLEditUtils::GetNextContent(
+ splitPoint,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost)) {
+ if (maybeBRContent->IsHTMLElement(nsGkAtoms::br) &&
+ splitPoint.GetChild()) {
+ // Making use of html structure... if next node after where we are
+ // putting our div is not a block, then the br we found is in same
+ // block we are, so it's safe to consume it.
+ if (nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling(
+ *splitPoint.GetChild(),
+ {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (!HTMLEditUtils::IsBlockElement(
+ *nextEditableSibling,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(splitPoint);
+ nsresult rv = DeleteNodeWithTransaction(*maybeBRContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Result<CreateElementResult, nsresult> createNewElementResult =
+ CreateAndInsertElement(WithTransaction::Yes, aTagName, splitPoint,
+ aInitializer);
+ if (MOZ_UNLIKELY(createNewElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewElementResult;
+ }
+ MOZ_ASSERT_IF(wasCaretPositionSuggestedAtSplit,
+ createNewElementResult.inspect().HasCaretPointSuggestion());
+ MOZ_ASSERT(createNewElementResult.inspect().GetNewNode());
+
+ // If the new block element was moved to different element or removed by
+ // the web app via mutation event listener, we should stop handling this
+ // action since we cannot handle each of a lot of edge cases.
+ if (NS_WARN_IF(
+ createNewElementResult.inspect().GetNewNode()->GetParentNode() !=
+ splitPoint.GetContainer())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ return createNewElementResult;
+}
+
+nsresult HTMLEditor::JoinNearestEditableNodesWithTransaction(
+ nsIContent& aNodeLeft, nsIContent& aNodeRight,
+ EditorDOMPoint* aNewFirstChildOfRightNode) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aNewFirstChildOfRightNode);
+
+ // Caller responsible for left and right node being the same type
+ if (NS_WARN_IF(!aNodeLeft.GetParentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+ // If they don't have the same parent, first move the right node to after
+ // the left one
+ if (aNodeLeft.GetParentNode() != aNodeRight.GetParentNode()) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeWithTransaction(aNodeRight, EditorDOMPoint(&aNodeLeft));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveNodeResult.unwrapErr();
+ }
+ nsresult rv = moveNodeResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ }
+
+ // Separate join rules for differing blocks
+ if (HTMLEditUtils::IsAnyListElement(&aNodeLeft) || aNodeLeft.IsText()) {
+ // For lists, merge shallow (wouldn't want to combine list items)
+ Result<JoinNodesResult, nsresult> joinNodesResult =
+ JoinNodesWithTransaction(aNodeLeft, aNodeRight);
+ if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
+ NS_WARNING("HTMLEditor::JoinNodesWithTransaction failed");
+ return joinNodesResult.unwrapErr();
+ }
+ *aNewFirstChildOfRightNode =
+ joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>();
+ return NS_OK;
+ }
+
+ // Remember the last left child, and first right child
+ nsCOMPtr<nsIContent> lastEditableChildOfLeftContent =
+ HTMLEditUtils::GetLastChild(aNodeLeft,
+ {WalkTreeOption::IgnoreNonEditableNode});
+ if (MOZ_UNLIKELY(NS_WARN_IF(!lastEditableChildOfLeftContent))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIContent> firstEditableChildOfRightContent =
+ HTMLEditUtils::GetFirstChild(aNodeRight,
+ {WalkTreeOption::IgnoreNonEditableNode});
+ if (NS_WARN_IF(!firstEditableChildOfRightContent)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // For list items, divs, etc., merge smart
+ Result<JoinNodesResult, nsresult> joinNodesResult =
+ JoinNodesWithTransaction(aNodeLeft, aNodeRight);
+ if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
+ NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
+ return joinNodesResult.unwrapErr();
+ }
+
+ if ((lastEditableChildOfLeftContent->IsText() ||
+ lastEditableChildOfLeftContent->IsElement()) &&
+ HTMLEditUtils::CanContentsBeJoined(*lastEditableChildOfLeftContent,
+ *firstEditableChildOfRightContent)) {
+ nsresult rv = JoinNearestEditableNodesWithTransaction(
+ *lastEditableChildOfLeftContent, *firstEditableChildOfRightContent,
+ aNewFirstChildOfRightNode);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::JoinNearestEditableNodesWithTransaction() failed");
+ return rv;
+ }
+ *aNewFirstChildOfRightNode =
+ joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>();
+ return NS_OK;
+}
+
+Element* HTMLEditor::GetMostDistantAncestorMailCiteElement(
+ const nsINode& aNode) const {
+ Element* mailCiteElement = nullptr;
+ const bool isPlaintextEditor = IsPlaintextMailComposer();
+ for (Element* element : aNode.InclusiveAncestorsOfType<Element>()) {
+ if ((isPlaintextEditor && element->IsHTMLElement(nsGkAtoms::pre)) ||
+ HTMLEditUtils::IsMailCite(*element)) {
+ mailCiteElement = element;
+ continue;
+ }
+ if (element->IsHTMLElement(nsGkAtoms::body)) {
+ break;
+ }
+ }
+ return mailCiteElement;
+}
+
+nsresult HTMLEditor::CacheInlineStyles(Element& Element) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ nsresult rv = GetInlineStyles(
+ Element, *TopLevelEditSubActionDataRef().mCachedPendingStyles);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetInlineStyles() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::GetInlineStyles(
+ Element& aElement, AutoPendingStyleCacheArray& aPendingStyleCacheArray) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPendingStyleCacheArray.IsEmpty());
+
+ if (!IsCSSEnabled()) {
+ // In the HTML styling mode, we should preserve the order of inline styles
+ // specified with HTML elements, then, we can keep same order as original
+ // one when we create new elements to apply the styles at new place.
+ // XXX Currently, we don't preserve all inline parents, therefore, we cannot
+ // restore all inline elements as-is. Perhaps, we should store all
+ // inline elements with more details (e.g., all attributes), and store
+ // same elements. For example, web apps may give style as:
+ // em {
+ // font-style: italic;
+ // }
+ // em em {
+ // font-style: normal;
+ // font-weight: bold;
+ // }
+ // but we cannot restore the style as-is.
+ nsString value;
+ const bool givenElementIsEditable =
+ HTMLEditUtils::IsSimplyEditableNode(aElement);
+ auto NeedToAppend = [&](nsStaticAtom& aTagName, nsStaticAtom* aAttribute) {
+ if (mPendingStylesToApplyToNewContent->GetStyleState(
+ aTagName, aAttribute) != PendingStyleState::NotUpdated) {
+ return false; // The style has already been changed.
+ }
+ if (aPendingStyleCacheArray.Contains(aTagName, aAttribute)) {
+ return false; // Already preserved
+ }
+ return true;
+ };
+ for (Element* const inclusiveAncestor :
+ aElement.InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsBlockElement(
+ *inclusiveAncestor,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ (givenElementIsEditable &&
+ !HTMLEditUtils::IsSimplyEditableNode(*inclusiveAncestor))) {
+ break;
+ }
+ if (inclusiveAncestor->IsAnyOfHTMLElements(
+ nsGkAtoms::b, nsGkAtoms::i, nsGkAtoms::u, nsGkAtoms::s,
+ nsGkAtoms::strike, nsGkAtoms::tt, nsGkAtoms::em,
+ nsGkAtoms::strong, nsGkAtoms::dfn, nsGkAtoms::code,
+ nsGkAtoms::samp, nsGkAtoms::var, nsGkAtoms::cite, nsGkAtoms::abbr,
+ nsGkAtoms::acronym, nsGkAtoms::sub, nsGkAtoms::sup)) {
+ nsStaticAtom& tagName = const_cast<nsStaticAtom&>(
+ *inclusiveAncestor->NodeInfo()->NameAtom()->AsStatic());
+ if (NeedToAppend(tagName, nullptr)) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(tagName, nullptr, EmptyString()));
+ }
+ continue;
+ }
+ if (inclusiveAncestor->IsHTMLElement(nsGkAtoms::font)) {
+ if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::face)) {
+ inclusiveAncestor->GetAttr(nsGkAtoms::face, value);
+ if (!value.IsEmpty()) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::face, value));
+ value.Truncate();
+ }
+ }
+ if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::size)) {
+ inclusiveAncestor->GetAttr(nsGkAtoms::size, value);
+ if (!value.IsEmpty()) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::size, value));
+ value.Truncate();
+ }
+ }
+ if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::color)) {
+ inclusiveAncestor->GetAttr(nsGkAtoms::color, value);
+ if (!value.IsEmpty()) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::color, value));
+ value.Truncate();
+ }
+ }
+ continue;
+ }
+ }
+ return NS_OK;
+ }
+
+ for (nsStaticAtom* property : {nsGkAtoms::b,
+ nsGkAtoms::i,
+ nsGkAtoms::u,
+ nsGkAtoms::s,
+ nsGkAtoms::strike,
+ nsGkAtoms::face,
+ nsGkAtoms::size,
+ nsGkAtoms::color,
+ nsGkAtoms::tt,
+ nsGkAtoms::em,
+ nsGkAtoms::strong,
+ nsGkAtoms::dfn,
+ nsGkAtoms::code,
+ nsGkAtoms::samp,
+ nsGkAtoms::var,
+ nsGkAtoms::cite,
+ nsGkAtoms::abbr,
+ nsGkAtoms::acronym,
+ nsGkAtoms::backgroundColor,
+ nsGkAtoms::sub,
+ nsGkAtoms::sup}) {
+ const EditorInlineStyle style =
+ property == nsGkAtoms::face || property == nsGkAtoms::size ||
+ property == nsGkAtoms::color
+ ? EditorInlineStyle(*nsGkAtoms::font, property)
+ : EditorInlineStyle(*property);
+ // If type-in state is set, don't intervene
+ const PendingStyleState styleState =
+ mPendingStylesToApplyToNewContent->GetStyleState(*style.mHTMLProperty,
+ style.mAttribute);
+ if (styleState != PendingStyleState::NotUpdated) {
+ continue;
+ }
+ bool isSet = false;
+ nsString value; // Don't use nsAutoString here because it requires memcpy
+ // at creating new PendingStyleCache instance.
+ // Don't use CSS for <font size>, we don't support it usefully (bug 780035)
+ if (property == nsGkAtoms::size) {
+ isSet = HTMLEditUtils::IsInlineStyleSetByElement(aElement, style, nullptr,
+ &value);
+ } else if (style.IsCSSSettable(aElement)) {
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(*this, aElement, style,
+ value);
+ if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
+ NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError.unwrapErr();
+ }
+ isSet = isComputedCSSEquivalentToStyleOrError.unwrap();
+ }
+ if (isSet) {
+ aPendingStyleCacheArray.AppendElement(
+ style.ToPendingStyleCache(std::move(value)));
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::ReapplyCachedStyles() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // The idea here is to examine our cached list of styles and see if any have
+ // been removed. If so, add typeinstate for them, so that they will be
+ // reinserted when new content is added.
+
+ if (TopLevelEditSubActionDataRef().mCachedPendingStyles->IsEmpty() ||
+ !SelectionRef().RangeCount()) {
+ return NS_OK;
+ }
+
+ // remember if we are in css mode
+ const bool useCSS = IsCSSEnabled();
+
+ const RangeBoundary& atStartOfSelection =
+ SelectionRef().GetRangeAt(0)->StartRef();
+ const RefPtr<Element> startContainerElement =
+ atStartOfSelection.Container() &&
+ atStartOfSelection.Container()->IsContent()
+ ? atStartOfSelection.Container()->GetAsElementOrParentElement()
+ : nullptr;
+ if (NS_WARN_IF(!startContainerElement)) {
+ return NS_OK;
+ }
+
+ AutoPendingStyleCacheArray styleCacheArrayAtInsertionPoint;
+ nsresult rv =
+ GetInlineStyles(*startContainerElement, styleCacheArrayAtInsertionPoint);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetInlineStyles() failed, but ignored");
+ return NS_OK;
+ }
+
+ for (PendingStyleCache& styleCacheBeforeEdit :
+ Reversed(*TopLevelEditSubActionDataRef().mCachedPendingStyles)) {
+ bool isFirst = false, isAny = false, isAll = false;
+ nsAutoString currentValue;
+ const EditorInlineStyle inlineStyle = styleCacheBeforeEdit.ToInlineStyle();
+ if (useCSS && inlineStyle.IsCSSSettable(*startContainerElement)) {
+ // check computed style first in css case
+ // MOZ_KnownLive(styleCacheBeforeEdit.*) because they are nsStaticAtom
+ // and its instances are alive until shutting down.
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(*this, *startContainerElement,
+ inlineStyle, currentValue);
+ if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
+ NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError.unwrapErr();
+ }
+ isAny = isComputedCSSEquivalentToStyleOrError.unwrap();
+ }
+ if (!isAny) {
+ // then check typeinstate and html style
+ nsresult rv = GetInlinePropertyBase(
+ inlineStyle, &styleCacheBeforeEdit.AttributeValueOrCSSValueRef(),
+ &isFirst, &isAny, &isAll, &currentValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetInlinePropertyBase() failed");
+ return rv;
+ }
+ }
+ // This style has disappeared through deletion. Let's add the styles to
+ // mPendingStylesToApplyToNewContent when same style isn't applied to the
+ // node already.
+ if (isAny &&
+ !IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) {
+ continue;
+ }
+ AutoPendingStyleCacheArray::index_type index =
+ styleCacheArrayAtInsertionPoint.IndexOf(
+ styleCacheBeforeEdit.TagRef(), styleCacheBeforeEdit.GetAttribute());
+ if (index == AutoPendingStyleCacheArray::NoIndex ||
+ styleCacheBeforeEdit.AttributeValueOrCSSValueRef() !=
+ styleCacheArrayAtInsertionPoint.ElementAt(index)
+ .AttributeValueOrCSSValueRef()) {
+ mPendingStylesToApplyToNewContent->PreserveStyle(styleCacheBeforeEdit);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange(
+ const RawRangeBoundary& aStartRef, const RawRangeBoundary& aEndRef) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoTArray<OwningNonNull<Element>, 64> arrayOfEmptyElements;
+ DOMIterator iter;
+ if (NS_FAILED(iter.Init(aStartRef, aEndRef))) {
+ NS_WARNING("DOMIterator::Init() failed");
+ return NS_ERROR_FAILURE;
+ }
+ iter.AppendNodesToArray(
+ +[](nsINode& aNode, void* aSelf) {
+ MOZ_ASSERT(Element::FromNode(&aNode));
+ MOZ_ASSERT(aSelf);
+ Element* element = aNode.AsElement();
+ if (!EditorUtils::IsEditableContent(*element, EditorType::HTML) ||
+ (!HTMLEditUtils::IsListItem(element) &&
+ !HTMLEditUtils::IsTableCellOrCaption(*element))) {
+ return false;
+ }
+ return HTMLEditUtils::IsEmptyNode(
+ *element, {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible});
+ },
+ arrayOfEmptyElements, this);
+
+ // Put padding <br> elements for empty <li> and <td>.
+ EditorDOMPoint pointToPutCaret;
+ for (auto& emptyElement : arrayOfEmptyElements) {
+ // Need to put br at END of node. It may have empty containers in it and
+ // still pass the "IsEmptyNode" test, and we want the br's to be after
+ // them. Also, we want the br to be after the selection if the selection
+ // is in this node.
+ EditorDOMPoint endOfNode(EditorDOMPoint::AtEndOf(emptyElement));
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(endOfNode);
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
+ "failed");
+ return insertPaddingBRElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInsertPaddingBRElementResult =
+ insertPaddingBRElementResult.unwrap();
+ unwrappedInsertPaddingBRElementResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ return NS_OK;
+}
+
+void HTMLEditor::SetSelectionInterlinePosition() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(SelectionRef().IsCollapsed());
+
+ // Get the (collapsed) selection location
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return;
+ }
+
+ EditorDOMPoint atCaret(firstRange->StartRef());
+ if (NS_WARN_IF(!atCaret.IsSet())) {
+ return;
+ }
+ MOZ_ASSERT(atCaret.IsSetAndValid());
+
+ // First, let's check to see if we are after a `<br>`. We take care of this
+ // special-case first so that we don't accidentally fall through into one of
+ // the other conditionals.
+ // XXX Although I don't understand "interline position", if caret is
+ // immediately after non-editable contents, but previous editable
+ // content is `<br>`, does this do right thing?
+ if (Element* editingHost = ComputeEditingHost()) {
+ if (nsIContent* previousEditableContentInBlock =
+ HTMLEditUtils::GetPreviousContent(
+ atCaret,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
+ if (previousEditableContentInBlock->IsHTMLElement(nsGkAtoms::br)) {
+ DebugOnly<nsresult> rvIgnored = SelectionRef().SetInterlinePosition(
+ InterlinePosition::StartOfNextLine);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "StartOfNextLine) failed, but ignored");
+ return;
+ }
+ }
+ }
+
+ if (!atCaret.GetChild()) {
+ return;
+ }
+
+ // If caret is immediately after a block, set interline position to "right".
+ // XXX Although I don't understand "interline position", if caret is
+ // immediately after non-editable contents, but previous editable
+ // content is a block, does this do right thing?
+ if (nsIContent* previousEditableContentInBlockAtCaret =
+ HTMLEditUtils::GetPreviousSibling(
+ *atCaret.GetChild(), {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (HTMLEditUtils::IsBlockElement(
+ *previousEditableContentInBlockAtCaret,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ DebugOnly<nsresult> rvIgnored = SelectionRef().SetInterlinePosition(
+ InterlinePosition::StartOfNextLine);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "StartOfNextLine) failed, but ignored");
+ return;
+ }
+ }
+
+ // If caret is immediately before a block, set interline position to "left".
+ // XXX Although I don't understand "interline position", if caret is
+ // immediately before non-editable contents, but next editable
+ // content is a block, does this do right thing?
+ if (nsIContent* nextEditableContentInBlockAtCaret =
+ HTMLEditUtils::GetNextSibling(
+ *atCaret.GetChild(), {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (HTMLEditUtils::IsBlockElement(
+ *nextEditableContentInBlockAtCaret,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ DebugOnly<nsresult> rvIgnored =
+ SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "EndOfLine) failed, but ignored");
+ }
+ }
+}
+
+nsresult HTMLEditor::AdjustCaretPositionAndEnsurePaddingBRElement(
+ nsIEditor::EDirection aDirectionAndAmount) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(SelectionRef().IsCollapsed());
+
+ auto point = GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!point.IsInContentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If selection start is not editable, climb up the tree until editable one.
+ while (!EditorUtils::IsEditableContent(*point.ContainerAs<nsIContent>(),
+ EditorType::HTML)) {
+ point.Set(point.GetContainer());
+ if (NS_WARN_IF(!point.IsInContentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // If caret is in empty block element, we need to insert a `<br>` element
+ // because the block should have one-line height.
+ // XXX Even if only a part of the block is editable, shouldn't we put
+ // caret if the block element is now empty?
+ if (Element* const editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *point.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ if (editableBlockElement &&
+ HTMLEditUtils::IsEmptyNode(
+ *editableBlockElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible}) &&
+ HTMLEditUtils::CanNodeContain(*point.GetContainer(), *nsGkAtoms::br)) {
+ Element* bodyOrDocumentElement = GetRoot();
+ if (NS_WARN_IF(!bodyOrDocumentElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (point.GetContainer() == bodyOrDocumentElement) {
+ // Our root node is completely empty. Don't add a <br> here.
+ // AfterEditInner() will add one for us when it calls
+ // EditorBase::MaybeCreatePaddingBRElementForEmptyEditor().
+ // XXX This kind of dependency between methods makes us spaghetti.
+ // Let's handle it here later.
+ // XXX This looks odd check. If active editing host is not a
+ // `<body>`, what are we doing?
+ return NS_OK;
+ }
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(point);
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction("
+ ") failed");
+ return insertPaddingBRElementResult.unwrapErr();
+ }
+ nsresult rv = insertPaddingBRElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ return NS_OK;
+ }
+ }
+
+ // XXX Perhaps, we should do something if we're in a data node but not
+ // a text node.
+ if (point.IsInTextNode()) {
+ return NS_OK;
+ }
+
+ // Do we need to insert a padding <br> element for empty last line? We do
+ // if we are:
+ // 1) prior node is in same block where selection is AND
+ // 2) prior node is a br AND
+ // 3) that br is not visible
+ RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ return NS_OK;
+ }
+
+ if (nsCOMPtr<nsIContent> previousEditableContent =
+ HTMLEditUtils::GetPreviousContent(
+ point, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
+ // If caret and previous editable content are in same block element
+ // (even if it's a non-editable element), we should put a padding <br>
+ // element at end of the block.
+ const Element* const blockElementContainingCaret =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *point.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ const Element* const blockElementContainingPreviousEditableContent =
+ HTMLEditUtils::GetAncestorElement(
+ *previousEditableContent, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ // If previous editable content of caret is in same block and a `<br>`
+ // element, we need to adjust interline position.
+ if (blockElementContainingCaret &&
+ blockElementContainingCaret ==
+ blockElementContainingPreviousEditableContent &&
+ point.ContainerAs<nsIContent>()->GetEditingHost() ==
+ previousEditableContent->GetEditingHost() &&
+ previousEditableContent &&
+ previousEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ // If it's an invisible `<br>` element, we need to insert a padding
+ // `<br>` element for making empty line have one-line height.
+ if (HTMLEditUtils::IsInvisibleBRElement(*previousEditableContent) &&
+ !EditorUtils::IsPaddingBRElementForEmptyLastLine(
+ *previousEditableContent)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(point);
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(point);
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertPaddingBRElementForEmptyLastLineWithTransaction() failed");
+ return insertPaddingBRElementResult.unwrapErr();
+ }
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ nsresult rv = CollapseSelectionTo(EditorRawDOMPoint(
+ insertPaddingBRElementResult.inspect().GetNewNode(),
+ InterlinePosition::StartOfNextLine));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ }
+ // If it's a visible `<br>` element and next editable content is a
+ // padding `<br>` element, we need to set interline position.
+ else if (nsIContent* nextEditableContentInBlock =
+ HTMLEditUtils::GetNextContent(
+ *previousEditableContent,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle,
+ editingHost)) {
+ if (EditorUtils::IsPaddingBRElementForEmptyLastLine(
+ *nextEditableContentInBlock)) {
+ // Make it stick to the padding `<br>` element so that it will be
+ // on blank line.
+ DebugOnly<nsresult> rvIgnored = SelectionRef().SetInterlinePosition(
+ InterlinePosition::StartOfNextLine);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "StartOfNextLine) failed, but ignored");
+ }
+ }
+ }
+ }
+
+ // If previous editable content in same block is `<br>`, text node, `<img>`
+ // or `<hr>`, current caret position is fine.
+ if (nsIContent* previousEditableContentInBlock =
+ HTMLEditUtils::GetPreviousContent(
+ point,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
+ if (previousEditableContentInBlock->IsHTMLElement(nsGkAtoms::br) ||
+ previousEditableContentInBlock->IsText() ||
+ HTMLEditUtils::IsImage(previousEditableContentInBlock) ||
+ previousEditableContentInBlock->IsHTMLElement(nsGkAtoms::hr)) {
+ return NS_OK;
+ }
+ }
+
+ // If next editable content in same block is `<br>`, text node, `<img>` or
+ // `<hr>`, current caret position is fine.
+ if (nsIContent* nextEditableContentInBlock = HTMLEditUtils::GetNextContent(
+ point,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
+ if (nextEditableContentInBlock->IsText() ||
+ nextEditableContentInBlock->IsAnyOfHTMLElements(
+ nsGkAtoms::br, nsGkAtoms::img, nsGkAtoms::hr)) {
+ return NS_OK;
+ }
+ }
+
+ // Otherwise, look for a near editable content towards edit action direction.
+
+ // If there is no editable content, keep current caret position.
+ // XXX Why do we treat `nsIEditor::ePreviousWord` etc as forward direction?
+ nsIContent* nearEditableContent = HTMLEditUtils::GetAdjacentContentToPutCaret(
+ point,
+ aDirectionAndAmount == nsIEditor::ePrevious ? WalkTreeDirection::Backward
+ : WalkTreeDirection::Forward,
+ *editingHost);
+ if (!nearEditableContent) {
+ return NS_OK;
+ }
+
+ EditorRawDOMPoint pointToPutCaret =
+ HTMLEditUtils::GetGoodCaretPointFor<EditorRawDOMPoint>(
+ *nearEditableContent, aDirectionAndAmount);
+ if (!pointToPutCaret.IsSet()) {
+ NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::RemoveEmptyNodesIn(const EditorDOMRange& aRange) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aRange.IsPositioned());
+
+ // Some general notes on the algorithm used here: the goal is to examine all
+ // the nodes in aRange, and remove the empty ones. We do this by
+ // using a content iterator to traverse all the nodes in the range, and
+ // placing the empty nodes into an array. After finishing the iteration,
+ // we delete the empty nodes in the array. (They cannot be deleted as we
+ // find them because that would invalidate the iterator.)
+ //
+ // Since checking to see if a node is empty can be costly for nodes with
+ // many descendants, there are some optimizations made. I rely on the fact
+ // that the iterator is post-order: it will visit children of a node before
+ // visiting the parent node. So if I find that a child node is not empty, I
+ // know that its parent is not empty without even checking. So I put the
+ // parent on a "skipList" which is just a voidArray of nodes I can skip the
+ // empty check on. If I encounter a node on the skiplist, i skip the
+ // processing for that node and replace its slot in the skiplist with that
+ // node's parent.
+ //
+ // An interesting idea is to go ahead and regard parent nodes that are NOT
+ // on the skiplist as being empty (without even doing the IsEmptyNode check)
+ // on the theory that if they weren't empty, we would have encountered a
+ // non-empty child earlier and thus put this parent node on the skiplist.
+ //
+ // Unfortunately I can't use that strategy here, because the range may
+ // include some children of a node while excluding others. Thus I could
+ // find all the _examined_ children empty, but still not have an empty
+ // parent.
+
+ const RawRangeBoundary endOfRange = [&]() {
+ // If the range is not collapsed and end of the range is start of a
+ // container, it means that the inclusive ancestor empty element may be
+ // created by splitting the left nodes.
+ if (aRange.Collapsed() || !aRange.IsInContentNodes() ||
+ !aRange.EndRef().IsStartOfContainer()) {
+ return aRange.EndRef().ToRawRangeBoundary();
+ }
+ nsINode* const commonAncestor =
+ nsContentUtils::GetClosestCommonInclusiveAncestor(
+ aRange.StartRef().ContainerAs<nsIContent>(),
+ aRange.EndRef().ContainerAs<nsIContent>());
+ if (!commonAncestor) {
+ return aRange.EndRef().ToRawRangeBoundary();
+ }
+ nsIContent* maybeRightContent = nullptr;
+ for (nsIContent* content : aRange.EndRef()
+ .ContainerAs<nsIContent>()
+ ->InclusiveAncestorsOfType<nsIContent>()) {
+ if (!HTMLEditUtils::IsSimplyEditableNode(*content) ||
+ content == commonAncestor) {
+ break;
+ }
+ if (aRange.StartRef().ContainerAs<nsIContent>() == content) {
+ break;
+ }
+ EmptyCheckOptions options = {
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible};
+ if (!HTMLEditUtils::IsBlockElement(
+ *content, BlockInlineCheck::UseComputedDisplayStyle)) {
+ options += EmptyCheckOption::TreatSingleBRElementAsVisible;
+ }
+ if (!HTMLEditUtils::IsEmptyNode(*content, options)) {
+ break;
+ }
+ maybeRightContent = content;
+ }
+ if (!maybeRightContent) {
+ return aRange.EndRef().ToRawRangeBoundary();
+ }
+ return EditorRawDOMPoint::After(*maybeRightContent).ToRawRangeBoundary();
+ }();
+
+ PostContentIterator postOrderIter;
+ nsresult rv =
+ postOrderIter.Init(aRange.StartRef().ToRawRangeBoundary(), endOfRange);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("PostContentIterator::Init() failed");
+ return rv;
+ }
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfEmptyContents,
+ arrayOfEmptyCites;
+
+ // Collect empty nodes first.
+ {
+ const bool isMailEditor = IsMailEditor();
+ AutoTArray<OwningNonNull<nsIContent>, 64> knownNonEmptyContents;
+ Maybe<AutoRangeArray> maybeSelectionRanges;
+ for (; !postOrderIter.IsDone(); postOrderIter.Next()) {
+ MOZ_ASSERT(postOrderIter.GetCurrentNode()->IsContent());
+
+ nsIContent* content = postOrderIter.GetCurrentNode()->AsContent();
+ nsIContent* parentContent = content->GetParent();
+
+ size_t idx = knownNonEmptyContents.IndexOf(content);
+ if (idx != decltype(knownNonEmptyContents)::NoIndex) {
+ // This node is on our skip list. Skip processing for this node, and
+ // replace its value in the skip list with the value of its parent
+ if (parentContent) {
+ knownNonEmptyContents[idx] = parentContent;
+ }
+ continue;
+ }
+
+ const bool isEmptyNode = [&]() {
+ if (!content->IsElement()) {
+ return false;
+ }
+ const bool isMailCite =
+ isMailEditor && HTMLEditUtils::IsMailCite(*content->AsElement());
+ const bool isCandidate = [&]() {
+ if (content->IsHTMLElement(nsGkAtoms::body)) {
+ // Don't delete the body
+ return false;
+ }
+ if (isMailCite || content->IsHTMLElement(nsGkAtoms::a) ||
+ HTMLEditUtils::IsInlineStyle(content) ||
+ HTMLEditUtils::IsAnyListElement(content) ||
+ content->IsHTMLElement(nsGkAtoms::div)) {
+ // Only consider certain nodes to be empty for purposes of removal
+ return true;
+ }
+ if (HTMLEditUtils::IsFormatElementForFormatBlockCommand(*content) ||
+ HTMLEditUtils::IsListItem(content) ||
+ content->IsHTMLElement(nsGkAtoms::blockquote)) {
+ // These node types are candidates if selection is not in them. If
+ // it is one of these, don't delete if selection inside. This is so
+ // we can create empty headings, etc., for the user to type into.
+ if (maybeSelectionRanges.isNothing()) {
+ maybeSelectionRanges.emplace(SelectionRef());
+ }
+ return !maybeSelectionRanges
+ ->IsAtLeastOneContainerOfRangeBoundariesInclusiveDescendantOf(
+ *content);
+ }
+ return false;
+ }();
+
+ if (!isCandidate) {
+ return false;
+ }
+
+ // We delete mailcites even if they have a solo br in them. Other
+ // nodes we require to be empty.
+ HTMLEditUtils::EmptyCheckOptions options{
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible};
+ if (!isMailCite) {
+ options += EmptyCheckOption::TreatSingleBRElementAsVisible;
+ } else {
+ // XXX Maybe unnecessary to specify this.
+ options += EmptyCheckOption::TreatNonEditableContentAsInvisible;
+ }
+ if (!HTMLEditUtils::IsEmptyNode(*content, options)) {
+ return false;
+ }
+
+ if (isMailCite) {
+ // mailcites go on a separate list from other empty nodes
+ arrayOfEmptyCites.AppendElement(*content);
+ }
+ // Don't delete non-editable nodes in this method because this is a
+ // clean up method to remove unnecessary nodes of the result of
+ // editing. So, we shouldn't delete non-editable nodes which were
+ // there before editing. Additionally, if the element is some special
+ // elements such as <body>, we shouldn't delete it.
+ else if (HTMLEditUtils::IsSimplyEditableNode(*content) &&
+ HTMLEditUtils::IsRemovableNode(*content)) {
+ arrayOfEmptyContents.AppendElement(*content);
+ }
+ return true;
+ }();
+ if (!isEmptyNode && parentContent) {
+ knownNonEmptyContents.AppendElement(*parentContent);
+ }
+ } // end of the for-loop iterating with postOrderIter
+ }
+
+ // now delete the empty nodes
+ for (OwningNonNull<nsIContent>& emptyContent : arrayOfEmptyContents) {
+ // MOZ_KnownLive due to bug 1622253
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(emptyContent));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+
+ // Now delete the empty mailcites. This is a separate step because we want
+ // to pull out any br's and preserve them.
+ EditorDOMPoint pointToPutCaret;
+ for (OwningNonNull<nsIContent>& emptyCite : arrayOfEmptyCites) {
+ if (!HTMLEditUtils::IsEmptyNode(
+ emptyCite,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ // We are deleting a cite that has just a `<br>`. We want to delete cite,
+ // but preserve `<br>`.
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, EditorDOMPoint(emptyCite));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // XXX Is this intentional selection change?
+ unwrappedInsertBRElementResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ }
+ // MOZ_KnownLive because 'arrayOfEmptyCites' is guaranteed to keep it alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(emptyCite));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+ // XXX Is this intentional selection change?
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::LiftUpListItemElement(
+ Element& aListItemElement,
+ LiftUpFromAllParentListElements aLiftUpFromAllParentListElements) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!HTMLEditUtils::IsListItem(&aListItemElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!aListItemElement.GetParentElement()) ||
+ NS_WARN_IF(!aListItemElement.GetParentElement()->GetParentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // if it's first or last list item, don't need to split the list
+ // otherwise we do.
+ const bool isFirstListItem = HTMLEditUtils::IsFirstChild(
+ aListItemElement, {WalkTreeOption::IgnoreNonEditableNode});
+ const bool isLastListItem = HTMLEditUtils::IsLastChild(
+ aListItemElement, {WalkTreeOption::IgnoreNonEditableNode});
+
+ Element* leftListElement = aListItemElement.GetParentElement();
+ if (NS_WARN_IF(!leftListElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If it's at middle of parent list element, split the parent list element.
+ // Then, aListItem becomes the first list item of the right list element.
+ if (!isFirstListItem && !isLastListItem) {
+ EditorDOMPoint atListItemElement(&aListItemElement);
+ if (NS_WARN_IF(!atListItemElement.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atListItemElement.IsSetAndValid());
+ Result<SplitNodeResult, nsresult> splitListItemParentResult =
+ SplitNodeWithTransaction(atListItemElement);
+ if (MOZ_UNLIKELY(splitListItemParentResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitListItemParentResult.unwrapErr();
+ }
+ nsresult rv = splitListItemParentResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+
+ leftListElement =
+ splitListItemParentResult.inspect().GetPreviousContentAs<Element>();
+ if (MOZ_UNLIKELY(!leftListElement)) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeWithTransaction() didn't return left list "
+ "element");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // In most cases, insert the list item into the new left list node..
+ EditorDOMPoint pointToInsertListItem(leftListElement);
+ if (NS_WARN_IF(!pointToInsertListItem.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // But when the list item was the first child of the right list, it should
+ // be inserted between the both list elements. This allows user to hit
+ // Enter twice at a list item breaks the parent list node.
+ if (!isFirstListItem) {
+ DebugOnly<bool> advanced = pointToInsertListItem.AdvanceOffset();
+ NS_WARNING_ASSERTION(advanced,
+ "Failed to advance offset to right list node");
+ }
+
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ MoveNodeWithTransaction(aListItemElement, pointToInsertListItem);
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveListItemElementResult.unwrapErr();
+ }
+ MoveNodeResult unwrappedMoveListItemElementResult =
+ moveListItemElementResult.unwrap();
+ unwrappedMoveListItemElementResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+
+ // Unwrap list item contents if they are no longer in a list
+ // XXX If the parent list element is a child of another list element
+ // (although invalid tree), the list item element won't be unwrapped.
+ // That makes the parent ancestor element tree valid, but might be
+ // unexpected result.
+ // XXX If aListItemElement is <dl> or <dd> and current parent is <ul> or <ol>,
+ // the list items won't be unwrapped. If aListItemElement is <li> and its
+ // current parent is <dl>, there is same issue.
+ if (!HTMLEditUtils::IsAnyListElement(pointToInsertListItem.GetContainer()) &&
+ HTMLEditUtils::IsListItem(&aListItemElement)) {
+ Result<EditorDOMPoint, nsresult> unwrapOrphanListItemElementResult =
+ RemoveBlockContainerWithTransaction(aListItemElement);
+ if (MOZ_UNLIKELY(unwrapOrphanListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapOrphanListItemElementResult.unwrapErr();
+ }
+ if (AllowsTransactionsToChangeSelection() &&
+ unwrapOrphanListItemElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapOrphanListItemElementResult.unwrap();
+ }
+ if (!pointToPutCaret.IsSet()) {
+ return NS_OK;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+
+ if (aLiftUpFromAllParentListElements == LiftUpFromAllParentListElements::No) {
+ return NS_OK;
+ }
+ // XXX If aListItemElement is moved to unexpected element by mutation event
+ // listener, shouldn't we stop calling this?
+ nsresult rv = LiftUpListItemElement(aListItemElement,
+ LiftUpFromAllParentListElements::Yes);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::LiftUpListItemElement("
+ "LiftUpFromAllParentListElements::Yes) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::DestroyListStructureRecursively(Element& aListElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+
+ // XXX If mutation event listener inserts new child into `aListElement`,
+ // this becomes infinite loop so that we should set limit of the
+ // loop count from original child count.
+ while (aListElement.GetFirstChild()) {
+ OwningNonNull<nsIContent> child = *aListElement.GetFirstChild();
+
+ if (HTMLEditUtils::IsListItem(child)) {
+ // XXX Using LiftUpListItemElement() is too expensive for this purpose.
+ // Looks like the reason why this method uses it is, only this loop
+ // wants to work with first child of aListElement. However, what it
+ // actually does is removing <li> as container. Perhaps, we should
+ // decide destination first, and then, move contents in `child`.
+ // XXX If aListElement is is a child of another list element (although
+ // it's invalid tree), this moves the list item to outside of
+ // aListElement's parent. Is that really intentional behavior?
+ nsresult rv = LiftUpListItemElement(
+ MOZ_KnownLive(*child->AsElement()),
+ HTMLEditor::LiftUpFromAllParentListElements::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:"
+ ":Yes) failed");
+ return rv;
+ }
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(child)) {
+ nsresult rv =
+ DestroyListStructureRecursively(MOZ_KnownLive(*child->AsElement()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DestroyListStructureRecursively() failed");
+ return rv;
+ }
+ continue;
+ }
+
+ // Delete any non-list items for now
+ // XXX This is not HTML5 aware. HTML5 allows all list elements to have
+ // <script> and <template> and <dl> element to have <div> to group
+ // some <dt> and <dd> elements. So, this may break valid children.
+ nsresult rv = DeleteNodeWithTransaction(*child);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+
+ // Delete the now-empty list
+ const Result<EditorDOMPoint, nsresult> unwrapListElementResult =
+ RemoveBlockContainerWithTransaction(aListElement);
+ if (MOZ_UNLIKELY(unwrapListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapListElementResult.inspectErr();
+ }
+ const EditorDOMPoint& pointToPutCaret = unwrapListElementResult.inspect();
+ if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
+ return NS_OK;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::EnsureSelectionInBodyOrDocumentElement() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Element> bodyOrDocumentElement = GetRoot();
+ if (NS_WARN_IF(!bodyOrDocumentElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const auto atCaret = GetFirstSelectionStartPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!atCaret.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX This does wrong things. Web apps can put any elements as sibling
+ // of `<body>` element. Therefore, this collapses `Selection` into
+ // the `<body>` element which `HTMLDocument.body` is set to. So,
+ // this makes users impossible to modify content outside of the
+ // `<body>` element even if caret is in an editing host.
+
+ // Check that selection start container is inside the <body> element.
+ // XXXsmaug this code is insane.
+ nsINode* temp = atCaret.GetContainer();
+ while (temp && !temp->IsHTMLElement(nsGkAtoms::body)) {
+ temp = temp->GetParentOrShadowHostNode();
+ }
+
+ // If we aren't in the <body> element, force the issue.
+ if (!temp) {
+ nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionToStartOf() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+ return NS_OK;
+ }
+
+ const auto selectionEndPoint = GetFirstSelectionEndPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!selectionEndPoint.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // check that selNode is inside body
+ // XXXsmaug this code is insane.
+ temp = selectionEndPoint.GetContainer();
+ while (temp && !temp->IsHTMLElement(nsGkAtoms::body)) {
+ temp = temp->GetParentOrShadowHostNode();
+ }
+
+ // If we aren't in the <body> element, force the issue.
+ if (!temp) {
+ nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionToStartOf() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::InsertPaddingBRElementForEmptyLastLineIfNeeded(
+ Element& aElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!HTMLEditUtils::IsBlockElement(
+ aElement, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return NS_OK;
+ }
+
+ if (!HTMLEditUtils::IsEmptyNode(
+ aElement, {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ return NS_OK;
+ }
+
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint(&aElement, 0u));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
+ "failed");
+ return insertPaddingBRElementResult.unwrapErr();
+ }
+ nsresult rv = insertPaddingBRElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ return NS_OK;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::RemoveAlignFromDescendants(
+ Element& aElement, const nsAString& aAlignType, EditTarget aEditTarget) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aElement.IsHTMLElement(nsGkAtoms::table));
+
+ const bool useCSS = IsCSSEnabled();
+
+ EditorDOMPoint pointToPutCaret;
+
+ // Let's remove all alignment hints in the children of aNode; it can
+ // be an ALIGN attribute (in case we just remove it) or a CENTER
+ // element (here we have to remove the container and keep its
+ // children). We break on tables and don't look at their children.
+ nsCOMPtr<nsIContent> nextSibling;
+ for (nsIContent* content =
+ aEditTarget == EditTarget::NodeAndDescendantsExceptTable
+ ? &aElement
+ : aElement.GetFirstChild();
+ content; content = nextSibling) {
+ // Get the next sibling before removing content from the DOM tree.
+ // XXX If next sibling is removed from the parent and/or inserted to
+ // different parent, we will behave unexpectedly. I think that
+ // we should create child list and handle it with checking whether
+ // it's still a child of expected parent.
+ nextSibling = aEditTarget == EditTarget::NodeAndDescendantsExceptTable
+ ? nullptr
+ : content->GetNextSibling();
+
+ if (content->IsHTMLElement(nsGkAtoms::center)) {
+ OwningNonNull<Element> centerElement = *content->AsElement();
+ {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ RemoveAlignFromDescendants(centerElement, aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveAlignFromDescendants(EditTarget::"
+ "OnlyDescendantsExceptTable) failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+
+ // We may have to insert a `<br>` element before first child of the
+ // `<center>` element because it should be first element of a hard line
+ // even after removing the `<center>` element.
+ {
+ Result<CreateElementResult, nsresult>
+ maybeInsertBRElementBeforeFirstChildResult =
+ EnsureHardLineBeginsWithFirstChildOf(centerElement);
+ if (MOZ_UNLIKELY(maybeInsertBRElementBeforeFirstChildResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::EnsureHardLineBeginsWithFirstChildOf() failed");
+ return maybeInsertBRElementBeforeFirstChildResult.propagateErr();
+ }
+ CreateElementResult unwrappedResult =
+ maybeInsertBRElementBeforeFirstChildResult.unwrap();
+ if (unwrappedResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedResult.UnwrapCaretPoint();
+ }
+ }
+
+ // We may have to insert a `<br>` element after last child of the
+ // `<center>` element because it should be last element of a hard line
+ // even after removing the `<center>` element.
+ {
+ Result<CreateElementResult, nsresult>
+ maybeInsertBRElementAfterLastChildResult =
+ EnsureHardLineEndsWithLastChildOf(centerElement);
+ if (MOZ_UNLIKELY(maybeInsertBRElementAfterLastChildResult.isErr())) {
+ NS_WARNING("HTMLEditor::EnsureHardLineEndsWithLastChildOf() failed");
+ return maybeInsertBRElementAfterLastChildResult.propagateErr();
+ }
+ CreateElementResult unwrappedResult =
+ maybeInsertBRElementAfterLastChildResult.unwrap();
+ if (unwrappedResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedResult.UnwrapCaretPoint();
+ }
+ }
+
+ {
+ Result<EditorDOMPoint, nsresult> unwrapCenterElementResult =
+ RemoveContainerWithTransaction(centerElement);
+ if (MOZ_UNLIKELY(unwrapCenterElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapCenterElementResult;
+ }
+ if (unwrapCenterElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapCenterElementResult.unwrap();
+ }
+ }
+ continue;
+ }
+
+ if (!HTMLEditUtils::IsBlockElement(*content,
+ BlockInlineCheck::UseHTMLDefaultStyle) &&
+ !content->IsHTMLElement(nsGkAtoms::hr)) {
+ continue;
+ }
+
+ const OwningNonNull<Element> blockOrHRElement = *content->AsElement();
+ if (HTMLEditUtils::SupportsAlignAttr(blockOrHRElement)) {
+ nsresult rv =
+ RemoveAttributeWithTransaction(blockOrHRElement, *nsGkAtoms::align);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::align) "
+ "failed");
+ return Err(rv);
+ }
+ }
+ if (useCSS) {
+ if (blockOrHRElement->IsAnyOfHTMLElements(nsGkAtoms::table,
+ nsGkAtoms::hr)) {
+ nsresult rv = SetAttributeOrEquivalent(
+ blockOrHRElement, nsGkAtoms::align, aAlignType, false);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align) failed");
+ return Err(rv);
+ }
+ } else {
+ nsStyledElement* styledBlockOrHRElement =
+ nsStyledElement::FromNode(blockOrHRElement);
+ if (NS_WARN_IF(!styledBlockOrHRElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // MOZ_KnownLive(*styledBlockOrHRElement): It's `blockOrHRElement
+ // which is OwningNonNull.
+ nsAutoString dummyCssValue;
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ CSSEditUtils::RemoveCSSInlineStyleWithTransaction(
+ *this, MOZ_KnownLive(*styledBlockOrHRElement),
+ nsGkAtoms::textAlign, dummyCssValue);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSInlineStyleWithTransaction(nsGkAtoms::"
+ "textAlign) failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ }
+ if (!blockOrHRElement->IsHTMLElement(nsGkAtoms::table)) {
+ // unless this is a table, look at children
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ RemoveAlignFromDescendants(blockOrHRElement, aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (pointToPutCaretOrError.isErr()) {
+ NS_WARNING(
+ "HTMLEditor::RemoveAlignFromDescendants(EditTarget::"
+ "OnlyDescendantsExceptTable) failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ }
+ return pointToPutCaret;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::EnsureHardLineBeginsWithFirstChildOf(
+ Element& aRemovingContainerElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsIContent* firstEditableChild = HTMLEditUtils::GetFirstChild(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!firstEditableChild) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(
+ *firstEditableChild, BlockInlineCheck::UseComputedDisplayStyle) ||
+ firstEditableChild->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousSibling(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!previousEditableContent) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(
+ *previousEditableContent,
+ BlockInlineCheck::UseComputedDisplayStyle) ||
+ previousEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult = InsertBRElement(
+ WithTransaction::Yes, EditorDOMPoint(&aRemovingContainerElement, 0u));
+ NS_WARNING_ASSERTION(
+ insertBRElementResult.isOk(),
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::EnsureHardLineEndsWithLastChildOf(
+ Element& aRemovingContainerElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsIContent* firstEditableContent = HTMLEditUtils::GetLastChild(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!firstEditableContent) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(
+ *firstEditableContent, BlockInlineCheck::UseComputedDisplayStyle) ||
+ firstEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ nsIContent* nextEditableContent = HTMLEditUtils::GetPreviousSibling(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!nextEditableContent) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(
+ *nextEditableContent, BlockInlineCheck::UseComputedDisplayStyle) ||
+ nextEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult = InsertBRElement(
+ WithTransaction::Yes, EditorDOMPoint::AtEndOf(aRemovingContainerElement));
+ NS_WARNING_ASSERTION(
+ insertBRElementResult.isOk(),
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::SetBlockElementAlign(
+ Element& aBlockOrHRElement, const nsAString& aAlignType,
+ EditTarget aEditTarget) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(HTMLEditUtils::IsBlockElement(
+ aBlockOrHRElement, BlockInlineCheck::UseHTMLDefaultStyle) ||
+ aBlockOrHRElement.IsHTMLElement(nsGkAtoms::hr));
+ MOZ_ASSERT(IsCSSEnabled() ||
+ HTMLEditUtils::SupportsAlignAttr(aBlockOrHRElement));
+
+ EditorDOMPoint pointToPutCaret;
+ if (!aBlockOrHRElement.IsHTMLElement(nsGkAtoms::table)) {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ RemoveAlignFromDescendants(aBlockOrHRElement, aAlignType, aEditTarget);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveAlignFromDescendants() failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ nsresult rv = SetAttributeOrEquivalent(&aBlockOrHRElement, nsGkAtoms::align,
+ aAlignType, false);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetAttributeOrEquivalent(nsGkAtoms::align) failed");
+ return Err(rv);
+ }
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::ChangeMarginStart(
+ Element& aElement, ChangeMargin aChangeMargin,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(aElement);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ nsAutoString value;
+ DebugOnly<nsresult> rvIgnored =
+ CSSEditUtils::GetSpecifiedProperty(aElement, marginProperty, value);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetSpecifiedProperty() failed, but ignored");
+ float f;
+ RefPtr<nsAtom> unit;
+ CSSEditUtils::ParseLength(value, &f, getter_AddRefs(unit));
+ if (!f) {
+ unit = nsGkAtoms::px;
+ }
+ int8_t multiplier = aChangeMargin == ChangeMargin::Increase ? 1 : -1;
+ if (nsGkAtoms::in == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_IN * multiplier;
+ } else if (nsGkAtoms::cm == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_CM * multiplier;
+ } else if (nsGkAtoms::mm == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_MM * multiplier;
+ } else if (nsGkAtoms::pt == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PT * multiplier;
+ } else if (nsGkAtoms::pc == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PC * multiplier;
+ } else if (nsGkAtoms::em == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_EM * multiplier;
+ } else if (nsGkAtoms::ex == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_EX * multiplier;
+ } else if (nsGkAtoms::px == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PX * multiplier;
+ } else if (nsGkAtoms::percentage == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PERCENT * multiplier;
+ }
+
+ if (0 < f) {
+ if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) {
+ nsAutoString newValue;
+ newValue.AppendFloat(f);
+ newValue.Append(nsDependentAtomString(unit));
+ // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must
+ // be guaranteed by caller because of MOZ_CAN_RUN_SCRIPT method.
+ // MOZ_KnownLive(merginProperty): It's nsStaticAtom.
+ nsresult rv = CSSEditUtils::SetCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), MOZ_KnownLive(marginProperty),
+ newValue);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyWithTransaction() destroyed the "
+ "editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyWithTransaction() failed, but ignored");
+ }
+ return EditorDOMPoint();
+ }
+
+ if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) {
+ // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must
+ // be guaranteed by caller because of MOZ_CAN_RUN_SCRIPT method.
+ // MOZ_KnownLive(merginProperty): It's nsStaticAtom.
+ nsresult rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), MOZ_KnownLive(marginProperty),
+ value);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction() destroyed the "
+ "editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction() failed, but ignored");
+ }
+
+ // Remove unnecessary divs
+ if (!aElement.IsHTMLElement(nsGkAtoms::div) ||
+ HTMLEditUtils::ElementHasAttribute(aElement)) {
+ return EditorDOMPoint();
+ }
+ // Don't touch editing host nor node which is outside of it.
+ if (&aElement == &aEditingHost ||
+ !aElement.IsInclusiveDescendantOf(&aEditingHost)) {
+ return EditorDOMPoint();
+ }
+
+ Result<EditorDOMPoint, nsresult> unwrapDivElementResult =
+ RemoveContainerWithTransaction(aElement);
+ NS_WARNING_ASSERTION(unwrapDivElementResult.isOk(),
+ "HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapDivElementResult;
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::SetSelectionToAbsoluteAsSubAction(const Element& aEditingHost) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSetPositionToAbsolute, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ auto EnsureCaretInElementIfCollapsedOutside =
+ [&](Element& aElement) MOZ_CAN_RUN_SCRIPT {
+ if (!SelectionRef().IsCollapsed() || !SelectionRef().RangeCount()) {
+ return NS_OK;
+ }
+ const auto firstRangeStartPoint =
+ GetFirstSelectionStartPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!firstRangeStartPoint.IsSet())) {
+ return NS_OK;
+ }
+ const Result<EditorRawDOMPoint, nsresult> pointToPutCaretOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(aElement, firstRangeStartPoint);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() "
+ "failed, but ignored");
+ return NS_OK;
+ }
+ if (!pointToPutCaretOrError.inspect().IsSet()) {
+ return NS_OK;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return NS_OK;
+ };
+
+ RefPtr<Element> focusElement = GetSelectionContainerElement();
+ if (focusElement && HTMLEditUtils::IsImage(focusElement)) {
+ nsresult rv = EnsureCaretInElementIfCollapsedOutside(*focusElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EnsureCaretInElementIfCollapsedOutside() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ SelectionRef().GetRangeAt(0u), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use Selection::SetStartAndEndInLimit() here.
+ IgnoredErrorResult error;
+ SelectionRef().SetBaseAndExtentInLimiter(
+ extendedRange.inspect().StartRef().ToRawRangeBoundary(),
+ extendedRange.inspect().EndRef().ToRawRangeBoundary(), error);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return Err(error.StealNSResult());
+ }
+ }
+
+ RefPtr<Element> divElement;
+ rv = MoveSelectedContentsToDivElementToMakeItAbsolutePosition(
+ address_of(divElement), aEditingHost);
+ // MoveSelectedContentsToDivElementToMakeItAbsolutePosition() may restore
+ // selection with AutoSelectionRestorer. Therefore, the editor might have
+ // already been destroyed now.
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MoveSelectedContentsToDivElementToMakeItAbsolutePosition()"
+ " failed");
+ return Err(rv);
+ }
+
+ if (IsSelectionRangeContainerNotContent()) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() "
+ "failed");
+ return Err(rv);
+ }
+
+ if (!divElement) {
+ return EditActionResult::HandledResult();
+ }
+
+ rv = SetPositionToAbsoluteOrStatic(*divElement, true);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetPositionToAbsoluteOrStatic() failed");
+ return Err(rv);
+ }
+
+ rv = EnsureCaretInElementIfCollapsedOutside(*divElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EnsureCaretInElementIfCollapsedOutside() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::MoveSelectedContentsToDivElementToMakeItAbsolutePosition(
+ RefPtr<Element>* aTargetElement, const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aTargetElement);
+
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ EditorDOMPoint pointToPutCaret;
+
+ // Use these ranges to construct a list of nodes to act on.
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedSelectionRanges(SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ EditSubAction::eSetPositionToAbsolute,
+ BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedSelectionRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eSetPositionToAbsolute,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eSetPositionToAbsolute, CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eSetPositionToAbsolute);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eSetPositionToAbsolute) failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+
+ if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaret.IsSetAndValid()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ }
+
+ // If there is no visible and editable nodes in the edit targets, make an
+ // empty block.
+ // XXX Isn't this odd if there are only non-editable visible nodes?
+ if (HTMLEditUtils::IsEmptyOneHardLine(
+ arrayOfContents, BlockInlineCheck::UseHTMLDefaultStyle)) {
+ const auto atCaret =
+ EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!atCaret.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure we can put a block here.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atCaret, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ // We'll update selection after deleting the content nodes and nobody
+ // refers selection until then. Therefore, we don't need to update
+ // selection here.
+ unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion();
+ RefPtr<Element> newDivElement =
+ unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newDivElement);
+ // Delete anything that was in the list of nodes
+ // XXX We don't need to remove items from the array.
+ for (OwningNonNull<nsIContent>& curNode : arrayOfContents) {
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*curNode));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+ // Don't restore the selection
+ restoreSelectionLater.Abort();
+ nsresult rv = CollapseSelectionToStartOf(*newDivElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ *aTargetElement = std::move(newDivElement);
+ return rv;
+ }
+
+ // `<div>` element to be positioned absolutely. This may have already
+ // existed or newly created by this method.
+ RefPtr<Element> targetDivElement;
+ // Newly created list element for moving selected list item elements into
+ // targetDivElement. I.e., this is created in the `<div>` element.
+ RefPtr<Element> createdListElement;
+ // If we handle a parent list item element, this is set to it. In such case,
+ // we should handle its children again.
+ RefPtr<Element> handledListItemElement;
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // Here's where we actually figure out what to do.
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ return NS_ERROR_FAILURE; // XXX not continue??
+ }
+
+ // Ignore all non-editable nodes. Leave them be.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+
+ // If current node is a child of a list element, we need another list
+ // element in absolute-positioned `<div>` element to avoid non-selected
+ // list items are moved into the `<div>` element.
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ // If we cannot move current node to created list element, we need a
+ // list element in the target `<div>` element for the destination.
+ // Therefore, duplicate same list element into the target `<div>`
+ // element.
+ nsIContent* previousEditableContent =
+ createdListElement
+ ? HTMLEditUtils::GetPreviousSibling(
+ content, {WalkTreeOption::IgnoreNonEditableNode})
+ : nullptr;
+ if (!createdListElement ||
+ (previousEditableContent &&
+ previousEditableContent != createdListElement)) {
+ nsAtom* ULOrOLOrDLTagName =
+ atContent.GetContainer()->NodeInfo()->NameAtom();
+ if (targetDivElement) {
+ // XXX Do we need to split the container? Since we'll append new
+ // element at end of the <div> element.
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ MaybeSplitAncestorsForInsertWithTransaction(
+ MOZ_KnownLive(*ULOrOLOrDLTagName), atContent, aEditingHost);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() "
+ "failed");
+ return splitNodeResult.unwrapErr();
+ }
+ // We'll update selection after creating a list element below.
+ // Therefore, we don't need to touch selection here.
+ splitNodeResult.inspect().IgnoreCaretPointSuggestion();
+ } else {
+ // If we've not had a target <div> element yet, let's insert a <div>
+ // element with splitting the ancestors.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction(nsGkAtoms::"
+ "div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ // We'll update selection after creating a list element below.
+ // Therefor, we don't need to touch selection here.
+ createNewDivElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(createNewDivElementResult.inspect().GetNewNode());
+ targetDivElement = createNewDivElementResult.unwrap().UnwrapNewNode();
+ }
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ CreateAndInsertElement(WithTransaction::Yes,
+ MOZ_KnownLive(*ULOrOLOrDLTagName),
+ EditorDOMPoint::AtEndOf(targetDivElement));
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) "
+ "failed");
+ return createNewListElementResult.unwrapErr();
+ }
+ nsresult rv = createNewListElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ createdListElement =
+ createNewListElementResult.unwrap().UnwrapNewNode();
+ MOZ_ASSERT(createdListElement);
+ }
+ // Move current node (maybe, assumed as a list item element) into the
+ // new list element in the target `<div>` element to be positioned
+ // absolutely.
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
+ *createdListElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ nsresult rv = moveNodeResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ continue;
+ }
+
+ // If contents in a list item element is selected, we should move current
+ // node into the target `<div>` element with the list item element itself
+ // because we want to keep indent level of the contents.
+ if (RefPtr<Element> listItemElement =
+ HTMLEditUtils::GetClosestAncestorListItemElement(content,
+ &aEditingHost)) {
+ if (handledListItemElement == listItemElement) {
+ // Current node has already been moved into the `<div>` element.
+ continue;
+ }
+ // If we cannot move the list item element into created list element,
+ // we need another list element in the target `<div>` element.
+ nsIContent* previousEditableContent =
+ createdListElement
+ ? HTMLEditUtils::GetPreviousSibling(
+ *listItemElement, {WalkTreeOption::IgnoreNonEditableNode})
+ : nullptr;
+ if (!createdListElement ||
+ (previousEditableContent &&
+ previousEditableContent != createdListElement)) {
+ EditorDOMPoint atListItem(listItemElement);
+ if (NS_WARN_IF(!atListItem.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ // XXX If content is the listItemElement and not in a list element,
+ // we duplicate wrong element into the target `<div>` element.
+ nsAtom* containerName =
+ atListItem.GetContainer()->NodeInfo()->NameAtom();
+ if (targetDivElement) {
+ // XXX Do we need to split the container? Since we'll append new
+ // element at end of the <div> element.
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ MaybeSplitAncestorsForInsertWithTransaction(
+ MOZ_KnownLive(*containerName), atListItem, aEditingHost);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() "
+ "failed");
+ return splitNodeResult.unwrapErr();
+ }
+ // We'll update selection after creating a list element below.
+ // Therefore, we don't need to touch selection here.
+ splitNodeResult.inspect().IgnoreCaretPointSuggestion();
+ } else {
+ // If we've not had a target <div> element yet, let's insert a <div>
+ // element with splitting the ancestors.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ // We'll update selection after creating a list element below.
+ // Therefore, we don't need to touch selection here.
+ createNewDivElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(createNewDivElementResult.inspect().GetNewNode());
+ targetDivElement = createNewDivElementResult.unwrap().UnwrapNewNode();
+ }
+ // XXX So, createdListElement may be set to a non-list element.
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ CreateAndInsertElement(WithTransaction::Yes,
+ MOZ_KnownLive(*containerName),
+ EditorDOMPoint::AtEndOf(targetDivElement));
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) "
+ "failed");
+ return createNewListElementResult.unwrapErr();
+ }
+ nsresult rv = createNewListElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ createdListElement =
+ createNewListElementResult.unwrap().UnwrapNewNode();
+ MOZ_ASSERT(createdListElement);
+ }
+ // Move current list item element into the createdListElement (could be
+ // non-list element due to the above bug) in a candidate `<div>` element
+ // to be positioned absolutely.
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ MoveNodeToEndWithTransaction(*listItemElement, *createdListElement);
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveListItemElementResult.unwrapErr();
+ }
+ nsresult rv = moveListItemElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ handledListItemElement = std::move(listItemElement);
+ continue;
+ }
+
+ if (!targetDivElement) {
+ // If we meet a `<div>` element, use it as the absolute-position
+ // container.
+ // XXX This looks odd. If there are 2 or more `<div>` elements are
+ // selected, first found `<div>` element will have all other
+ // selected nodes.
+ if (content->IsHTMLElement(nsGkAtoms::div)) {
+ targetDivElement = content->AsElement();
+ MOZ_ASSERT(!createdListElement);
+ MOZ_ASSERT(!handledListItemElement);
+ continue;
+ }
+ // Otherwise, create new `<div>` element to be positioned absolutely
+ // and to contain all selected nodes.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ nsresult rv = createNewDivElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ MOZ_ASSERT(createNewDivElementResult.inspect().GetNewNode());
+ targetDivElement = createNewDivElementResult.unwrap().UnwrapNewNode();
+ }
+
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *targetDivElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.unwrapErr();
+ }
+ nsresult rv = moveNodeResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ // Forget createdListElement, if any
+ createdListElement = nullptr;
+ }
+ *aTargetElement = std::move(targetDivElement);
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::SetSelectionToStaticAsSubAction() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSetPositionToStatic, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ RefPtr<Element> element = GetAbsolutelyPositionedSelectionContainer();
+ if (!element) {
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::GetAbsolutelyPositionedSelectionContainer() returned "
+ "nullptr");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ {
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ nsresult rv = SetPositionToAbsoluteOrStatic(*element, false);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetPositionToAbsoluteOrStatic() failed");
+ return Err(rv);
+ }
+ }
+
+ // Restoring Selection might cause destroying the HTML editor.
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING("Destroying AutoSelectionRestorer caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ return EditActionResult::HandledResult();
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AddZIndexAsSubAction(
+ int32_t aChange) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this,
+ aChange < 0 ? EditSubAction::eDecreaseZIndex
+ : EditSubAction::eIncreaseZIndex,
+ nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ RefPtr<Element> absolutelyPositionedElement =
+ GetAbsolutelyPositionedSelectionContainer();
+ if (!absolutelyPositionedElement) {
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::GetAbsolutelyPositionedSelectionContainer() returned "
+ "nullptr");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ nsStyledElement* absolutelyPositionedStyledElement =
+ nsStyledElement::FromNode(absolutelyPositionedElement);
+ if (NS_WARN_IF(!absolutelyPositionedStyledElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ {
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ // MOZ_KnownLive(*absolutelyPositionedStyledElement): It's
+ // absolutelyPositionedElement whose type is RefPtr.
+ Result<int32_t, nsresult> result = AddZIndexWithTransaction(
+ MOZ_KnownLive(*absolutelyPositionedStyledElement), aChange);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::AddZIndexWithTransaction() failed");
+ return result.propagateErr();
+ }
+ }
+
+ // Restoring Selection might cause destroying the HTML editor.
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING("Destroying AutoSelectionRestorer caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::OnDocumentModified() {
+ if (mPendingDocumentModifiedRunner) {
+ return NS_OK; // We've already posted same runnable into the queue.
+ }
+ mPendingDocumentModifiedRunner = NewRunnableMethod(
+ "HTMLEditor::OnModifyDocument", this, &HTMLEditor::OnModifyDocument);
+ nsContentUtils::AddScriptRunner(do_AddRef(mPendingDocumentModifiedRunner));
+ // Be aware, if OnModifyDocument() may be called synchronously, the
+ // editor might have been destroyed here.
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp
new file mode 100644
index 0000000000..8c3d09c1e2
--- /dev/null
+++ b/editor/libeditor/HTMLEditUtils.cpp
@@ -0,0 +1,2710 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditUtils.h"
+
+#include "AutoRangeArray.h" // for AutoRangeArray
+#include "CSSEditUtils.h" // for CSSEditUtils
+#include "EditAction.h" // for EditAction
+#include "EditorBase.h" // for EditorBase, EditorType
+#include "EditorDOMPoint.h" // for EditorDOMPoint, etc.
+#include "EditorForwards.h" // for CollectChildrenOptions
+#include "EditorUtils.h" // for EditorUtils
+#include "HTMLEditHelpers.h" // for EditorInlineStyle
+#include "WSRunObject.h" // for WSRunScanner
+
+#include "mozilla/ArrayUtils.h" // for ArrayLength
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
+#include "mozilla/Attributes.h"
+#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_
+#include "mozilla/RangeUtils.h" // for RangeUtils
+#include "mozilla/dom/DocumentInlines.h" // for GetBodyElement()
+#include "mozilla/dom/Element.h" // for Element, nsINode
+#include "mozilla/dom/HTMLAnchorElement.h"
+#include "mozilla/dom/HTMLBodyElement.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/ServoCSSParser.h" // for ServoCSSParser
+#include "mozilla/dom/StaticRange.h"
+#include "mozilla/dom/Text.h" // for Text
+
+#include "nsAString.h" // for nsAString::IsEmpty
+#include "nsAtom.h" // for nsAtom
+#include "nsAttrValue.h" // nsAttrValue
+#include "nsCaseTreatment.h"
+#include "nsCOMPtr.h" // for nsCOMPtr, operator==, etc.
+#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle
+#include "nsDebug.h" // for NS_ASSERTION, etc.
+#include "nsElementTable.h" // for nsHTMLElement
+#include "nsError.h" // for NS_SUCCEEDED
+#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::a, etc.
+#include "nsHTMLTags.h"
+#include "nsIFrameInlines.h" // for nsIFrame::IsFlexOrGridItem()
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsNameSpaceManager.h" // for kNameSpaceID_None
+#include "nsPrintfCString.h" // nsPringfCString
+#include "nsString.h" // for nsAutoString
+#include "nsStyledElement.h"
+#include "nsStyleStruct.h" // for StyleDisplay
+#include "nsStyleUtil.h" // for nsStyleUtil
+#include "nsTextFragment.h" // for nsTextFragment
+#include "nsTextFrame.h" // for nsTextFrame
+
+namespace mozilla {
+
+using namespace dom;
+using EditorType = EditorBase::EditorType;
+
+template nsIContent* HTMLEditUtils::GetPreviousContent(
+ const EditorDOMPoint& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetPreviousContent(
+ const EditorRawDOMPoint& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetPreviousContent(
+ const EditorDOMPointInText& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetPreviousContent(
+ const EditorRawDOMPointInText& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorDOMPoint& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorRawDOMPoint& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorDOMPointInText& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorRawDOMPointInText& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter);
+
+template EditorDOMPoint HTMLEditUtils::GetPreviousEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary);
+template EditorRawDOMPoint HTMLEditUtils::GetPreviousEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary);
+template EditorDOMPoint HTMLEditUtils::GetNextEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary);
+template EditorRawDOMPoint HTMLEditUtils::GetNextEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary);
+
+template nsIContent* HTMLEditUtils::GetContentToPreserveInlineStyles(
+ const EditorDOMPoint& aPoint, const Element& aEditingHost);
+template nsIContent* HTMLEditUtils::GetContentToPreserveInlineStyles(
+ const EditorRawDOMPoint& aPoint, const Element& aEditingHost);
+
+template EditorDOMPoint HTMLEditUtils::GetBetterInsertionPointFor(
+ const nsIContent& aContentToInsert, const EditorDOMPoint& aPointToInsert,
+ const Element& aEditingHost);
+template EditorRawDOMPoint HTMLEditUtils::GetBetterInsertionPointFor(
+ const nsIContent& aContentToInsert, const EditorRawDOMPoint& aPointToInsert,
+ const Element& aEditingHost);
+template EditorDOMPoint HTMLEditUtils::GetBetterInsertionPointFor(
+ const nsIContent& aContentToInsert, const EditorRawDOMPoint& aPointToInsert,
+ const Element& aEditingHost);
+template EditorRawDOMPoint HTMLEditUtils::GetBetterInsertionPointFor(
+ const nsIContent& aContentToInsert, const EditorDOMPoint& aPointToInsert,
+ const Element& aEditingHost);
+
+template EditorDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText(
+ const EditorDOMPoint& aPoint, const Element& aEditingHost);
+template EditorDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText(
+ const EditorRawDOMPoint& aPoint, const Element& aEditingHost);
+template EditorRawDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText(
+ const EditorDOMPoint& aPoint, const Element& aEditingHost);
+template EditorRawDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText(
+ const EditorRawDOMPoint& aPoint, const Element& aEditingHost);
+
+template Result<EditorDOMPoint, nsresult>
+HTMLEditUtils::ComputePointToPutCaretInElementIfOutside(
+ const Element& aElement, const EditorDOMPoint& aCurrentPoint);
+template Result<EditorRawDOMPoint, nsresult>
+HTMLEditUtils::ComputePointToPutCaretInElementIfOutside(
+ const Element& aElement, const EditorDOMPoint& aCurrentPoint);
+template Result<EditorDOMPoint, nsresult>
+HTMLEditUtils::ComputePointToPutCaretInElementIfOutside(
+ const Element& aElement, const EditorRawDOMPoint& aCurrentPoint);
+template Result<EditorRawDOMPoint, nsresult>
+HTMLEditUtils::ComputePointToPutCaretInElementIfOutside(
+ const Element& aElement, const EditorRawDOMPoint& aCurrentPoint);
+
+template bool HTMLEditUtils::IsSameCSSColorValue(const nsAString& aColorA,
+ const nsAString& aColorB);
+template bool HTMLEditUtils::IsSameCSSColorValue(const nsACString& aColorA,
+ const nsACString& aColorB);
+
+bool HTMLEditUtils::CanContentsBeJoined(const nsIContent& aLeftContent,
+ const nsIContent& aRightContent) {
+ if (aLeftContent.NodeInfo()->NameAtom() !=
+ aRightContent.NodeInfo()->NameAtom()) {
+ return false;
+ }
+
+ if (!aLeftContent.IsElement()) {
+ return true; // can join text nodes, etc
+ }
+ MOZ_ASSERT(aRightContent.IsElement());
+
+ if (aLeftContent.NodeInfo()->NameAtom() == nsGkAtoms::font) {
+ const nsAttrValue* const leftSize =
+ aLeftContent.AsElement()->GetParsedAttr(nsGkAtoms::size);
+ const nsAttrValue* const rightSize =
+ aRightContent.AsElement()->GetParsedAttr(nsGkAtoms::size);
+ if (!leftSize ^ !rightSize || (leftSize && !leftSize->Equals(*rightSize))) {
+ return false;
+ }
+
+ const nsAttrValue* const leftColor =
+ aLeftContent.AsElement()->GetParsedAttr(nsGkAtoms::color);
+ const nsAttrValue* const rightColor =
+ aRightContent.AsElement()->GetParsedAttr(nsGkAtoms::color);
+ if (!leftColor ^ !rightColor ||
+ (leftColor && !leftColor->Equals(*rightColor))) {
+ return false;
+ }
+
+ const nsAttrValue* const leftFace =
+ aLeftContent.AsElement()->GetParsedAttr(nsGkAtoms::face);
+ const nsAttrValue* const rightFace =
+ aRightContent.AsElement()->GetParsedAttr(nsGkAtoms::face);
+ if (!leftFace ^ !rightFace || (leftFace && !leftFace->Equals(*rightFace))) {
+ return false;
+ }
+ }
+ nsStyledElement* leftStyledElement =
+ nsStyledElement::FromNode(const_cast<nsIContent*>(&aLeftContent));
+ if (!leftStyledElement) {
+ return false;
+ }
+ nsStyledElement* rightStyledElement =
+ nsStyledElement::FromNode(const_cast<nsIContent*>(&aRightContent));
+ if (!rightStyledElement) {
+ return false;
+ }
+ return CSSEditUtils::DoStyledElementsHaveSameStyle(*leftStyledElement,
+ *rightStyledElement);
+}
+
+static bool IsHTMLBlockElementByDefault(const nsIContent& aContent) {
+ if (!aContent.IsHTMLElement()) {
+ return false;
+ }
+ if (aContent.IsHTMLElement(nsGkAtoms::br)) { // shortcut for TextEditor
+ MOZ_ASSERT(!nsHTMLElement::IsBlock(
+ nsHTMLTags::CaseSensitiveAtomTagToId(nsGkAtoms::br)));
+ return false;
+ }
+ // We want to treat these as block nodes even though nsHTMLElement says
+ // they're not.
+ if (aContent.IsAnyOfHTMLElements(
+ nsGkAtoms::body, nsGkAtoms::head, nsGkAtoms::tbody, nsGkAtoms::thead,
+ nsGkAtoms::tfoot, nsGkAtoms::tr, nsGkAtoms::th, nsGkAtoms::td,
+ nsGkAtoms::dt, nsGkAtoms::dd)) {
+ return true;
+ }
+
+ return nsHTMLElement::IsBlock(
+ nsHTMLTags::CaseSensitiveAtomTagToId(aContent.NodeInfo()->NameAtom()));
+}
+
+bool HTMLEditUtils::IsBlockElement(const nsIContent& aContent,
+ BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(aBlockInlineCheck != BlockInlineCheck::Unused);
+
+ if (MOZ_UNLIKELY(!aContent.IsElement())) {
+ return false;
+ }
+ // If it's a <br>, we should always treat it as an inline element because
+ // its preceding collapse white-spaces and another <br> works same as usual
+ // even if you set its style to `display:block`.
+ if (aContent.IsHTMLElement(nsGkAtoms::br)) {
+ return false;
+ }
+ if (!StaticPrefs::editor_block_inline_check_use_computed_style() ||
+ aBlockInlineCheck == BlockInlineCheck::UseHTMLDefaultStyle) {
+ return IsHTMLBlockElementByDefault(aContent);
+ }
+ // Let's treat the document element and the body element is a block to avoid
+ // complicated things which may be detected by fuzzing.
+ if (aContent.OwnerDoc()->GetDocumentElement() == &aContent ||
+ (aContent.IsHTMLElement(nsGkAtoms::body) &&
+ aContent.OwnerDoc()->GetBodyElement() == &aContent)) {
+ return true;
+ }
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(aContent.AsElement());
+ if (MOZ_UNLIKELY(!elementStyle)) { // If aContent is not in the composed tree
+ return IsHTMLBlockElementByDefault(aContent);
+ }
+ const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay();
+ if (MOZ_UNLIKELY(styleDisplay->mDisplay == StyleDisplay::None)) {
+ // Typically, we should not keep handling editing in invisible nodes, but if
+ // we reach here, let's fallback to the default style for protecting the
+ // structure as far as possible.
+ return IsHTMLBlockElementByDefault(aContent);
+ }
+ // Both Blink and WebKit treat ruby style as a block, see IsEnclosingBlock()
+ // in Chromium or isBlock() in WebKit.
+ if (styleDisplay->IsRubyDisplayType()) {
+ return true;
+ }
+ // If the outside is not inline, treat it as block.
+ if (!styleDisplay->IsInlineOutsideStyle()) {
+ return true;
+ }
+ // If we're checking display-inside, inline-block, etc should be a block too.
+ return aBlockInlineCheck == BlockInlineCheck::UseComputedDisplayStyle &&
+ styleDisplay->DisplayInside() == StyleDisplayInside::FlowRoot &&
+ // Treat widgets as inline since they won't hide collapsible
+ // white-spaces around them.
+ styleDisplay->EffectiveAppearance() == StyleAppearance::None;
+}
+
+bool HTMLEditUtils::IsInlineContent(const nsIContent& aContent,
+ BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(aBlockInlineCheck != BlockInlineCheck::Unused);
+
+ if (!aContent.IsElement()) {
+ return true;
+ }
+ // If it's a <br>, we should always treat it as an inline element because
+ // its preceding collapse white-spaces and another <br> works same as usual
+ // even if you set its style to `display:block`.
+ if (aContent.IsHTMLElement(nsGkAtoms::br)) {
+ return true;
+ }
+ if (!StaticPrefs::editor_block_inline_check_use_computed_style() ||
+ aBlockInlineCheck == BlockInlineCheck::UseHTMLDefaultStyle) {
+ return !IsHTMLBlockElementByDefault(aContent);
+ }
+ // Let's treat the document element and the body element is a block to avoid
+ // complicated things which may be detected by fuzzing.
+ if (aContent.OwnerDoc()->GetDocumentElement() == &aContent ||
+ (aContent.IsHTMLElement(nsGkAtoms::body) &&
+ aContent.OwnerDoc()->GetBodyElement() == &aContent)) {
+ return false;
+ }
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(aContent.AsElement());
+ if (MOZ_UNLIKELY(!elementStyle)) { // If aContent is not in the composed tree
+ return !IsHTMLBlockElementByDefault(aContent);
+ }
+ const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay();
+ if (MOZ_UNLIKELY(styleDisplay->mDisplay == StyleDisplay::None)) {
+ // Similar to IsBlockElement, let's fallback to refer the default style.
+ // Note that if you change here, you may need to check the parent element
+ // style if aContent.
+ return !IsHTMLBlockElementByDefault(aContent);
+ }
+ // Different block IsBlockElement, when the display-outside is inline, it's
+ // simply an inline element.
+ return styleDisplay->IsInlineOutsideStyle() ||
+ styleDisplay->IsRubyDisplayType();
+}
+
+bool HTMLEditUtils::IsFlexOrGridItem(const Element& aElement) {
+ nsIFrame* frame = aElement.GetPrimaryFrame();
+ return frame && frame->IsFlexOrGridItem();
+}
+
+bool HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone(
+ const nsIContent& aContent) {
+ if (NS_WARN_IF(!aContent.IsInComposedDoc())) {
+ return true;
+ }
+ for (const Element* element :
+ aContent.InclusiveFlatTreeAncestorsOfType<Element>()) {
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(element);
+ if (NS_WARN_IF(!elementStyle)) {
+ continue;
+ }
+ const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay();
+ if (MOZ_UNLIKELY(styleDisplay->mDisplay == StyleDisplay::None)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool HTMLEditUtils::IsVisibleElementEvenIfLeafNode(const nsIContent& aContent) {
+ if (!aContent.IsElement()) {
+ return false;
+ }
+ // Assume non-HTML element is visible.
+ if (!aContent.IsHTMLElement()) {
+ return true;
+ }
+ // XXX Should we return false if the element is display:none?
+ if (HTMLEditUtils::IsBlockElement(
+ aContent, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return true;
+ }
+ if (aContent.IsAnyOfHTMLElements(nsGkAtoms::applet, nsGkAtoms::iframe,
+ nsGkAtoms::img, nsGkAtoms::meter,
+ nsGkAtoms::progress, nsGkAtoms::select,
+ nsGkAtoms::textarea)) {
+ return true;
+ }
+ if (const HTMLInputElement* inputElement =
+ HTMLInputElement::FromNode(&aContent)) {
+ return inputElement->ControlType() != FormControlType::InputHidden;
+ }
+ // Maybe, empty inline element such as <span>.
+ return false;
+}
+
+/**
+ * IsInlineStyle() returns true if aNode is an inline style.
+ */
+bool HTMLEditUtils::IsInlineStyle(nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(
+ nsGkAtoms::b, nsGkAtoms::i, nsGkAtoms::u, nsGkAtoms::tt, nsGkAtoms::s,
+ nsGkAtoms::strike, nsGkAtoms::big, nsGkAtoms::small, nsGkAtoms::sub,
+ nsGkAtoms::sup, nsGkAtoms::font);
+}
+
+bool HTMLEditUtils::IsDisplayOutsideInline(const Element& aElement) {
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(&aElement);
+ if (!elementStyle) {
+ return false;
+ }
+ return elementStyle->StyleDisplay()->DisplayOutside() ==
+ StyleDisplayOutside::Inline;
+}
+
+bool HTMLEditUtils::IsDisplayInsideFlowRoot(const Element& aElement) {
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(&aElement);
+ if (!elementStyle) {
+ return false;
+ }
+ return elementStyle->StyleDisplay()->DisplayInside() ==
+ StyleDisplayInside::FlowRoot;
+}
+
+bool HTMLEditUtils::IsRemovableInlineStyleElement(Element& aElement) {
+ if (!aElement.IsHTMLElement()) {
+ return false;
+ }
+ // https://w3c.github.io/editing/execCommand.html#removeformat-candidate
+ if (aElement.IsAnyOfHTMLElements(
+ nsGkAtoms::abbr, // Chrome ignores, but does not make sense.
+ nsGkAtoms::acronym, nsGkAtoms::b,
+ nsGkAtoms::bdi, // Chrome ignores, but does not make sense.
+ nsGkAtoms::bdo, nsGkAtoms::big, nsGkAtoms::cite, nsGkAtoms::code,
+ // nsGkAtoms::del, Chrome ignores, but does not make sense but
+ // execCommand unofficial draft excludes this. Spec issue:
+ // https://github.com/w3c/editing/issues/192
+ nsGkAtoms::dfn, nsGkAtoms::em, nsGkAtoms::font, nsGkAtoms::i,
+ nsGkAtoms::ins, nsGkAtoms::kbd,
+ nsGkAtoms::mark, // Chrome ignores, but does not make sense.
+ nsGkAtoms::nobr, nsGkAtoms::q, nsGkAtoms::s, nsGkAtoms::samp,
+ nsGkAtoms::small, nsGkAtoms::span, nsGkAtoms::strike,
+ nsGkAtoms::strong, nsGkAtoms::sub, nsGkAtoms::sup, nsGkAtoms::tt,
+ nsGkAtoms::u, nsGkAtoms::var)) {
+ return true;
+ }
+ // If it's a <blink> element, we can remove it.
+ nsAutoString tagName;
+ aElement.GetTagName(tagName);
+ return tagName.LowerCaseEqualsASCII("blink");
+}
+
+/**
+ * IsNodeThatCanOutdent() returns true if aNode is a list, list item or
+ * blockquote.
+ */
+bool HTMLEditUtils::IsNodeThatCanOutdent(nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::ul, nsGkAtoms::ol, nsGkAtoms::dl,
+ nsGkAtoms::li, nsGkAtoms::dd, nsGkAtoms::dt,
+ nsGkAtoms::blockquote);
+}
+
+/**
+ * IsHeader() returns true if aNode is an html header.
+ */
+bool HTMLEditUtils::IsHeader(nsINode& aNode) {
+ return aNode.IsAnyOfHTMLElements(nsGkAtoms::h1, nsGkAtoms::h2, nsGkAtoms::h3,
+ nsGkAtoms::h4, nsGkAtoms::h5, nsGkAtoms::h6);
+}
+
+/**
+ * IsListItem() returns true if aNode is an html list item.
+ */
+bool HTMLEditUtils::IsListItem(const nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::dd,
+ nsGkAtoms::dt);
+}
+
+/**
+ * IsAnyTableElement() returns true if aNode is an html table, td, tr, ...
+ */
+bool HTMLEditUtils::IsAnyTableElement(const nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(
+ nsGkAtoms::table, nsGkAtoms::tr, nsGkAtoms::td, nsGkAtoms::th,
+ nsGkAtoms::thead, nsGkAtoms::tfoot, nsGkAtoms::tbody, nsGkAtoms::caption);
+}
+
+/**
+ * IsAnyTableElementButNotTable() returns true if aNode is an html td, tr, ...
+ * (doesn't include table)
+ */
+bool HTMLEditUtils::IsAnyTableElementButNotTable(nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::tr, nsGkAtoms::td, nsGkAtoms::th,
+ nsGkAtoms::thead, nsGkAtoms::tfoot,
+ nsGkAtoms::tbody, nsGkAtoms::caption);
+}
+
+/**
+ * IsTable() returns true if aNode is an html table.
+ */
+bool HTMLEditUtils::IsTable(nsINode* aNode) {
+ return aNode && aNode->IsHTMLElement(nsGkAtoms::table);
+}
+
+/**
+ * IsTableRow() returns true if aNode is an html tr.
+ */
+bool HTMLEditUtils::IsTableRow(nsINode* aNode) {
+ return aNode && aNode->IsHTMLElement(nsGkAtoms::tr);
+}
+
+/**
+ * IsTableCell() returns true if aNode is an html td or th.
+ */
+bool HTMLEditUtils::IsTableCell(const nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
+}
+
+/**
+ * IsTableCellOrCaption() returns true if aNode is an html td or th or caption.
+ */
+bool HTMLEditUtils::IsTableCellOrCaption(nsINode& aNode) {
+ return aNode.IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th,
+ nsGkAtoms::caption);
+}
+
+/**
+ * IsAnyListElement() returns true if aNode is an html list.
+ */
+bool HTMLEditUtils::IsAnyListElement(const nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::ul, nsGkAtoms::ol,
+ nsGkAtoms::dl);
+}
+
+/**
+ * IsPre() returns true if aNode is an html pre node.
+ */
+bool HTMLEditUtils::IsPre(const nsINode* aNode) {
+ return aNode && aNode->IsHTMLElement(nsGkAtoms::pre);
+}
+
+/**
+ * IsImage() returns true if aNode is an html image node.
+ */
+bool HTMLEditUtils::IsImage(nsINode* aNode) {
+ return aNode && aNode->IsHTMLElement(nsGkAtoms::img);
+}
+
+bool HTMLEditUtils::IsLink(const nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+
+ if (!aNode->IsContent()) {
+ return false;
+ }
+
+ RefPtr<const dom::HTMLAnchorElement> anchor =
+ dom::HTMLAnchorElement::FromNodeOrNull(aNode->AsContent());
+ if (!anchor) {
+ return false;
+ }
+
+ nsAutoString tmpText;
+ anchor->GetHref(tmpText);
+ return !tmpText.IsEmpty();
+}
+
+bool HTMLEditUtils::IsNamedAnchor(const nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ if (!aNode->IsHTMLElement(nsGkAtoms::a)) {
+ return false;
+ }
+
+ nsAutoString text;
+ return aNode->AsElement()->GetAttr(nsGkAtoms::name, text) && !text.IsEmpty();
+}
+
+/**
+ * IsMozDiv() returns true if aNode is an html div node with |type = _moz|.
+ */
+bool HTMLEditUtils::IsMozDiv(nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsHTMLElement(nsGkAtoms::div) &&
+ aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ u"_moz"_ns, eIgnoreCase);
+}
+
+/**
+ * IsMailCite() returns true if aNode is an html blockquote with |type=cite|.
+ */
+bool HTMLEditUtils::IsMailCite(const Element& aElement) {
+ // don't ask me why, but our html mailcites are id'd by "type=cite"...
+ if (aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, u"cite"_ns,
+ eIgnoreCase)) {
+ return true;
+ }
+
+ // ... but our plaintext mailcites by "_moz_quote=true". go figure.
+ if (aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozquote, u"true"_ns,
+ eIgnoreCase)) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * IsFormWidget() returns true if aNode is a form widget of some kind.
+ */
+bool HTMLEditUtils::IsFormWidget(const nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(nsGkAtoms::textarea, nsGkAtoms::select,
+ nsGkAtoms::button, nsGkAtoms::output,
+ nsGkAtoms::progress, nsGkAtoms::meter,
+ nsGkAtoms::input);
+}
+
+bool HTMLEditUtils::SupportsAlignAttr(nsINode& aNode) {
+ return aNode.IsAnyOfHTMLElements(
+ nsGkAtoms::hr, nsGkAtoms::table, nsGkAtoms::tbody, nsGkAtoms::tfoot,
+ nsGkAtoms::thead, nsGkAtoms::tr, nsGkAtoms::td, nsGkAtoms::th,
+ nsGkAtoms::div, nsGkAtoms::p, nsGkAtoms::h1, nsGkAtoms::h2, nsGkAtoms::h3,
+ nsGkAtoms::h4, nsGkAtoms::h5, nsGkAtoms::h6);
+}
+
+bool HTMLEditUtils::IsVisibleTextNode(const Text& aText) {
+ if (!aText.TextDataLength()) {
+ return false;
+ }
+
+ Maybe<uint32_t> visibleCharOffset =
+ HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
+ EditorDOMPointInText(&aText, 0));
+ if (visibleCharOffset.isSome()) {
+ return true;
+ }
+
+ // Now, all characters in aText is collapsible white-spaces. The node is
+ // invisible if next to block boundary.
+ return !HTMLEditUtils::GetElementOfImmediateBlockBoundary(
+ aText, WalkTreeDirection::Forward) &&
+ !HTMLEditUtils::GetElementOfImmediateBlockBoundary(
+ aText, WalkTreeDirection::Backward);
+}
+
+bool HTMLEditUtils::IsInVisibleTextFrames(nsPresContext* aPresContext,
+ const Text& aText) {
+ // TODO(dholbert): aPresContext is now unused; maybe we can remove it, here
+ // and in IsEmptyNode? We do use it as a signal (implicitly here,
+ // more-explicitly in IsEmptyNode) that we are in a "SafeToAskLayout" case...
+ // If/when we remove it, we should be sure we're not losing that signal of
+ // strictness, since this function here does absolutely need to query layout.
+ MOZ_ASSERT(aPresContext);
+
+ if (!aText.TextDataLength()) {
+ return false;
+ }
+
+ nsTextFrame* textFrame = do_QueryFrame(aText.GetPrimaryFrame());
+ if (!textFrame) {
+ return false;
+ }
+
+ return textFrame->HasVisibleText();
+}
+
+Element* HTMLEditUtils::GetElementOfImmediateBlockBoundary(
+ const nsIContent& aContent, const WalkTreeDirection aDirection) {
+ MOZ_ASSERT(aContent.IsHTMLElement(nsGkAtoms::br) || aContent.IsText());
+
+ // First, we get a block container. This is not designed for reaching
+ // no block boundaries in the tree.
+ Element* maybeNonEditableAncestorBlock = HTMLEditUtils::GetAncestorElement(
+ aContent, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!maybeNonEditableAncestorBlock)) {
+ return nullptr;
+ }
+
+ auto getNextContent = [&aDirection, &maybeNonEditableAncestorBlock](
+ const nsIContent& aContent) -> nsIContent* {
+ return aDirection == WalkTreeDirection::Forward
+ ? HTMLEditUtils::GetNextContent(
+ aContent,
+ {WalkTreeOption::IgnoreDataNodeExceptText,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle,
+ maybeNonEditableAncestorBlock)
+ : HTMLEditUtils::GetPreviousContent(
+ aContent,
+ {WalkTreeOption::IgnoreDataNodeExceptText,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle,
+ maybeNonEditableAncestorBlock);
+ };
+
+ // Then, scan block element boundary while we don't see visible things.
+ const bool isBRElement = aContent.IsHTMLElement(nsGkAtoms::br);
+ for (nsIContent* nextContent = getNextContent(aContent); nextContent;
+ nextContent = getNextContent(*nextContent)) {
+ if (nextContent->IsElement()) {
+ // Break is right before a child block, it's not visible
+ if (HTMLEditUtils::IsBlockElement(
+ *nextContent, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return nextContent->AsElement();
+ }
+
+ // XXX How about other non-HTML elements? Assume they are styled as
+ // blocks for now.
+ if (!nextContent->IsHTMLElement()) {
+ return nextContent->AsElement();
+ }
+
+ // If there is a visible content which generates something visible,
+ // stop scanning.
+ if (HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*nextContent)) {
+ return nullptr;
+ }
+
+ if (nextContent->IsHTMLElement(nsGkAtoms::br)) {
+ // If aContent is a <br> element, another <br> element prevents the
+ // block boundary special handling.
+ if (isBRElement) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(aContent.IsText());
+ // Following <br> element always hides its following block boundary.
+ // I.e., white-spaces is at end of the text node is visible.
+ if (aDirection == WalkTreeDirection::Forward) {
+ return nullptr;
+ }
+ // Otherwise, if text node follows <br> element, its white-spaces at
+ // start of the text node are invisible. In this case, we return
+ // the found <br> element.
+ return nextContent->AsElement();
+ }
+
+ continue;
+ }
+
+ switch (nextContent->NodeType()) {
+ case nsINode::TEXT_NODE:
+ case nsINode::CDATA_SECTION_NODE:
+ break;
+ default:
+ continue;
+ }
+
+ Text* textNode = Text::FromNode(nextContent);
+ MOZ_ASSERT(textNode);
+ if (!textNode->TextLength()) {
+ continue; // empty invisible text node, keep scanning next one.
+ }
+ if (HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone(*textNode)) {
+ continue; // Styled as invisible.
+ }
+ if (!textNode->TextIsOnlyWhitespace()) {
+ return nullptr; // found a visible text node.
+ }
+ const nsTextFragment& textFragment = textNode->TextFragment();
+ const bool isWhiteSpacePreformatted =
+ EditorUtils::IsWhiteSpacePreformatted(*textNode);
+ const bool isNewLinePreformatted =
+ EditorUtils::IsNewLinePreformatted(*textNode);
+ if (!isWhiteSpacePreformatted && !isNewLinePreformatted) {
+ // if the white-space only text node is not preformatted, ignore it.
+ continue;
+ }
+ for (uint32_t i = 0; i < textFragment.GetLength(); i++) {
+ if (textFragment.CharAt(i) == HTMLEditUtils::kNewLine) {
+ if (isNewLinePreformatted) {
+ return nullptr; // found a visible text node.
+ }
+ continue;
+ }
+ if (isWhiteSpacePreformatted) {
+ return nullptr; // found a visible text node.
+ }
+ }
+ // All white-spaces in the text node is invisible, keep scanning next one.
+ }
+
+ // There is no visible content and reached current block boundary. Then,
+ // the <br> element is the last content in the block and invisible.
+ // XXX Should we treat it visible if it's the only child of a block?
+ return maybeNonEditableAncestorBlock;
+}
+
+nsIContent* HTMLEditUtils::GetUnnecessaryLineBreakContent(
+ const Element& aBlockElement, ScanLineBreak aScanLineBreak) {
+ auto* lastLineBreakContent = [&]() -> nsIContent* {
+ const LeafNodeTypes leafNodeOrNonEditableNode{
+ LeafNodeType::LeafNodeOrNonEditableNode};
+ const WalkTreeOptions onlyPrecedingLine{
+ WalkTreeOption::StopAtBlockBoundary};
+ for (nsIContent* content =
+ aScanLineBreak == ScanLineBreak::AtEndOfBlock
+ ? HTMLEditUtils::GetLastLeafContent(aBlockElement,
+ leafNodeOrNonEditableNode)
+ : HTMLEditUtils::GetPreviousContent(
+ aBlockElement, onlyPrecedingLine,
+ BlockInlineCheck::UseComputedDisplayStyle,
+ aBlockElement.GetParentElement());
+ content;
+ content =
+ aScanLineBreak == ScanLineBreak::AtEndOfBlock
+ ? HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *content, aBlockElement, leafNodeOrNonEditableNode,
+ BlockInlineCheck::UseComputedDisplayStyle)
+ : HTMLEditUtils::GetPreviousContent(
+ *content, onlyPrecedingLine,
+ BlockInlineCheck::UseComputedDisplayStyle,
+ aBlockElement.GetParentElement())) {
+ // If we're scanning preceding <br> element of aBlockElement, we don't
+ // need to look for a line break in another block because the caller
+ // needs to handle only preceding <br> element of aBlockElement.
+ if (aScanLineBreak == ScanLineBreak::BeforeBlock &&
+ HTMLEditUtils::IsBlockElement(
+ *content, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return nullptr;
+ }
+ if (Text* textNode = Text::FromNode(content)) {
+ if (!textNode->TextLength()) {
+ continue; // ignore empty text node
+ }
+ const nsTextFragment& textFragment = textNode->TextFragment();
+ if (EditorUtils::IsNewLinePreformatted(*textNode) &&
+ textFragment.CharAt(textFragment.GetLength() - 1u) ==
+ HTMLEditUtils::kNewLine) {
+ // If the text node ends with a preserved line break, it's unnecessary
+ // unless it follows another preformatted line break.
+ if (textFragment.GetLength() == 1u) {
+ return textNode; // Need to scan previous leaf.
+ }
+ return textFragment.CharAt(textFragment.GetLength() - 2u) ==
+ HTMLEditUtils::kNewLine
+ ? nullptr
+ : textNode;
+ }
+ if (HTMLEditUtils::IsVisibleTextNode(*textNode)) {
+ return nullptr;
+ }
+ continue;
+ }
+ if (content->IsCharacterData()) {
+ continue; // ignore hidden character data nodes like comment
+ }
+ if (content->IsHTMLElement(nsGkAtoms::br)) {
+ return content;
+ }
+ // If found element is empty block or visible element, there is no
+ // unnecessary line break.
+ if (HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*content)) {
+ return nullptr;
+ }
+ // Otherwise, e.g., empty <b>, we should keep scanning.
+ }
+ return nullptr;
+ }();
+ if (!lastLineBreakContent) {
+ return nullptr;
+ }
+
+ // If the found node is a text node and contains only one preformatted new
+ // line break, we need to keep scanning previous one, but if it has 2 or more
+ // characters, we know it has redundant line break.
+ Text* lastLineBreakText = Text::FromNode(lastLineBreakContent);
+ if (lastLineBreakText && lastLineBreakText->TextDataLength() != 1u) {
+ return lastLineBreakText;
+ }
+
+ // Scan previous leaf content, but now, we can stop at child block boundary.
+ const LeafNodeTypes leafNodeOrNonEditableNodeOrChildBlock{
+ LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock};
+ const Element* blockElement = HTMLEditUtils::GetAncestorElement(
+ *lastLineBreakContent, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ for (nsIContent* content =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *lastLineBreakContent, *blockElement,
+ leafNodeOrNonEditableNodeOrChildBlock,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ content;
+ content = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *content, *blockElement, leafNodeOrNonEditableNodeOrChildBlock,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ if (HTMLEditUtils::IsBlockElement(
+ *content, BlockInlineCheck::UseComputedDisplayStyle) ||
+ (content->IsElement() && !content->IsHTMLElement())) {
+ // Now, must found <div>...<div>...</div><br></div>
+ // ^^^^
+ // In this case, the <br> element is necessary to make a following empty
+ // line of the inner <div> visible.
+ return nullptr;
+ }
+ if (Text* textNode = Text::FromNode(content)) {
+ if (!textNode->TextLength()) {
+ continue; // ignore empty text node
+ }
+ const nsTextFragment& textFragment = textNode->TextFragment();
+ if (EditorUtils::IsNewLinePreformatted(*textNode) &&
+ textFragment.CharAt(textFragment.GetLength() - 1u) ==
+ HTMLEditUtils::kNewLine) {
+ // So, we are here because the preformatted line break is followed by
+ // lastLineBreakContent which is <br> or a text node containing only
+ // one. In this case, even if their parents are different,
+ // lastLineBreakContent is necessary to make the last line visible.
+ return nullptr;
+ }
+ if (!HTMLEditUtils::IsVisibleTextNode(*textNode)) {
+ continue;
+ }
+ if (EditorUtils::IsWhiteSpacePreformatted(*textNode)) {
+ // If the white-space is preserved, neither following <br> nor a
+ // preformatted line break is not necessary.
+ return lastLineBreakContent;
+ }
+ // Otherwise, only if the last character is a collapsible white-space,
+ // we need lastLineBreakContent to make the trailing white-space visible.
+ switch (textFragment.CharAt(textFragment.GetLength() - 1u)) {
+ case HTMLEditUtils::kSpace:
+ case HTMLEditUtils::kCarriageReturn:
+ case HTMLEditUtils::kTab:
+ case HTMLEditUtils::kNBSP:
+ return nullptr;
+ default:
+ return lastLineBreakContent;
+ }
+ }
+ if (content->IsCharacterData()) {
+ continue; // ignore hidden character data nodes like comment
+ }
+ // If lastLineBreakContent follows a <br> element in same block, it's
+ // necessary to make the empty last line visible.
+ if (content->IsHTMLElement(nsGkAtoms::br)) {
+ return nullptr;
+ }
+ // If it follows a visible element, it's unnecessary line break.
+ if (HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*content)) {
+ return lastLineBreakContent;
+ }
+ // Otherwise, ignore empty inline elements such as <b>.
+ }
+ // If the block is empty except invisible data nodes and lastLineBreakContent,
+ // lastLineBreakContent is necessary to make the block visible.
+ return nullptr;
+}
+
+bool HTMLEditUtils::IsEmptyNode(nsPresContext* aPresContext,
+ const nsINode& aNode,
+ const EmptyCheckOptions& aOptions /* = {} */,
+ bool* aSeenBR /* = nullptr */) {
+ MOZ_ASSERT_IF(aOptions.contains(EmptyCheckOption::SafeToAskLayout),
+ aPresContext);
+
+ if (aSeenBR) {
+ *aSeenBR = false;
+ }
+
+ if (const Text* text = Text::FromNode(&aNode)) {
+ return aOptions.contains(EmptyCheckOption::SafeToAskLayout)
+ ? !IsInVisibleTextFrames(aPresContext, *text)
+ : !IsVisibleTextNode(*text);
+ }
+
+ if (!aNode.IsElement()) {
+ return false;
+ }
+
+ if (
+ // If it's not a container such as an <hr> or <br>, etc, it should be
+ // treated as not empty.
+ !IsContainerNode(*aNode.AsContent()) ||
+ // If it's a named anchor, we shouldn't treat it as empty because it
+ // has special meaning even if invisible.
+ IsNamedAnchor(&aNode) ||
+ // Form widgets should be treated as not empty because they have special
+ // meaning even if invisible.
+ IsFormWidget(&aNode)) {
+ return false;
+ }
+
+ const auto [isListItem, isTableCell, hasAppearance] =
+ [&]() MOZ_NEVER_INLINE_DEBUG -> std::tuple<bool, bool, bool> {
+ if (!StaticPrefs::editor_block_inline_check_use_computed_style()) {
+ return {IsListItem(&aNode), IsTableCell(&aNode), false};
+ }
+ // Let's stop treating the document element and the <body> as a list item
+ // nor a table cell to avoid tricky cases.
+ if (aNode.OwnerDoc()->GetDocumentElement() == &aNode ||
+ (aNode.IsHTMLElement(nsGkAtoms::body) &&
+ aNode.OwnerDoc()->GetBodyElement() == &aNode)) {
+ return {false, false, false};
+ }
+
+ RefPtr<const ComputedStyle> elementStyle =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(aNode.AsElement());
+ // If there is no style information like in a document fragment, let's refer
+ // the default style.
+ if (MOZ_UNLIKELY(!elementStyle)) {
+ return {IsListItem(&aNode), IsTableCell(&aNode), false};
+ }
+ const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay();
+ if (NS_WARN_IF(!styleDisplay)) {
+ return {IsListItem(&aNode), IsTableCell(&aNode), false};
+ }
+ if (styleDisplay->mDisplay != StyleDisplay::None &&
+ styleDisplay->HasAppearance()) {
+ return {false, false, true};
+ }
+ if (styleDisplay->IsListItem()) {
+ return {true, false, false};
+ }
+ if (styleDisplay->mDisplay == StyleDisplay::TableCell) {
+ return {false, true, false};
+ }
+ // The default display of <dt> and <dd> is block. Therefore, we need
+ // special handling for them.
+ return {styleDisplay->mDisplay == StyleDisplay::Block &&
+ aNode.IsAnyOfHTMLElements(nsGkAtoms::dd, nsGkAtoms::dt),
+ false, false};
+ }();
+
+ // The web author created native widget without form control elements. Let's
+ // treat it as visible.
+ if (hasAppearance) {
+ return false;
+ }
+
+ if (isListItem &&
+ aOptions.contains(EmptyCheckOption::TreatListItemAsVisible)) {
+ return false;
+ }
+ if (isTableCell &&
+ aOptions.contains(EmptyCheckOption::TreatTableCellAsVisible)) {
+ return false;
+ }
+
+ bool seenBR = aSeenBR && *aSeenBR;
+ for (nsIContent* childContent = aNode.GetFirstChild(); childContent;
+ childContent = childContent->GetNextSibling()) {
+ // Is the child editable and non-empty? if so, return false
+ if (aOptions.contains(
+ EmptyCheckOption::TreatNonEditableContentAsInvisible) &&
+ !EditorUtils::IsEditableContent(*childContent, EditorType::HTML)) {
+ continue;
+ }
+
+ if (Text* text = Text::FromNode(childContent)) {
+ // break out if we find we aren't empty
+ if (aOptions.contains(EmptyCheckOption::SafeToAskLayout)
+ ? IsInVisibleTextFrames(aPresContext, *text)
+ : IsVisibleTextNode(*text)) {
+ return false;
+ }
+ continue;
+ }
+
+ MOZ_ASSERT(childContent != &aNode);
+
+ if (!aOptions.contains(EmptyCheckOption::TreatSingleBRElementAsVisible) &&
+ !seenBR && childContent->IsHTMLElement(nsGkAtoms::br)) {
+ // Ignore first <br> element in it if caller wants so because it's
+ // typically a padding <br> element of for a parent block.
+ seenBR = true;
+ if (aSeenBR) {
+ *aSeenBR = true;
+ }
+ continue;
+ }
+
+ // Note: list items or table cells are not considered empty
+ // if they contain other lists or tables
+ EmptyCheckOptions options(aOptions);
+ if (childContent->IsElement() && (isListItem || isTableCell)) {
+ options += {EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible};
+ }
+ if (!IsEmptyNode(aPresContext, *childContent, options, &seenBR)) {
+ if (aSeenBR) {
+ *aSeenBR = seenBR;
+ }
+ return false;
+ }
+ }
+
+ if (aSeenBR) {
+ *aSeenBR = seenBR;
+ }
+ return true;
+}
+
+bool HTMLEditUtils::ShouldInsertLinefeedCharacter(
+ const EditorDOMPoint& aPointToInsert, const Element& aEditingHost) {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ if (!aPointToInsert.IsInContentNode()) {
+ return false;
+ }
+
+ // closestEditableBlockElement can be nullptr if aEditingHost is an inline
+ // element.
+ Element* closestEditableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *aPointToInsert.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+
+ // If and only if the nearest block is the editing host or its parent,
+ // and new line character is preformatted, we should insert a linefeed.
+ return (!closestEditableBlockElement ||
+ closestEditableBlockElement == &aEditingHost) &&
+ EditorUtils::IsNewLinePreformatted(
+ *aPointToInsert.ContainerAs<nsIContent>());
+}
+
+// We use bitmasks to test containment of elements. Elements are marked to be
+// in certain groups by setting the mGroup member of the `ElementInfo` struct
+// to the corresponding GROUP_ values (OR'ed together). Similarly, elements are
+// marked to allow containment of certain groups by setting the
+// mCanContainGroups member of the `ElementInfo` struct to the corresponding
+// GROUP_ values (OR'ed together).
+// Testing containment then simply consists of checking whether the
+// mCanContainGroups bitmask of an element and the mGroup bitmask of a
+// potential child overlap.
+
+#define GROUP_NONE 0
+
+// body, head, html
+#define GROUP_TOPLEVEL (1 << 1)
+
+// base, link, meta, script, style, title
+#define GROUP_HEAD_CONTENT (1 << 2)
+
+// b, big, i, s, small, strike, tt, u
+#define GROUP_FONTSTYLE (1 << 3)
+
+// abbr, acronym, cite, code, datalist, del, dfn, em, ins, kbd, mark, rb, rp
+// rt, rtc, ruby, samp, strong, var
+#define GROUP_PHRASE (1 << 4)
+
+// a, applet, basefont, bdi, bdo, br, font, iframe, img, map, meter, object,
+// output, picture, progress, q, script, span, sub, sup
+#define GROUP_SPECIAL (1 << 5)
+
+// button, form, input, label, select, textarea
+#define GROUP_FORMCONTROL (1 << 6)
+
+// address, applet, article, aside, blockquote, button, center, del, details,
+// dialog, dir, div, dl, fieldset, figure, footer, form, h1, h2, h3, h4, h5,
+// h6, header, hgroup, hr, iframe, ins, main, map, menu, nav, noframes,
+// noscript, object, ol, p, pre, table, search, section, summary, ul
+#define GROUP_BLOCK (1 << 7)
+
+// frame, frameset
+#define GROUP_FRAME (1 << 8)
+
+// col, tbody
+#define GROUP_TABLE_CONTENT (1 << 9)
+
+// tr
+#define GROUP_TBODY_CONTENT (1 << 10)
+
+// td, th
+#define GROUP_TR_CONTENT (1 << 11)
+
+// col
+#define GROUP_COLGROUP_CONTENT (1 << 12)
+
+// param
+#define GROUP_OBJECT_CONTENT (1 << 13)
+
+// li
+#define GROUP_LI (1 << 14)
+
+// area
+#define GROUP_MAP_CONTENT (1 << 15)
+
+// optgroup, option
+#define GROUP_SELECT_CONTENT (1 << 16)
+
+// option
+#define GROUP_OPTIONS (1 << 17)
+
+// dd, dt
+#define GROUP_DL_CONTENT (1 << 18)
+
+// p
+#define GROUP_P (1 << 19)
+
+// text, white-space, newline, comment
+#define GROUP_LEAF (1 << 20)
+
+// XXX This is because the editor does sublists illegally.
+// ol, ul
+#define GROUP_OL_UL (1 << 21)
+
+// h1, h2, h3, h4, h5, h6
+#define GROUP_HEADING (1 << 22)
+
+// figcaption
+#define GROUP_FIGCAPTION (1 << 23)
+
+// picture members (img, source)
+#define GROUP_PICTURE_CONTENT (1 << 24)
+
+#define GROUP_INLINE_ELEMENT \
+ (GROUP_FONTSTYLE | GROUP_PHRASE | GROUP_SPECIAL | GROUP_FORMCONTROL | \
+ GROUP_LEAF)
+
+#define GROUP_FLOW_ELEMENT (GROUP_INLINE_ELEMENT | GROUP_BLOCK)
+
+struct ElementInfo final {
+#ifdef DEBUG
+ nsHTMLTag mTag;
+#endif
+ // See `GROUP_NONE`'s comment.
+ uint32_t mGroup;
+ // See `GROUP_NONE`'s comment.
+ uint32_t mCanContainGroups;
+ bool mIsContainer;
+ bool mCanContainSelf;
+};
+
+#ifdef DEBUG
+# define ELEM(_tag, _isContainer, _canContainSelf, _group, _canContainGroups) \
+ { \
+ eHTMLTag_##_tag, _group, _canContainGroups, _isContainer, \
+ _canContainSelf \
+ }
+#else
+# define ELEM(_tag, _isContainer, _canContainSelf, _group, _canContainGroups) \
+ { _group, _canContainGroups, _isContainer, _canContainSelf }
+#endif
+
+static const ElementInfo kElements[eHTMLTag_userdefined] = {
+ ELEM(a, true, false, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(abbr, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(acronym, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(address, true, true, GROUP_BLOCK, GROUP_INLINE_ELEMENT | GROUP_P),
+ // While applet is no longer a valid tag, removing it here breaks the editor
+ // (compiles, but causes many tests to fail in odd ways). This list is
+ // tracked against the main HTML Tag list, so any changes will require more
+ // than just removing entries.
+ ELEM(applet, true, true, GROUP_SPECIAL | GROUP_BLOCK,
+ GROUP_FLOW_ELEMENT | GROUP_OBJECT_CONTENT),
+ ELEM(area, false, false, GROUP_MAP_CONTENT, GROUP_NONE),
+ ELEM(article, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(aside, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(audio, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(b, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(base, false, false, GROUP_HEAD_CONTENT, GROUP_NONE),
+ ELEM(basefont, false, false, GROUP_SPECIAL, GROUP_NONE),
+ ELEM(bdi, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(bdo, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(bgsound, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(big, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(blockquote, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(body, true, true, GROUP_TOPLEVEL, GROUP_FLOW_ELEMENT),
+ ELEM(br, false, false, GROUP_SPECIAL, GROUP_NONE),
+ ELEM(button, true, true, GROUP_FORMCONTROL | GROUP_BLOCK,
+ GROUP_FLOW_ELEMENT),
+ ELEM(canvas, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(caption, true, true, GROUP_NONE, GROUP_INLINE_ELEMENT),
+ ELEM(center, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(cite, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(code, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(col, false, false, GROUP_TABLE_CONTENT | GROUP_COLGROUP_CONTENT,
+ GROUP_NONE),
+ ELEM(colgroup, true, false, GROUP_NONE, GROUP_COLGROUP_CONTENT),
+ ELEM(data, true, false, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(datalist, true, false, GROUP_PHRASE,
+ GROUP_OPTIONS | GROUP_INLINE_ELEMENT),
+ ELEM(dd, true, false, GROUP_DL_CONTENT, GROUP_FLOW_ELEMENT),
+ ELEM(del, true, true, GROUP_PHRASE | GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(details, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(dfn, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(dialog, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(dir, true, false, GROUP_BLOCK, GROUP_LI),
+ ELEM(div, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(dl, true, false, GROUP_BLOCK, GROUP_DL_CONTENT),
+ ELEM(dt, true, true, GROUP_DL_CONTENT, GROUP_INLINE_ELEMENT),
+ ELEM(em, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(embed, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(fieldset, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(figcaption, true, false, GROUP_FIGCAPTION, GROUP_FLOW_ELEMENT),
+ ELEM(figure, true, true, GROUP_BLOCK,
+ GROUP_FLOW_ELEMENT | GROUP_FIGCAPTION),
+ ELEM(font, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(footer, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(form, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(frame, false, false, GROUP_FRAME, GROUP_NONE),
+ ELEM(frameset, true, true, GROUP_FRAME, GROUP_FRAME),
+ ELEM(h1, true, false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
+ ELEM(h2, true, false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
+ ELEM(h3, true, false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
+ ELEM(h4, true, false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
+ ELEM(h5, true, false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
+ ELEM(h6, true, false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
+ ELEM(head, true, false, GROUP_TOPLEVEL, GROUP_HEAD_CONTENT),
+ ELEM(header, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(hgroup, true, false, GROUP_BLOCK, GROUP_HEADING),
+ ELEM(hr, false, false, GROUP_BLOCK, GROUP_NONE),
+ ELEM(html, true, false, GROUP_TOPLEVEL, GROUP_TOPLEVEL),
+ ELEM(i, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(iframe, true, true, GROUP_SPECIAL | GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(image, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(img, false, false, GROUP_SPECIAL | GROUP_PICTURE_CONTENT, GROUP_NONE),
+ ELEM(input, false, false, GROUP_FORMCONTROL, GROUP_NONE),
+ ELEM(ins, true, true, GROUP_PHRASE | GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(kbd, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(keygen, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(label, true, false, GROUP_FORMCONTROL, GROUP_INLINE_ELEMENT),
+ ELEM(legend, true, true, GROUP_NONE, GROUP_INLINE_ELEMENT),
+ ELEM(li, true, false, GROUP_LI, GROUP_FLOW_ELEMENT),
+ ELEM(link, false, false, GROUP_HEAD_CONTENT, GROUP_NONE),
+ ELEM(listing, true, true, GROUP_BLOCK, GROUP_INLINE_ELEMENT),
+ ELEM(main, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(map, true, true, GROUP_SPECIAL, GROUP_BLOCK | GROUP_MAP_CONTENT),
+ ELEM(mark, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(marquee, true, false, GROUP_NONE, GROUP_NONE),
+ ELEM(menu, true, true, GROUP_BLOCK, GROUP_LI | GROUP_FLOW_ELEMENT),
+ ELEM(meta, false, false, GROUP_HEAD_CONTENT, GROUP_NONE),
+ ELEM(meter, true, false, GROUP_SPECIAL, GROUP_FLOW_ELEMENT),
+ ELEM(multicol, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(nav, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(nobr, true, false, GROUP_NONE, GROUP_NONE),
+ ELEM(noembed, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(noframes, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(noscript, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(object, true, true, GROUP_SPECIAL | GROUP_BLOCK,
+ GROUP_FLOW_ELEMENT | GROUP_OBJECT_CONTENT),
+ // XXX Can contain self and ul because editor does sublists illegally.
+ ELEM(ol, true, true, GROUP_BLOCK | GROUP_OL_UL, GROUP_LI | GROUP_OL_UL),
+ ELEM(optgroup, true, false, GROUP_SELECT_CONTENT, GROUP_OPTIONS),
+ ELEM(option, true, false, GROUP_SELECT_CONTENT | GROUP_OPTIONS, GROUP_LEAF),
+ ELEM(output, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(p, true, false, GROUP_BLOCK | GROUP_P, GROUP_INLINE_ELEMENT),
+ ELEM(param, false, false, GROUP_OBJECT_CONTENT, GROUP_NONE),
+ ELEM(picture, true, false, GROUP_SPECIAL, GROUP_PICTURE_CONTENT),
+ ELEM(plaintext, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(pre, true, true, GROUP_BLOCK, GROUP_INLINE_ELEMENT),
+ ELEM(progress, true, false, GROUP_SPECIAL, GROUP_FLOW_ELEMENT),
+ ELEM(q, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(rb, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(rp, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(rt, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(rtc, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(ruby, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(s, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(samp, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(script, true, false, GROUP_HEAD_CONTENT | GROUP_SPECIAL, GROUP_LEAF),
+ ELEM(search, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(section, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(select, true, false, GROUP_FORMCONTROL, GROUP_SELECT_CONTENT),
+ ELEM(small, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(slot, true, false, GROUP_NONE, GROUP_FLOW_ELEMENT),
+ ELEM(source, false, false, GROUP_PICTURE_CONTENT, GROUP_NONE),
+ ELEM(span, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(strike, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(strong, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(style, true, false, GROUP_HEAD_CONTENT, GROUP_LEAF),
+ ELEM(sub, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(summary, true, true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
+ ELEM(sup, true, true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
+ ELEM(table, true, false, GROUP_BLOCK, GROUP_TABLE_CONTENT),
+ ELEM(tbody, true, false, GROUP_TABLE_CONTENT, GROUP_TBODY_CONTENT),
+ ELEM(td, true, false, GROUP_TR_CONTENT, GROUP_FLOW_ELEMENT),
+ ELEM(textarea, true, false, GROUP_FORMCONTROL, GROUP_LEAF),
+ ELEM(tfoot, true, false, GROUP_NONE, GROUP_TBODY_CONTENT),
+ ELEM(th, true, false, GROUP_TR_CONTENT, GROUP_FLOW_ELEMENT),
+ ELEM(thead, true, false, GROUP_NONE, GROUP_TBODY_CONTENT),
+ ELEM(template, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(time, true, false, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(title, true, false, GROUP_HEAD_CONTENT, GROUP_LEAF),
+ ELEM(tr, true, false, GROUP_TBODY_CONTENT, GROUP_TR_CONTENT),
+ ELEM(track, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(tt, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ ELEM(u, true, true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
+ // XXX Can contain self and ol because editor does sublists illegally.
+ ELEM(ul, true, true, GROUP_BLOCK | GROUP_OL_UL, GROUP_LI | GROUP_OL_UL),
+ ELEM(var, true, true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
+ ELEM(video, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(wbr, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(xmp, true, false, GROUP_BLOCK, GROUP_NONE),
+
+ // These aren't elements.
+ ELEM(text, false, false, GROUP_LEAF, GROUP_NONE),
+ ELEM(whitespace, false, false, GROUP_LEAF, GROUP_NONE),
+ ELEM(newline, false, false, GROUP_LEAF, GROUP_NONE),
+ ELEM(comment, false, false, GROUP_LEAF, GROUP_NONE),
+ ELEM(entity, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(doctypeDecl, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(markupDecl, false, false, GROUP_NONE, GROUP_NONE),
+ ELEM(instruction, false, false, GROUP_NONE, GROUP_NONE),
+
+ ELEM(userdefined, true, false, GROUP_NONE, GROUP_FLOW_ELEMENT)};
+
+bool HTMLEditUtils::CanNodeContain(nsHTMLTag aParentTagId,
+ nsHTMLTag aChildTagId) {
+ NS_ASSERTION(
+ aParentTagId > eHTMLTag_unknown && aParentTagId <= eHTMLTag_userdefined,
+ "aParentTagId out of range!");
+ NS_ASSERTION(
+ aChildTagId > eHTMLTag_unknown && aChildTagId <= eHTMLTag_userdefined,
+ "aChildTagId out of range!");
+
+#ifdef DEBUG
+ static bool checked = false;
+ if (!checked) {
+ checked = true;
+ int32_t i;
+ for (i = 1; i <= eHTMLTag_userdefined; ++i) {
+ NS_ASSERTION(kElements[i - 1].mTag == i,
+ "You need to update kElements (missing tags).");
+ }
+ }
+#endif
+
+ // Special-case button.
+ if (aParentTagId == eHTMLTag_button) {
+ static const nsHTMLTag kButtonExcludeKids[] = {
+ eHTMLTag_a, eHTMLTag_fieldset, eHTMLTag_form, eHTMLTag_iframe,
+ eHTMLTag_input, eHTMLTag_select, eHTMLTag_textarea};
+
+ uint32_t j;
+ for (j = 0; j < ArrayLength(kButtonExcludeKids); ++j) {
+ if (kButtonExcludeKids[j] == aChildTagId) {
+ return false;
+ }
+ }
+ }
+
+ // Deprecated elements.
+ if (aChildTagId == eHTMLTag_bgsound) {
+ return false;
+ }
+
+ // Bug #67007, dont strip userdefined tags.
+ if (aChildTagId == eHTMLTag_userdefined) {
+ return true;
+ }
+
+ const ElementInfo& parent = kElements[aParentTagId - 1];
+ if (aParentTagId == aChildTagId) {
+ return parent.mCanContainSelf;
+ }
+
+ const ElementInfo& child = kElements[aChildTagId - 1];
+ return !!(parent.mCanContainGroups & child.mGroup);
+}
+
+bool HTMLEditUtils::ContentIsInert(const nsIContent& aContent) {
+ for (nsIContent* content :
+ aContent.InclusiveFlatTreeAncestorsOfType<nsIContent>()) {
+ if (nsIFrame* frame = content->GetPrimaryFrame()) {
+ return frame->StyleUI()->IsInert();
+ }
+ // If it doesn't have primary frame, we need to check its ancestors.
+ // This may occur if it's an invisible text node or element nodes whose
+ // display is an invisible value.
+ if (!content->IsElement()) {
+ continue;
+ }
+ if (content->AsElement()->State().HasState(dom::ElementState::INERT)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool HTMLEditUtils::IsContainerNode(nsHTMLTag aTagId) {
+ NS_ASSERTION(aTagId > eHTMLTag_unknown && aTagId <= eHTMLTag_userdefined,
+ "aTagId out of range!");
+
+ return kElements[aTagId - 1].mIsContainer;
+}
+
+bool HTMLEditUtils::IsNonListSingleLineContainer(const nsINode& aNode) {
+ return aNode.IsAnyOfHTMLElements(
+ nsGkAtoms::address, nsGkAtoms::div, nsGkAtoms::h1, nsGkAtoms::h2,
+ nsGkAtoms::h3, nsGkAtoms::h4, nsGkAtoms::h5, nsGkAtoms::h6,
+ nsGkAtoms::listing, nsGkAtoms::p, nsGkAtoms::pre, nsGkAtoms::xmp);
+}
+
+bool HTMLEditUtils::IsSingleLineContainer(const nsINode& aNode) {
+ return IsNonListSingleLineContainer(aNode) ||
+ aNode.IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::dt, nsGkAtoms::dd);
+}
+
+// static
+template <typename PT, typename CT>
+nsIContent* HTMLEditUtils::GetPreviousContent(
+ const EditorDOMPointBase<PT, CT>& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter /* = nullptr */) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ NS_WARNING_ASSERTION(
+ !aPoint.IsInDataNode() || aPoint.IsInTextNode(),
+ "GetPreviousContent() doesn't assume that the start point is a "
+ "data node except text node");
+
+ // If we are at the beginning of the node, or it is a text node, then just
+ // look before it.
+ if (aPoint.IsStartOfContainer() || aPoint.IsInTextNode()) {
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ aPoint.IsInContentNode() &&
+ HTMLEditUtils::IsBlockElement(
+ *aPoint.template ContainerAs<nsIContent>(), aBlockInlineCheck)) {
+ // If we aren't allowed to cross blocks, don't look before this block.
+ return nullptr;
+ }
+ return HTMLEditUtils::GetPreviousContent(
+ *aPoint.GetContainer(), aOptions, aBlockInlineCheck, aAncestorLimiter);
+ }
+
+ // else look before the child at 'aOffset'
+ if (aPoint.GetChild()) {
+ return HTMLEditUtils::GetPreviousContent(
+ *aPoint.GetChild(), aOptions, aBlockInlineCheck, aAncestorLimiter);
+ }
+
+ // unless there isn't one, in which case we are at the end of the node
+ // and want the deep-right child.
+ nsIContent* lastLeafContent = HTMLEditUtils::GetLastLeafContent(
+ *aPoint.GetContainer(),
+ {aOptions.contains(WalkTreeOption::StopAtBlockBoundary)
+ ? LeafNodeType::LeafNodeOrChildBlock
+ : LeafNodeType::OnlyLeafNode},
+ aBlockInlineCheck);
+ if (!lastLeafContent) {
+ return nullptr;
+ }
+
+ if (!HTMLEditUtils::IsContentIgnored(*lastLeafContent, aOptions)) {
+ return lastLeafContent;
+ }
+
+ // restart the search from the non-editable node we just found
+ return HTMLEditUtils::GetPreviousContent(*lastLeafContent, aOptions,
+ aBlockInlineCheck, aAncestorLimiter);
+}
+
+// static
+template <typename PT, typename CT>
+nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorDOMPointBase<PT, CT>& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter /* = nullptr */) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ NS_WARNING_ASSERTION(
+ !aPoint.IsInDataNode() || aPoint.IsInTextNode(),
+ "GetNextContent() doesn't assume that the start point is a "
+ "data node except text node");
+
+ auto point = aPoint.template To<EditorRawDOMPoint>();
+
+ // if the container is a text node, use its location instead
+ if (point.IsInTextNode()) {
+ point.SetAfter(point.GetContainer());
+ if (NS_WARN_IF(!point.IsSet())) {
+ return nullptr;
+ }
+ }
+
+ if (point.GetChild()) {
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*point.GetChild(), aBlockInlineCheck)) {
+ return point.GetChild();
+ }
+
+ nsIContent* firstLeafContent = HTMLEditUtils::GetFirstLeafContent(
+ *point.GetChild(),
+ {aOptions.contains(WalkTreeOption::StopAtBlockBoundary)
+ ? LeafNodeType::LeafNodeOrChildBlock
+ : LeafNodeType::OnlyLeafNode},
+ aBlockInlineCheck);
+ if (!firstLeafContent) {
+ return point.GetChild();
+ }
+
+ // XXX Why do we need to do this check? The leaf node must be a descendant
+ // of `point.GetChild()`.
+ if (aAncestorLimiter &&
+ (firstLeafContent == aAncestorLimiter ||
+ !firstLeafContent->IsInclusiveDescendantOf(aAncestorLimiter))) {
+ return nullptr;
+ }
+
+ if (!HTMLEditUtils::IsContentIgnored(*firstLeafContent, aOptions)) {
+ return firstLeafContent;
+ }
+
+ // restart the search from the non-editable node we just found
+ return HTMLEditUtils::GetNextContent(*firstLeafContent, aOptions,
+ aBlockInlineCheck, aAncestorLimiter);
+ }
+
+ // unless there isn't one, in which case we are at the end of the node
+ // and want the next one.
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ point.IsInContentNode() &&
+ HTMLEditUtils::IsBlockElement(*point.template ContainerAs<nsIContent>(),
+ aBlockInlineCheck)) {
+ // don't cross out of parent block
+ return nullptr;
+ }
+
+ return HTMLEditUtils::GetNextContent(*point.GetContainer(), aOptions,
+ aBlockInlineCheck, aAncestorLimiter);
+}
+
+// static
+nsIContent* HTMLEditUtils::GetAdjacentLeafContent(
+ const nsINode& aNode, WalkTreeDirection aWalkTreeDirection,
+ const WalkTreeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter /* = nullptr */) {
+ // called only by GetPriorNode so we don't need to check params.
+ MOZ_ASSERT(&aNode != aAncestorLimiter);
+ MOZ_ASSERT_IF(aAncestorLimiter,
+ aAncestorLimiter->IsInclusiveDescendantOf(aAncestorLimiter));
+
+ const nsINode* node = &aNode;
+ for (;;) {
+ // if aNode has a sibling in the right direction, return
+ // that sibling's closest child (or itself if it has no children)
+ nsIContent* sibling = aWalkTreeDirection == WalkTreeDirection::Forward
+ ? node->GetNextSibling()
+ : node->GetPreviousSibling();
+ if (sibling) {
+ // XXX If `sibling` belongs to siblings of inclusive ancestors of aNode,
+ // perhaps, we need to use
+ // IgnoreInsideBlockBoundary(aBlockInlineCheck) here.
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) {
+ // don't look inside previous sibling, since it is a block
+ return sibling;
+ }
+ const LeafNodeTypes leafNodeTypes = {
+ aOptions.contains(WalkTreeOption::StopAtBlockBoundary)
+ ? LeafNodeType::LeafNodeOrChildBlock
+ : LeafNodeType::OnlyLeafNode};
+ nsIContent* leafContent =
+ aWalkTreeDirection == WalkTreeDirection::Forward
+ ? HTMLEditUtils::GetFirstLeafContent(*sibling, leafNodeTypes,
+ aBlockInlineCheck)
+ : HTMLEditUtils::GetLastLeafContent(*sibling, leafNodeTypes,
+ aBlockInlineCheck);
+ return leafContent ? leafContent : sibling;
+ }
+
+ nsIContent* parent = node->GetParent();
+ if (!parent) {
+ return nullptr;
+ }
+
+ if (parent == aAncestorLimiter ||
+ (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*parent, aBlockInlineCheck))) {
+ return nullptr;
+ }
+
+ node = parent;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("What part of for(;;) do you not understand?");
+ return nullptr;
+}
+
+// static
+nsIContent* HTMLEditUtils::GetAdjacentContent(
+ const nsINode& aNode, WalkTreeDirection aWalkTreeDirection,
+ const WalkTreeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter /* = nullptr */) {
+ if (&aNode == aAncestorLimiter) {
+ // Don't allow traversal above the root node! This helps
+ // prevent us from accidentally editing browser content
+ // when the editor is in a text widget.
+ return nullptr;
+ }
+
+ nsIContent* leafContent = HTMLEditUtils::GetAdjacentLeafContent(
+ aNode, aWalkTreeDirection, aOptions, aBlockInlineCheck, aAncestorLimiter);
+ if (!leafContent) {
+ return nullptr;
+ }
+
+ if (!HTMLEditUtils::IsContentIgnored(*leafContent, aOptions)) {
+ return leafContent;
+ }
+
+ return HTMLEditUtils::GetAdjacentContent(*leafContent, aWalkTreeDirection,
+ aOptions, aBlockInlineCheck,
+ aAncestorLimiter);
+}
+
+// static
+template <typename EditorDOMPointType>
+EditorDOMPointType HTMLEditUtils::GetPreviousEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary) {
+ MOZ_ASSERT(HTMLEditUtils::IsSimplyEditableNode(aContent));
+ NS_ASSERTION(!HTMLEditUtils::IsAnyTableElement(&aContent) ||
+ HTMLEditUtils::IsTableCellOrCaption(aContent),
+ "HTMLEditUtils::GetPreviousEditablePoint() may return a point "
+ "between table structure elements");
+
+ if (&aContent == aAncestorLimiter) {
+ return EditorDOMPointType();
+ }
+
+ // First, look for previous content.
+ nsIContent* previousContent = aContent.GetPreviousSibling();
+ if (!previousContent) {
+ if (!aContent.GetParentElement()) {
+ return EditorDOMPointType();
+ }
+ nsIContent* inclusiveAncestor = &aContent;
+ for (Element* parentElement : aContent.AncestorsOfType<Element>()) {
+ if (parentElement == aAncestorLimiter ||
+ !HTMLEditUtils::IsSimplyEditableNode(*parentElement) ||
+ !HTMLEditUtils::CanCrossContentBoundary(*parentElement,
+ aHowToTreatTableBoundary)) {
+ // If cannot cross the parent element boundary, return the point of
+ // last inclusive ancestor point.
+ return EditorDOMPointType(inclusiveAncestor);
+ }
+
+ // Start of the parent element is a next editable point if it's an
+ // element which is not a table structure element.
+ if (!HTMLEditUtils::IsAnyTableElement(parentElement) ||
+ HTMLEditUtils::IsTableCellOrCaption(*parentElement)) {
+ inclusiveAncestor = parentElement;
+ }
+
+ previousContent = parentElement->GetPreviousSibling();
+ if (!previousContent) {
+ continue; // Keep looking for previous sibling of an ancestor.
+ }
+
+ // XXX Should we ignore data node like CDATA, Comment, etc?
+
+ // If previous content is not editable, let's return the point after it.
+ if (!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
+ return EditorDOMPointType::After(*previousContent);
+ }
+
+ // If cannot cross previous content boundary, return start of last
+ // inclusive ancestor.
+ if (!HTMLEditUtils::CanCrossContentBoundary(*previousContent,
+ aHowToTreatTableBoundary)) {
+ return inclusiveAncestor == &aContent
+ ? EditorDOMPointType(inclusiveAncestor)
+ : EditorDOMPointType(inclusiveAncestor, 0);
+ }
+ break;
+ }
+ if (!previousContent) {
+ return EditorDOMPointType(inclusiveAncestor);
+ }
+ } else if (!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
+ return EditorDOMPointType::After(*previousContent);
+ } else if (!HTMLEditUtils::CanCrossContentBoundary(
+ *previousContent, aHowToTreatTableBoundary)) {
+ return EditorDOMPointType(&aContent);
+ }
+
+ // Next, look for end of the previous content.
+ nsIContent* leafContent = previousContent;
+ if (previousContent->GetChildCount() &&
+ HTMLEditUtils::IsContainerNode(*previousContent)) {
+ for (nsIContent* maybeLeafContent = previousContent->GetLastChild();
+ maybeLeafContent;
+ maybeLeafContent = maybeLeafContent->GetLastChild()) {
+ // If it's not an editable content or cannot cross the boundary,
+ // return the point after the content. Note that in this case,
+ // the content must not be any table elements except `<table>`
+ // because we've climbed down the tree.
+ if (!HTMLEditUtils::IsSimplyEditableNode(*maybeLeafContent) ||
+ !HTMLEditUtils::CanCrossContentBoundary(*maybeLeafContent,
+ aHowToTreatTableBoundary)) {
+ return EditorDOMPointType::After(*maybeLeafContent);
+ }
+ leafContent = maybeLeafContent;
+ if (!HTMLEditUtils::IsContainerNode(*leafContent)) {
+ break;
+ }
+ }
+ }
+
+ if (leafContent->IsText()) {
+ Text* textNode = leafContent->AsText();
+ if (aInvisibleWhiteSpaces == InvisibleWhiteSpaces::Preserve) {
+ return EditorDOMPointType::AtEndOf(*textNode);
+ }
+ // There may be invisible trailing white-spaces which should be
+ // ignored. Let's scan its start.
+ return WSRunScanner::GetAfterLastVisiblePoint<EditorDOMPointType>(
+ *textNode, aAncestorLimiter);
+ }
+
+ // If it's a container element, return end of it. Otherwise, return
+ // the point after the non-container element.
+ return HTMLEditUtils::IsContainerNode(*leafContent)
+ ? EditorDOMPointType::AtEndOf(*leafContent)
+ : EditorDOMPointType::After(*leafContent);
+}
+
+// static
+template <typename EditorDOMPointType>
+EditorDOMPointType HTMLEditUtils::GetNextEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary) {
+ MOZ_ASSERT(HTMLEditUtils::IsSimplyEditableNode(aContent));
+ NS_ASSERTION(!HTMLEditUtils::IsAnyTableElement(&aContent) ||
+ HTMLEditUtils::IsTableCellOrCaption(aContent),
+ "HTMLEditUtils::GetPreviousEditablePoint() may return a point "
+ "between table structure elements");
+
+ if (&aContent == aAncestorLimiter) {
+ return EditorDOMPointType();
+ }
+
+ // First, look for next content.
+ nsIContent* nextContent = aContent.GetNextSibling();
+ if (!nextContent) {
+ if (!aContent.GetParentElement()) {
+ return EditorDOMPointType();
+ }
+ nsIContent* inclusiveAncestor = &aContent;
+ for (Element* parentElement : aContent.AncestorsOfType<Element>()) {
+ if (parentElement == aAncestorLimiter ||
+ !HTMLEditUtils::IsSimplyEditableNode(*parentElement) ||
+ !HTMLEditUtils::CanCrossContentBoundary(*parentElement,
+ aHowToTreatTableBoundary)) {
+ // If cannot cross the parent element boundary, return the point of
+ // last inclusive ancestor point.
+ return EditorDOMPointType(inclusiveAncestor);
+ }
+
+ // End of the parent element is a next editable point if it's an
+ // element which is not a table structure element.
+ if (!HTMLEditUtils::IsAnyTableElement(parentElement) ||
+ HTMLEditUtils::IsTableCellOrCaption(*parentElement)) {
+ inclusiveAncestor = parentElement;
+ }
+
+ nextContent = parentElement->GetNextSibling();
+ if (!nextContent) {
+ continue; // Keep looking for next sibling of an ancestor.
+ }
+
+ // XXX Should we ignore data node like CDATA, Comment, etc?
+
+ // If next content is not editable, let's return the point after
+ // the last inclusive ancestor.
+ if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) {
+ return EditorDOMPointType::After(*parentElement);
+ }
+
+ // If cannot cross next content boundary, return after the last
+ // inclusive ancestor.
+ if (!HTMLEditUtils::CanCrossContentBoundary(*nextContent,
+ aHowToTreatTableBoundary)) {
+ return EditorDOMPointType::After(*inclusiveAncestor);
+ }
+ break;
+ }
+ if (!nextContent) {
+ return EditorDOMPointType::After(*inclusiveAncestor);
+ }
+ } else if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) {
+ return EditorDOMPointType::After(aContent);
+ } else if (!HTMLEditUtils::CanCrossContentBoundary(
+ *nextContent, aHowToTreatTableBoundary)) {
+ return EditorDOMPointType::After(aContent);
+ }
+
+ // Next, look for start of the next content.
+ nsIContent* leafContent = nextContent;
+ if (nextContent->GetChildCount() &&
+ HTMLEditUtils::IsContainerNode(*nextContent)) {
+ for (nsIContent* maybeLeafContent = nextContent->GetFirstChild();
+ maybeLeafContent;
+ maybeLeafContent = maybeLeafContent->GetFirstChild()) {
+ // If it's not an editable content or cannot cross the boundary,
+ // return the point at the content (i.e., start of its parent). Note
+ // that in this case, the content must not be any table elements except
+ // `<table>` because we've climbed down the tree.
+ if (!HTMLEditUtils::IsSimplyEditableNode(*maybeLeafContent) ||
+ !HTMLEditUtils::CanCrossContentBoundary(*maybeLeafContent,
+ aHowToTreatTableBoundary)) {
+ return EditorDOMPointType(maybeLeafContent);
+ }
+ leafContent = maybeLeafContent;
+ if (!HTMLEditUtils::IsContainerNode(*leafContent)) {
+ break;
+ }
+ }
+ }
+
+ if (leafContent->IsText()) {
+ Text* textNode = leafContent->AsText();
+ if (aInvisibleWhiteSpaces == InvisibleWhiteSpaces::Preserve) {
+ return EditorDOMPointType(textNode, 0);
+ }
+ // There may be invisible leading white-spaces which should be
+ // ignored. Let's scan its start.
+ return WSRunScanner::GetFirstVisiblePoint<EditorDOMPointType>(
+ *textNode, aAncestorLimiter);
+ }
+
+ // If it's a container element, return start of it. Otherwise, return
+ // the point at the non-container element (i.e., start of its parent).
+ return HTMLEditUtils::IsContainerNode(*leafContent)
+ ? EditorDOMPointType(leafContent, 0)
+ : EditorDOMPointType(leafContent);
+}
+
+// static
+Element* HTMLEditUtils::GetAncestorElement(
+ const nsIContent& aContent, const AncestorTypes& aAncestorTypes,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter /* = nullptr */) {
+ MOZ_ASSERT(
+ aAncestorTypes.contains(AncestorType::ClosestBlockElement) ||
+ aAncestorTypes.contains(AncestorType::MostDistantInlineElementInBlock) ||
+ aAncestorTypes.contains(AncestorType::ButtonElement));
+
+ if (&aContent == aAncestorLimiter) {
+ return nullptr;
+ }
+
+ const Element* theBodyElement = aContent.OwnerDoc()->GetBody();
+ const Element* theDocumentElement = aContent.OwnerDoc()->GetDocumentElement();
+ Element* lastAncestorElement = nullptr;
+ const bool editableElementOnly =
+ aAncestorTypes.contains(AncestorType::EditableElement);
+ const bool lookingForClosestBlockElement =
+ aAncestorTypes.contains(AncestorType::ClosestBlockElement);
+ const bool lookingForMostDistantInlineElementInBlock =
+ aAncestorTypes.contains(AncestorType::MostDistantInlineElementInBlock);
+ const bool ignoreHRElement =
+ aAncestorTypes.contains(AncestorType::IgnoreHRElement);
+ const bool lookingForButtonElement =
+ aAncestorTypes.contains(AncestorType::ButtonElement);
+ auto IsSearchingElementType = [&](const nsIContent& aContent) -> bool {
+ if (!aContent.IsElement() ||
+ (ignoreHRElement && aContent.IsHTMLElement(nsGkAtoms::hr))) {
+ return false;
+ }
+ if (editableElementOnly &&
+ !EditorUtils::IsEditableContent(aContent, EditorType::HTML)) {
+ return false;
+ }
+ return (lookingForClosestBlockElement &&
+ HTMLEditUtils::IsBlockElement(aContent, aBlockInlineCheck)) ||
+ (lookingForMostDistantInlineElementInBlock &&
+ HTMLEditUtils::IsInlineContent(aContent, aBlockInlineCheck)) ||
+ (lookingForButtonElement &&
+ aContent.IsHTMLElement(nsGkAtoms::button));
+ };
+ for (Element* element : aContent.AncestorsOfType<Element>()) {
+ if (editableElementOnly &&
+ !EditorUtils::IsEditableContent(*element, EditorType::HTML)) {
+ return lastAncestorElement && IsSearchingElementType(*lastAncestorElement)
+ ? lastAncestorElement // editing host (can be inline element)
+ : nullptr;
+ }
+ if (ignoreHRElement && element->IsHTMLElement(nsGkAtoms::hr)) {
+ if (element == aAncestorLimiter) {
+ break;
+ }
+ continue;
+ }
+ if (lookingForButtonElement && element->IsHTMLElement(nsGkAtoms::button)) {
+ return element; // closest button element
+ }
+ if (HTMLEditUtils::IsBlockElement(*element, aBlockInlineCheck)) {
+ if (lookingForClosestBlockElement) {
+ return element; // closest block element
+ }
+ MOZ_ASSERT_IF(lastAncestorElement,
+ HTMLEditUtils::IsInlineContent(*lastAncestorElement,
+ aBlockInlineCheck));
+ return lastAncestorElement; // the last inline element which we found
+ }
+ if (element == aAncestorLimiter || element == theBodyElement ||
+ element == theDocumentElement) {
+ break;
+ }
+ lastAncestorElement = element;
+ }
+ return lastAncestorElement && IsSearchingElementType(*lastAncestorElement)
+ ? lastAncestorElement
+ : nullptr;
+}
+
+// static
+Element* HTMLEditUtils::GetInclusiveAncestorElement(
+ const nsIContent& aContent, const AncestorTypes& aAncestorTypes,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter /* = nullptr */) {
+ MOZ_ASSERT(
+ aAncestorTypes.contains(AncestorType::ClosestBlockElement) ||
+ aAncestorTypes.contains(AncestorType::MostDistantInlineElementInBlock) ||
+ aAncestorTypes.contains(AncestorType::ButtonElement));
+
+ const Element* theBodyElement = aContent.OwnerDoc()->GetBody();
+ const Element* theDocumentElement = aContent.OwnerDoc()->GetDocumentElement();
+ const bool editableElementOnly =
+ aAncestorTypes.contains(AncestorType::EditableElement);
+ const bool lookingForClosestBlockElement =
+ aAncestorTypes.contains(AncestorType::ClosestBlockElement);
+ const bool lookingForMostDistantInlineElementInBlock =
+ aAncestorTypes.contains(AncestorType::MostDistantInlineElementInBlock);
+ const bool lookingForButtonElement =
+ aAncestorTypes.contains(AncestorType::ButtonElement);
+ const bool ignoreHRElement =
+ aAncestorTypes.contains(AncestorType::IgnoreHRElement);
+ auto IsSearchingElementType = [&](const nsIContent& aContent) -> bool {
+ if (!aContent.IsElement() ||
+ (ignoreHRElement && aContent.IsHTMLElement(nsGkAtoms::hr))) {
+ return false;
+ }
+ if (editableElementOnly &&
+ !EditorUtils::IsEditableContent(aContent, EditorType::HTML)) {
+ return false;
+ }
+ return (lookingForClosestBlockElement &&
+ HTMLEditUtils::IsBlockElement(aContent, aBlockInlineCheck)) ||
+ (lookingForMostDistantInlineElementInBlock &&
+ HTMLEditUtils::IsInlineContent(aContent, aBlockInlineCheck)) ||
+ (lookingForButtonElement &&
+ aContent.IsHTMLElement(nsGkAtoms::button));
+ };
+
+ // If aContent is the body element or the document element, we shouldn't climb
+ // up to its parent.
+ if (editableElementOnly &&
+ (&aContent == theBodyElement || &aContent == theDocumentElement)) {
+ return IsSearchingElementType(aContent)
+ ? const_cast<Element*>(aContent.AsElement())
+ : nullptr;
+ }
+
+ if (lookingForButtonElement && aContent.IsHTMLElement(nsGkAtoms::button)) {
+ return const_cast<Element*>(aContent.AsElement());
+ }
+
+ // If aContent is a block element, we don't need to climb up the tree.
+ // Consider the result right now.
+ if ((lookingForClosestBlockElement ||
+ lookingForMostDistantInlineElementInBlock) &&
+ HTMLEditUtils::IsBlockElement(aContent, aBlockInlineCheck) &&
+ !(ignoreHRElement && aContent.IsHTMLElement(nsGkAtoms::hr))) {
+ return IsSearchingElementType(aContent)
+ ? const_cast<Element*>(aContent.AsElement())
+ : nullptr;
+ }
+
+ // If aContent is the last element to search range because of the parent
+ // element type, consider the result before calling GetAncestorElement()
+ // because it won't return aContent.
+ if (!aContent.GetParent() ||
+ (editableElementOnly && !EditorUtils::IsEditableContent(
+ *aContent.GetParent(), EditorType::HTML)) ||
+ (!lookingForClosestBlockElement &&
+ HTMLEditUtils::IsBlockElement(*aContent.GetParent(),
+ aBlockInlineCheck) &&
+ !(ignoreHRElement &&
+ aContent.GetParent()->IsHTMLElement(nsGkAtoms::hr)))) {
+ return IsSearchingElementType(aContent)
+ ? const_cast<Element*>(aContent.AsElement())
+ : nullptr;
+ }
+
+ if (&aContent == aAncestorLimiter) {
+ return nullptr;
+ }
+
+ return HTMLEditUtils::GetAncestorElement(aContent, aAncestorTypes,
+ aBlockInlineCheck, aAncestorLimiter);
+}
+
+// static
+Element* HTMLEditUtils::GetClosestAncestorAnyListElement(
+ const nsIContent& aContent) {
+ for (Element* element : aContent.AncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsAnyListElement(element)) {
+ return element;
+ }
+ }
+ return nullptr;
+}
+
+// static
+Element* HTMLEditUtils::GetClosestInclusiveAncestorAnyListElement(
+ const nsIContent& aContent) {
+ for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsAnyListElement(element)) {
+ return element;
+ }
+ }
+ return nullptr;
+}
+
+EditAction HTMLEditUtils::GetEditActionForInsert(const nsAtom& aTagName) {
+ // This method may be in a hot path. So, return only necessary
+ // EditAction::eInsert*Element.
+ if (&aTagName == nsGkAtoms::ul) {
+ // For InputEvent.inputType, "insertUnorderedList".
+ return EditAction::eInsertUnorderedListElement;
+ }
+ if (&aTagName == nsGkAtoms::ol) {
+ // For InputEvent.inputType, "insertOrderedList".
+ return EditAction::eInsertOrderedListElement;
+ }
+ if (&aTagName == nsGkAtoms::hr) {
+ // For InputEvent.inputType, "insertHorizontalRule".
+ return EditAction::eInsertHorizontalRuleElement;
+ }
+ return EditAction::eInsertNode;
+}
+
+EditAction HTMLEditUtils::GetEditActionForRemoveList(const nsAtom& aTagName) {
+ // This method may be in a hot path. So, return only necessary
+ // EditAction::eRemove*Element.
+ if (&aTagName == nsGkAtoms::ul) {
+ // For InputEvent.inputType, "insertUnorderedList".
+ return EditAction::eRemoveUnorderedListElement;
+ }
+ if (&aTagName == nsGkAtoms::ol) {
+ // For InputEvent.inputType, "insertOrderedList".
+ return EditAction::eRemoveOrderedListElement;
+ }
+ return EditAction::eRemoveListElement;
+}
+
+EditAction HTMLEditUtils::GetEditActionForInsert(const Element& aElement) {
+ return GetEditActionForInsert(*aElement.NodeInfo()->NameAtom());
+}
+
+EditAction HTMLEditUtils::GetEditActionForFormatText(const nsAtom& aProperty,
+ const nsAtom* aAttribute,
+ bool aToSetStyle) {
+ // This method may be in a hot path. So, return only necessary
+ // EditAction::eSet*Property or EditAction::eRemove*Property.
+ if (&aProperty == nsGkAtoms::b) {
+ return aToSetStyle ? EditAction::eSetFontWeightProperty
+ : EditAction::eRemoveFontWeightProperty;
+ }
+ if (&aProperty == nsGkAtoms::i) {
+ return aToSetStyle ? EditAction::eSetTextStyleProperty
+ : EditAction::eRemoveTextStyleProperty;
+ }
+ if (&aProperty == nsGkAtoms::u) {
+ return aToSetStyle ? EditAction::eSetTextDecorationPropertyUnderline
+ : EditAction::eRemoveTextDecorationPropertyUnderline;
+ }
+ if (&aProperty == nsGkAtoms::strike) {
+ return aToSetStyle ? EditAction::eSetTextDecorationPropertyLineThrough
+ : EditAction::eRemoveTextDecorationPropertyLineThrough;
+ }
+ if (&aProperty == nsGkAtoms::sup) {
+ return aToSetStyle ? EditAction::eSetVerticalAlignPropertySuper
+ : EditAction::eRemoveVerticalAlignPropertySuper;
+ }
+ if (&aProperty == nsGkAtoms::sub) {
+ return aToSetStyle ? EditAction::eSetVerticalAlignPropertySub
+ : EditAction::eRemoveVerticalAlignPropertySub;
+ }
+ if (&aProperty == nsGkAtoms::font) {
+ if (aAttribute == nsGkAtoms::face) {
+ return aToSetStyle ? EditAction::eSetFontFamilyProperty
+ : EditAction::eRemoveFontFamilyProperty;
+ }
+ if (aAttribute == nsGkAtoms::color) {
+ return aToSetStyle ? EditAction::eSetColorProperty
+ : EditAction::eRemoveColorProperty;
+ }
+ if (aAttribute == nsGkAtoms::bgcolor) {
+ return aToSetStyle ? EditAction::eSetBackgroundColorPropertyInline
+ : EditAction::eRemoveBackgroundColorPropertyInline;
+ }
+ }
+ return aToSetStyle ? EditAction::eSetInlineStyleProperty
+ : EditAction::eRemoveInlineStyleProperty;
+}
+
+EditAction HTMLEditUtils::GetEditActionForAlignment(
+ const nsAString& aAlignType) {
+ // This method may be in a hot path. So, return only necessary
+ // EditAction::eAlign*.
+ if (aAlignType.EqualsLiteral("left")) {
+ return EditAction::eAlignLeft;
+ }
+ if (aAlignType.EqualsLiteral("right")) {
+ return EditAction::eAlignRight;
+ }
+ if (aAlignType.EqualsLiteral("center")) {
+ return EditAction::eAlignCenter;
+ }
+ if (aAlignType.EqualsLiteral("justify")) {
+ return EditAction::eJustify;
+ }
+ return EditAction::eSetAlignment;
+}
+
+// static
+template <typename EditorDOMPointType>
+nsIContent* HTMLEditUtils::GetContentToPreserveInlineStyles(
+ const EditorDOMPointType& aPoint, const Element& aEditingHost) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ if (MOZ_UNLIKELY(!aPoint.IsInContentNode())) {
+ return nullptr;
+ }
+ // If it points middle of a text node, use it. Otherwise, scan next visible
+ // thing and use the style of following text node if there is.
+ if (aPoint.IsInTextNode() && !aPoint.IsEndOfContainer()) {
+ return aPoint.template ContainerAs<nsIContent>();
+ }
+ for (auto point = aPoint.template To<EditorRawDOMPoint>(); point.IsSet();) {
+ WSScanResult nextVisibleThing =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost, point,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (nextVisibleThing.InVisibleOrCollapsibleCharacters()) {
+ return nextVisibleThing.TextPtr();
+ }
+ // Ignore empty inline container elements because it's not visible for
+ // users so that using the style will appear suddenly from point of
+ // view of users.
+ if (nextVisibleThing.ReachedSpecialContent() &&
+ nextVisibleThing.IsContentEditable() &&
+ nextVisibleThing.GetContent()->IsElement() &&
+ !nextVisibleThing.GetContent()->HasChildNodes() &&
+ HTMLEditUtils::IsContainerNode(*nextVisibleThing.ElementPtr())) {
+ point.SetAfter(nextVisibleThing.ElementPtr());
+ continue;
+ }
+ // Otherwise, we should use style of the container of the start point.
+ break;
+ }
+ return aPoint.template ContainerAs<nsIContent>();
+}
+
+template <typename EditorDOMPointType, typename EditorDOMPointTypeInput>
+EditorDOMPointType HTMLEditUtils::GetBetterInsertionPointFor(
+ const nsIContent& aContentToInsert,
+ const EditorDOMPointTypeInput& aPointToInsert,
+ const Element& aEditingHost) {
+ if (NS_WARN_IF(!aPointToInsert.IsSet())) {
+ return EditorDOMPointType();
+ }
+
+ auto pointToInsert =
+ aPointToInsert.template GetNonAnonymousSubtreePoint<EditorDOMPointType>();
+ if (MOZ_UNLIKELY(
+ NS_WARN_IF(!pointToInsert.IsSet()) ||
+ NS_WARN_IF(!pointToInsert.GetContainer()->IsInclusiveDescendantOf(
+ &aEditingHost)))) {
+ // Cannot insert aContentToInsert into this DOM tree.
+ return EditorDOMPointType();
+ }
+
+ // If the node to insert is not a block level element, we can insert it
+ // at any point.
+ if (!HTMLEditUtils::IsBlockElement(
+ aContentToInsert, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return pointToInsert;
+ }
+
+ WSRunScanner wsScannerForPointToInsert(
+ const_cast<Element*>(&aEditingHost), pointToInsert,
+ BlockInlineCheck::UseComputedDisplayStyle);
+
+ // If the insertion position is after the last visible item in a line,
+ // i.e., the insertion position is just before a visible line break <br>,
+ // we want to skip to the position just after the line break (see bug 68767).
+ WSScanResult forwardScanFromPointToInsertResult =
+ wsScannerForPointToInsert.ScanNextVisibleNodeOrBlockBoundaryFrom(
+ pointToInsert);
+ // So, if the next visible node isn't a <br> element, we can insert the block
+ // level element to the point.
+ if (!forwardScanFromPointToInsertResult.GetContent() ||
+ !forwardScanFromPointToInsertResult.ReachedBRElement()) {
+ return pointToInsert;
+ }
+
+ // However, we must not skip next <br> element when the caret appears to be
+ // positioned at the beginning of a block, in that case skipping the <br>
+ // would not insert the <br> at the caret position, but after the current
+ // empty line.
+ WSScanResult backwardScanFromPointToInsertResult =
+ wsScannerForPointToInsert.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ pointToInsert);
+ // So, if there is no previous visible node,
+ // or, if both nodes of the insertion point is <br> elements,
+ // or, if the previous visible node is different block,
+ // we need to skip the following <br>. So, otherwise, we can insert the
+ // block at the insertion point.
+ if (!backwardScanFromPointToInsertResult.GetContent() ||
+ backwardScanFromPointToInsertResult.ReachedBRElement() ||
+ backwardScanFromPointToInsertResult.ReachedCurrentBlockBoundary()) {
+ return pointToInsert;
+ }
+
+ return forwardScanFromPointToInsertResult
+ .template PointAfterContent<EditorDOMPointType>();
+}
+
+// static
+template <typename EditorDOMPointType, typename EditorDOMPointTypeInput>
+EditorDOMPointType HTMLEditUtils::GetBetterCaretPositionToInsertText(
+ const EditorDOMPointTypeInput& aPoint, const Element& aEditingHost) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ MOZ_ASSERT(
+ aPoint.GetContainer()->IsInclusiveFlatTreeDescendantOf(&aEditingHost));
+
+ if (aPoint.IsInTextNode()) {
+ return aPoint.template To<EditorDOMPointType>();
+ }
+ if (!aPoint.IsEndOfContainer() && aPoint.GetChild() &&
+ aPoint.GetChild()->IsText()) {
+ return EditorDOMPointType(aPoint.GetChild(), 0u);
+ }
+ if (aPoint.IsEndOfContainer()) {
+ WSRunScanner scanner(&aEditingHost, aPoint,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ WSScanResult previousThing =
+ scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(aPoint);
+ if (previousThing.InVisibleOrCollapsibleCharacters()) {
+ return EditorDOMPointType::AtEndOf(*previousThing.TextPtr());
+ }
+ }
+ if (HTMLEditUtils::CanNodeContain(*aPoint.GetContainer(),
+ *nsGkAtoms::textTagName)) {
+ return aPoint.template To<EditorDOMPointType>();
+ }
+ if (MOZ_UNLIKELY(aPoint.GetContainer() == &aEditingHost ||
+ !aPoint.template GetContainerParentAs<nsIContent>() ||
+ !HTMLEditUtils::CanNodeContain(
+ *aPoint.template ContainerParentAs<nsIContent>(),
+ *nsGkAtoms::textTagName))) {
+ return EditorDOMPointType();
+ }
+ return aPoint.ParentPoint().template To<EditorDOMPointType>();
+}
+
+// static
+template <typename EditorDOMPointType, typename EditorDOMPointTypeInput>
+Result<EditorDOMPointType, nsresult>
+HTMLEditUtils::ComputePointToPutCaretInElementIfOutside(
+ const Element& aElement, const EditorDOMPointTypeInput& aCurrentPoint) {
+ MOZ_ASSERT(aCurrentPoint.IsSet());
+
+ // FYI: This was moved from
+ // https://searchfox.org/mozilla-central/rev/d3c2f51d89c3ca008ff0cb5a057e77ccd973443e/editor/libeditor/HTMLEditSubActionHandler.cpp#9193
+
+ // Use range boundaries and RangeUtils::CompareNodeToRange() to compare
+ // selection start to new block.
+ bool nodeBefore, nodeAfter;
+ nsresult rv = RangeUtils::CompareNodeToRangeBoundaries(
+ const_cast<Element*>(&aElement), aCurrentPoint.ToRawRangeBoundary(),
+ aCurrentPoint.ToRawRangeBoundary(), &nodeBefore, &nodeAfter);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("RangeUtils::CompareNodeToRange() failed");
+ return Err(rv);
+ }
+
+ if (nodeBefore && nodeAfter) {
+ return EditorDOMPointType(); // aCurrentPoint is in aElement
+ }
+
+ if (nodeBefore) {
+ // selection is after block. put at end of block.
+ const nsIContent* lastEditableContent = HTMLEditUtils::GetLastChild(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!lastEditableContent) {
+ lastEditableContent = &aElement;
+ }
+ if (lastEditableContent->IsText() ||
+ HTMLEditUtils::IsContainerNode(*lastEditableContent)) {
+ return EditorDOMPointType::AtEndOf(*lastEditableContent);
+ }
+ MOZ_ASSERT(lastEditableContent->GetParentNode());
+ return EditorDOMPointType::After(*lastEditableContent);
+ }
+
+ // selection is before block. put at start of block.
+ const nsIContent* firstEditableContent = HTMLEditUtils::GetFirstChild(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!firstEditableContent) {
+ firstEditableContent = &aElement;
+ }
+ if (firstEditableContent->IsText() ||
+ HTMLEditUtils::IsContainerNode(*firstEditableContent)) {
+ MOZ_ASSERT(firstEditableContent->GetParentNode());
+ // XXX Shouldn't this be EditorDOMPointType(firstEditableContent, 0u)?
+ return EditorDOMPointType(firstEditableContent);
+ }
+ // XXX And shouldn't this be EditorDOMPointType(firstEditableContent)?
+ return EditorDOMPointType(firstEditableContent, 0u);
+}
+
+// static
+bool HTMLEditUtils::IsInlineStyleSetByElement(
+ const nsIContent& aContent, const EditorInlineStyle& aStyle,
+ const nsAString* aValue, nsAString* aOutValue /* = nullptr */) {
+ for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) {
+ if (aStyle.mHTMLProperty != element->NodeInfo()->NameAtom()) {
+ continue;
+ }
+ if (!aStyle.mAttribute) {
+ return true;
+ }
+ nsAutoString value;
+ element->GetAttr(aStyle.mAttribute, value);
+ if (aOutValue) {
+ *aOutValue = value;
+ }
+ if (!value.IsEmpty()) {
+ if (!aValue) {
+ return true;
+ }
+ if (aValue->Equals(value, nsCaseInsensitiveStringComparator)) {
+ return true;
+ }
+ // We found the prop with the attribute, but the value doesn't match.
+ return false;
+ }
+ }
+ return false;
+}
+
+// static
+size_t HTMLEditUtils::CollectChildren(
+ const nsINode& aNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ size_t aIndexToInsertChildren, const CollectChildrenOptions& aOptions) {
+ // FYI: This was moved from
+ // https://searchfox.org/mozilla-central/rev/4bce7d85ba4796dd03c5dcc7cfe8eee0e4c07b3b/editor/libeditor/HTMLEditSubActionHandler.cpp#6261
+
+ size_t numberOfFoundChildren = 0;
+ for (nsIContent* content =
+ GetFirstChild(aNode, {WalkTreeOption::IgnoreNonEditableNode});
+ content; content = content->GetNextSibling()) {
+ if ((aOptions.contains(CollectChildrenOption::CollectListChildren) &&
+ (HTMLEditUtils::IsAnyListElement(content) ||
+ HTMLEditUtils::IsListItem(content))) ||
+ (aOptions.contains(CollectChildrenOption::CollectTableChildren) &&
+ HTMLEditUtils::IsAnyTableElement(content))) {
+ numberOfFoundChildren += HTMLEditUtils::CollectChildren(
+ *content, aOutArrayOfContents,
+ aIndexToInsertChildren + numberOfFoundChildren, aOptions);
+ continue;
+ }
+
+ if (aOptions.contains(CollectChildrenOption::IgnoreNonEditableChildren) &&
+ !EditorUtils::IsEditableContent(*content, EditorType::HTML)) {
+ continue;
+ }
+ if (aOptions.contains(CollectChildrenOption::IgnoreInvisibleTextNodes) &&
+ content->IsText() &&
+ !HTMLEditUtils::IsVisibleTextNode(*content->AsText())) {
+ continue;
+ }
+ aOutArrayOfContents.InsertElementAt(
+ aIndexToInsertChildren + numberOfFoundChildren++, *content);
+ }
+ return numberOfFoundChildren;
+}
+
+// static
+size_t HTMLEditUtils::CollectEmptyInlineContainerDescendants(
+ const nsINode& aNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ const EmptyCheckOptions& aOptions, BlockInlineCheck aBlockInlineCheck) {
+ size_t numberOfFoundElements = 0;
+ for (Element* element = aNode.GetFirstElementChild(); element;) {
+ if (HTMLEditUtils::IsEmptyInlineContainer(*element, aOptions,
+ aBlockInlineCheck)) {
+ aOutArrayOfContents.AppendElement(*element);
+ numberOfFoundElements++;
+ nsIContent* nextContent = element->GetNextNonChildNode(&aNode);
+ element = nullptr;
+ for (; nextContent; nextContent = nextContent->GetNextNode(&aNode)) {
+ if (nextContent->IsElement()) {
+ element = nextContent->AsElement();
+ break;
+ }
+ }
+ continue;
+ }
+
+ nsIContent* nextContent = element->GetNextNode(&aNode);
+ element = nullptr;
+ for (; nextContent; nextContent = nextContent->GetNextNode(&aNode)) {
+ if (nextContent->IsElement()) {
+ element = nextContent->AsElement();
+ break;
+ }
+ }
+ }
+ return numberOfFoundElements;
+}
+
+// static
+bool HTMLEditUtils::ElementHasAttributeExcept(const Element& aElement,
+ const nsAtom& aAttribute1,
+ const nsAtom& aAttribute2,
+ const nsAtom& aAttribute3) {
+ // FYI: This was moved from
+ // https://searchfox.org/mozilla-central/rev/0b1543e85d13c30a13c57e959ce9815a3f0fa1d3/editor/libeditor/HTMLStyleEditor.cpp#1626
+ for (auto i : IntegerRange<uint32_t>(aElement.GetAttrCount())) {
+ const nsAttrName* name = aElement.GetAttrNameAt(i);
+ if (!name->NamespaceEquals(kNameSpaceID_None)) {
+ return true;
+ }
+
+ if (name->LocalName() == &aAttribute1 ||
+ name->LocalName() == &aAttribute2 ||
+ name->LocalName() == &aAttribute3) {
+ continue; // Ignore the given attribute
+ }
+
+ // Ignore empty style, class and id attributes because those attributes are
+ // not meaningful with empty value.
+ if (name->LocalName() == nsGkAtoms::style ||
+ name->LocalName() == nsGkAtoms::_class ||
+ name->LocalName() == nsGkAtoms::id) {
+ if (aElement.HasNonEmptyAttr(name->LocalName())) {
+ return true;
+ }
+ continue;
+ }
+
+ // Ignore special _moz attributes
+ nsAutoString attrString;
+ name->LocalName()->ToString(attrString);
+ if (!StringBeginsWith(attrString, u"_moz"_ns)) {
+ return true;
+ }
+ }
+ // if we made it through all of them without finding a real attribute
+ // other than aAttribute, then return true
+ return false;
+}
+
+bool HTMLEditUtils::GetNormalizedHTMLColorValue(const nsAString& aColorValue,
+ nsAString& aNormalizedValue) {
+ nsAttrValue value;
+ if (!value.ParseColor(aColorValue)) {
+ aNormalizedValue = aColorValue;
+ return false;
+ }
+ nscolor color = NS_RGB(0, 0, 0);
+ MOZ_ALWAYS_TRUE(value.GetColorValue(color));
+ aNormalizedValue = NS_ConvertASCIItoUTF16(nsPrintfCString(
+ "#%02x%02x%02x", NS_GET_R(color), NS_GET_G(color), NS_GET_B(color)));
+ return true;
+}
+
+bool HTMLEditUtils::IsSameHTMLColorValue(
+ const nsAString& aColorA, const nsAString& aColorB,
+ TransparentKeyword aTransparentKeyword) {
+ if (aTransparentKeyword == TransparentKeyword::Allowed) {
+ const bool isATransparent = aColorA.LowerCaseEqualsLiteral("transparent");
+ const bool isBTransparent = aColorB.LowerCaseEqualsLiteral("transparent");
+ if (isATransparent || isBTransparent) {
+ return isATransparent && isBTransparent;
+ }
+ }
+ nsAttrValue valueA, valueB;
+ if (!valueA.ParseColor(aColorA) || !valueB.ParseColor(aColorB)) {
+ return false;
+ }
+ nscolor colorA = NS_RGB(0, 0, 0), colorB = NS_RGB(0, 0, 0);
+ MOZ_ALWAYS_TRUE(valueA.GetColorValue(colorA));
+ MOZ_ALWAYS_TRUE(valueB.GetColorValue(colorB));
+ return colorA == colorB;
+}
+
+bool HTMLEditUtils::MaybeCSSSpecificColorValue(const nsAString& aColorValue) {
+ if (aColorValue.IsEmpty() || aColorValue.First() == '#') {
+ return false; // Quick return for the most cases.
+ }
+
+ nsAutoString colorValue(aColorValue);
+ colorValue.CompressWhitespace(true, true);
+ if (colorValue.LowerCaseEqualsASCII("transparent")) {
+ return true;
+ }
+ nscolor color = NS_RGB(0, 0, 0);
+ if (colorValue.IsEmpty() || colorValue.First() == '#') {
+ return false;
+ }
+ const NS_ConvertUTF16toUTF8 colorU8(colorValue);
+ if (Servo_ColorNameToRgb(&colorU8, &color)) {
+ return false;
+ }
+ if (colorValue.LowerCaseEqualsASCII("initial") ||
+ colorValue.LowerCaseEqualsASCII("inherit") ||
+ colorValue.LowerCaseEqualsASCII("unset") ||
+ colorValue.LowerCaseEqualsASCII("revert") ||
+ colorValue.LowerCaseEqualsASCII("currentcolor")) {
+ return true;
+ }
+ return ServoCSSParser::IsValidCSSColor(colorU8);
+}
+
+static bool ComputeColor(const nsAString& aColorValue, nscolor* aColor,
+ bool* aIsCurrentColor) {
+ return ServoCSSParser::ComputeColor(nullptr, NS_RGB(0, 0, 0),
+ NS_ConvertUTF16toUTF8(aColorValue),
+ aColor, aIsCurrentColor);
+}
+
+static bool ComputeColor(const nsACString& aColorValue, nscolor* aColor,
+ bool* aIsCurrentColor) {
+ return ServoCSSParser::ComputeColor(nullptr, NS_RGB(0, 0, 0), aColorValue,
+ aColor, aIsCurrentColor);
+}
+
+bool HTMLEditUtils::CanConvertToHTMLColorValue(const nsAString& aColorValue) {
+ bool isCurrentColor = false;
+ nscolor color = NS_RGB(0, 0, 0);
+ return ComputeColor(aColorValue, &color, &isCurrentColor) &&
+ !isCurrentColor && NS_GET_A(color) == 0xFF;
+}
+
+bool HTMLEditUtils::ConvertToNormalizedHTMLColorValue(
+ const nsAString& aColorValue, nsAString& aNormalizedValue) {
+ bool isCurrentColor = false;
+ nscolor color = NS_RGB(0, 0, 0);
+ if (!ComputeColor(aColorValue, &color, &isCurrentColor) || isCurrentColor ||
+ NS_GET_A(color) != 0xFF) {
+ aNormalizedValue = aColorValue;
+ return false;
+ }
+ aNormalizedValue.Truncate();
+ aNormalizedValue.AppendPrintf("#%02x%02x%02x", NS_GET_R(color),
+ NS_GET_G(color), NS_GET_B(color));
+ return true;
+}
+
+bool HTMLEditUtils::GetNormalizedCSSColorValue(const nsAString& aColorValue,
+ ZeroAlphaColor aZeroAlphaColor,
+ nsAString& aNormalizedValue) {
+ bool isCurrentColor = false;
+ nscolor color = NS_RGB(0, 0, 0);
+ if (!ComputeColor(aColorValue, &color, &isCurrentColor)) {
+ aNormalizedValue = aColorValue;
+ return false;
+ }
+
+ // If it's currentcolor, let's return it as-is since we cannot resolve it
+ // without ancestors.
+ if (isCurrentColor) {
+ aNormalizedValue = aColorValue;
+ return true;
+ }
+
+ if (aZeroAlphaColor == ZeroAlphaColor::TransparentKeyword &&
+ NS_GET_A(color) == 0) {
+ aNormalizedValue.AssignLiteral("transparent");
+ return true;
+ }
+
+ // Get serialized color value (i.e., "rgb()" or "rgba()").
+ aNormalizedValue.Truncate();
+ nsStyleUtil::GetSerializedColorValue(color, aNormalizedValue);
+ return true;
+}
+
+template <typename CharType>
+bool HTMLEditUtils::IsSameCSSColorValue(const nsTSubstring<CharType>& aColorA,
+ const nsTSubstring<CharType>& aColorB) {
+ bool isACurrentColor = false;
+ nscolor colorA = NS_RGB(0, 0, 0);
+ if (!ComputeColor(aColorA, &colorA, &isACurrentColor)) {
+ return false;
+ }
+ bool isBCurrentColor = false;
+ nscolor colorB = NS_RGB(0, 0, 0);
+ if (!ComputeColor(aColorB, &colorB, &isBCurrentColor)) {
+ return false;
+ }
+ if (isACurrentColor || isBCurrentColor) {
+ return isACurrentColor && isBCurrentColor;
+ }
+ return colorA == colorB;
+}
+
+/******************************************************************************
+ * SelectedTableCellScanner
+ ******************************************************************************/
+
+SelectedTableCellScanner::SelectedTableCellScanner(
+ const AutoRangeArray& aRanges) {
+ if (aRanges.Ranges().IsEmpty()) {
+ return;
+ }
+ Element* firstSelectedCellElement =
+ HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(
+ aRanges.FirstRangeRef());
+ if (!firstSelectedCellElement) {
+ return; // We're not in table cell selection mode.
+ }
+ mSelectedCellElements.SetCapacity(aRanges.Ranges().Length());
+ mSelectedCellElements.AppendElement(*firstSelectedCellElement);
+ for (uint32_t i = 1; i < aRanges.Ranges().Length(); i++) {
+ nsRange* range = aRanges.Ranges()[i];
+ if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
+ continue; // Shouldn't occur in normal conditions.
+ }
+ // Just ignore selection ranges which do not select only one table
+ // cell element. This is possible case if web apps sets multiple
+ // selections and first range selects a table cell element.
+ if (Element* selectedCellElement =
+ HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(*range)) {
+ mSelectedCellElements.AppendElement(*selectedCellElement);
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h
new file mode 100644
index 0000000000..115a329247
--- /dev/null
+++ b/editor/libeditor/HTMLEditUtils.h
@@ -0,0 +1,2788 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLEditUtils_h
+#define HTMLEditUtils_h
+
+/**
+ * This header declares/defines static helper methods as members of
+ * HTMLEditUtils. If you want to create or look for helper trivial classes for
+ * HTMLEditor, see HTMLEditHelpers.h.
+ */
+
+#include "EditorBase.h"
+#include "EditorDOMPoint.h"
+#include "EditorForwards.h"
+#include "EditorUtils.h"
+#include "HTMLEditHelpers.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/AbstractRange.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Text.h"
+
+#include "nsContentUtils.h"
+#include "nsCRT.h"
+#include "nsGkAtoms.h"
+#include "nsHTMLTags.h"
+#include "nsTArray.h"
+
+class nsAtom;
+class nsPresContext;
+
+namespace mozilla {
+
+enum class CollectChildrenOption {
+ // Ignore non-editable nodes
+ IgnoreNonEditableChildren,
+ // Ignore invisible text nodes
+ IgnoreInvisibleTextNodes,
+ // Collect list children too.
+ CollectListChildren,
+ // Collect table children too.
+ CollectTableChildren,
+};
+
+class HTMLEditUtils final {
+ using AbstractRange = dom::AbstractRange;
+ using Element = dom::Element;
+ using Selection = dom::Selection;
+ using Text = dom::Text;
+
+ public:
+ static constexpr char16_t kNewLine = '\n';
+ static constexpr char16_t kCarriageReturn = '\r';
+ static constexpr char16_t kTab = '\t';
+ static constexpr char16_t kSpace = ' ';
+ static constexpr char16_t kNBSP = 0x00A0;
+ static constexpr char16_t kGreaterThan = '>';
+
+ /**
+ * IsSimplyEditableNode() returns true when aNode is simply editable.
+ * This does NOT means that aNode can be removed from current parent nor
+ * aNode's data is editable.
+ */
+ static bool IsSimplyEditableNode(const nsINode& aNode) {
+ return aNode.IsEditable();
+ }
+
+ /**
+ * Return true if inclusive flat tree ancestor has `inert` state.
+ */
+ static bool ContentIsInert(const nsIContent& aContent);
+
+ /**
+ * IsNeverContentEditableElementByUser() returns true if the element's content
+ * is never editable by user. E.g., the content is always replaced by
+ * native anonymous node or something.
+ */
+ static bool IsNeverElementContentsEditableByUser(const nsIContent& aContent) {
+ return aContent.IsElement() &&
+ (!HTMLEditUtils::IsContainerNode(aContent) ||
+ aContent.IsAnyOfHTMLElements(
+ nsGkAtoms::applet, nsGkAtoms::colgroup, nsGkAtoms::frameset,
+ nsGkAtoms::head, nsGkAtoms::html, nsGkAtoms::iframe,
+ nsGkAtoms::meter, nsGkAtoms::progress, nsGkAtoms::select,
+ nsGkAtoms::textarea));
+ }
+
+ /**
+ * IsNonEditableReplacedContent() returns true when aContent is an inclusive
+ * descendant of a replaced element whose content shouldn't be editable by
+ * user's operation.
+ */
+ static bool IsNonEditableReplacedContent(const nsIContent& aContent) {
+ for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) {
+ if (element->IsAnyOfHTMLElements(nsGkAtoms::select, nsGkAtoms::option,
+ nsGkAtoms::optgroup)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /*
+ * IsRemovalNode() returns true when parent of aContent is editable even
+ * if aContent isn't editable.
+ * This is a valid method to check it if you find the content from point
+ * of view of siblings or parents of aContent.
+ * Note that padding `<br>` element for empty editor and manual native
+ * anonymous content should be deletable even after `HTMLEditor` is destroyed
+ * because they are owned/managed by `HTMLEditor`.
+ */
+ static bool IsRemovableNode(const nsIContent& aContent) {
+ return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent) ||
+ aContent.IsRootOfNativeAnonymousSubtree() ||
+ (aContent.GetParentNode() &&
+ aContent.GetParentNode()->IsEditable() &&
+ &aContent != aContent.OwnerDoc()->GetBody() &&
+ &aContent != aContent.OwnerDoc()->GetDocumentElement());
+ }
+
+ /**
+ * IsRemovableFromParentNode() returns true when aContent is editable, has a
+ * parent node and the parent node is also editable.
+ * This is a valid method to check it if you find the content from point
+ * of view of descendants of aContent.
+ * Note that padding `<br>` element for empty editor and manual native
+ * anonymous content should be deletable even after `HTMLEditor` is destroyed
+ * because they are owned/managed by `HTMLEditor`.
+ */
+ static bool IsRemovableFromParentNode(const nsIContent& aContent) {
+ return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent) ||
+ aContent.IsRootOfNativeAnonymousSubtree() ||
+ (aContent.IsEditable() && aContent.GetParentNode() &&
+ aContent.GetParentNode()->IsEditable() &&
+ &aContent != aContent.OwnerDoc()->GetBody() &&
+ &aContent != aContent.OwnerDoc()->GetDocumentElement());
+ }
+
+ /**
+ * CanContentsBeJoined() returns true if aLeftContent and aRightContent can be
+ * joined.
+ */
+ static bool CanContentsBeJoined(const nsIContent& aLeftContent,
+ const nsIContent& aRightContent);
+
+ /**
+ * Returns true if aContent is an element and it should be treated as a block.
+ *
+ * @param aBlockInlineCheck
+ * - If UseHTMLDefaultStyle or `editor.block_inline_check.use_computed_style`
+ * pref is false, this returns true only for HTML elements which are defined
+ * as a block by the default style. I.e., non-HTML elements are always
+ * treated as inline.
+ * - If UseComputedDisplayOutsideStyle, this returns true for element nodes
+ * whose display-outside is not inline nor ruby. This is useful to get
+ * inclusive ancestor block element.
+ * - If UseComputedDisplayStyle, this returns true for element nodes whose
+ * display-outside is not inline or whose display-inside is flow-root and they
+ * do not appear as a form control. This is useful to check whether
+ * collapsible white-spaces at the element edges are visible or invisible or
+ * whether <br> element at end of the element is visible or invisible.
+ */
+ [[nodiscard]] static bool IsBlockElement(const nsIContent& aContent,
+ BlockInlineCheck aBlockInlineCheck);
+
+ /**
+ * This is designed to check elements or non-element nodes which are layed out
+ * as inline. Therefore, inline-block etc and ruby are treated as inline.
+ * Note that invisible non-element nodes like comment nodes are also treated
+ * as inline.
+ *
+ * @param aBlockInlineCheck UseComputedDisplayOutsideStyle and
+ * UseComputedDisplayStyle return same result for
+ * any elements.
+ */
+ [[nodiscard]] static bool IsInlineContent(const nsIContent& aContent,
+ BlockInlineCheck aBlockInlineCheck);
+
+ /**
+ * IsVisibleElementEvenIfLeafNode() returns true if aContent is an empty block
+ * element, a visible replaced element such as a form control. This does not
+ * check the layout information.
+ */
+ static bool IsVisibleElementEvenIfLeafNode(const nsIContent& aContent);
+
+ static bool IsInlineStyle(nsINode* aNode);
+
+ /**
+ * IsDisplayOutsideInline() returns true if display-outside value is
+ * "inside". This does NOT flush the layout.
+ */
+ [[nodiscard]] static bool IsDisplayOutsideInline(const Element& aElement);
+
+ /**
+ * IsDisplayInsideFlowRoot() returns true if display-inline value of aElement
+ * is "flow-root". This does NOT flush the layout.
+ */
+ [[nodiscard]] static bool IsDisplayInsideFlowRoot(const Element& aElement);
+
+ /**
+ * Return true if aElement is a flex item or a grid item. This works only
+ * when aElement has a primary frame.
+ */
+ [[nodiscard]] static bool IsFlexOrGridItem(const Element& aElement);
+
+ /**
+ * IsRemovableInlineStyleElement() returns true if aElement is an inline
+ * element and can be removed or split to in order to modifying inline
+ * styles.
+ */
+ static bool IsRemovableInlineStyleElement(Element& aElement);
+
+ /**
+ * Return true if aTagName is one of the format element name of
+ * Document.execCommand("formatBlock").
+ */
+ [[nodiscard]] static bool IsFormatTagForFormatBlockCommand(
+ const nsStaticAtom& aTagName) {
+ return
+ // clang-format off
+ &aTagName == nsGkAtoms::address ||
+ &aTagName == nsGkAtoms::article ||
+ &aTagName == nsGkAtoms::aside ||
+ &aTagName == nsGkAtoms::blockquote ||
+ &aTagName == nsGkAtoms::dd ||
+ &aTagName == nsGkAtoms::div ||
+ &aTagName == nsGkAtoms::dl ||
+ &aTagName == nsGkAtoms::dt ||
+ &aTagName == nsGkAtoms::footer ||
+ &aTagName == nsGkAtoms::h1 ||
+ &aTagName == nsGkAtoms::h2 ||
+ &aTagName == nsGkAtoms::h3 ||
+ &aTagName == nsGkAtoms::h4 ||
+ &aTagName == nsGkAtoms::h5 ||
+ &aTagName == nsGkAtoms::h6 ||
+ &aTagName == nsGkAtoms::header ||
+ &aTagName == nsGkAtoms::hgroup ||
+ &aTagName == nsGkAtoms::main ||
+ &aTagName == nsGkAtoms::nav ||
+ &aTagName == nsGkAtoms::p ||
+ &aTagName == nsGkAtoms::pre ||
+ &aTagName == nsGkAtoms::section;
+ // clang-format on
+ }
+
+ /**
+ * Return true if aContent is a format element of
+ * Document.execCommand("formatBlock").
+ */
+ [[nodiscard]] static bool IsFormatElementForFormatBlockCommand(
+ const nsIContent& aContent) {
+ if (!aContent.IsHTMLElement() ||
+ !aContent.NodeInfo()->NameAtom()->IsStatic()) {
+ return false;
+ }
+ const nsStaticAtom* tagName = aContent.NodeInfo()->NameAtom()->AsStatic();
+ return IsFormatTagForFormatBlockCommand(*tagName);
+ }
+
+ /**
+ * Return true if aTagName is one of the format element name of
+ * cmd_paragraphState.
+ */
+ [[nodiscard]] static bool IsFormatTagForParagraphStateCommand(
+ const nsStaticAtom& aTagName) {
+ return
+ // clang-format off
+ &aTagName == nsGkAtoms::address ||
+ &aTagName == nsGkAtoms::dd ||
+ &aTagName == nsGkAtoms::dl ||
+ &aTagName == nsGkAtoms::dt ||
+ &aTagName == nsGkAtoms::h1 ||
+ &aTagName == nsGkAtoms::h2 ||
+ &aTagName == nsGkAtoms::h3 ||
+ &aTagName == nsGkAtoms::h4 ||
+ &aTagName == nsGkAtoms::h5 ||
+ &aTagName == nsGkAtoms::h6 ||
+ &aTagName == nsGkAtoms::p ||
+ &aTagName == nsGkAtoms::pre;
+ // clang-format on
+ }
+
+ /**
+ * Return true if aContent is a format element of cmd_paragraphState.
+ */
+ [[nodiscard]] static bool IsFormatElementForParagraphStateCommand(
+ const nsIContent& aContent) {
+ if (!aContent.IsHTMLElement() ||
+ !aContent.NodeInfo()->NameAtom()->IsStatic()) {
+ return false;
+ }
+ const nsStaticAtom* tagName = aContent.NodeInfo()->NameAtom()->AsStatic();
+ return IsFormatTagForParagraphStateCommand(*tagName);
+ }
+
+ static bool IsNodeThatCanOutdent(nsINode* aNode);
+ static bool IsHeader(nsINode& aNode);
+ static bool IsListItem(const nsINode* aNode);
+ static bool IsTable(nsINode* aNode);
+ static bool IsTableRow(nsINode* aNode);
+ static bool IsAnyTableElement(const nsINode* aNode);
+ static bool IsAnyTableElementButNotTable(nsINode* aNode);
+ static bool IsTableCell(const nsINode* aNode);
+ static bool IsTableCellOrCaption(nsINode& aNode);
+ static bool IsAnyListElement(const nsINode* aNode);
+ static bool IsPre(const nsINode* aNode);
+ static bool IsImage(nsINode* aNode);
+ static bool IsLink(const nsINode* aNode);
+ static bool IsNamedAnchor(const nsINode* aNode);
+ static bool IsMozDiv(nsINode* aNode);
+ static bool IsMailCite(const Element& aElement);
+ static bool IsFormWidget(const nsINode* aNode);
+ static bool SupportsAlignAttr(nsINode& aNode);
+
+ static bool CanNodeContain(const nsINode& aParent, const nsIContent& aChild) {
+ switch (aParent.NodeType()) {
+ case nsINode::ELEMENT_NODE:
+ case nsINode::DOCUMENT_FRAGMENT_NODE:
+ return HTMLEditUtils::CanNodeContain(*aParent.NodeInfo()->NameAtom(),
+ aChild);
+ }
+ return false;
+ }
+
+ static bool CanNodeContain(const nsINode& aParent,
+ const nsAtom& aChildNodeName) {
+ switch (aParent.NodeType()) {
+ case nsINode::ELEMENT_NODE:
+ case nsINode::DOCUMENT_FRAGMENT_NODE:
+ return HTMLEditUtils::CanNodeContain(*aParent.NodeInfo()->NameAtom(),
+ aChildNodeName);
+ }
+ return false;
+ }
+
+ static bool CanNodeContain(const nsAtom& aParentNodeName,
+ const nsIContent& aChild) {
+ switch (aChild.NodeType()) {
+ case nsINode::TEXT_NODE:
+ case nsINode::COMMENT_NODE:
+ case nsINode::CDATA_SECTION_NODE:
+ case nsINode::ELEMENT_NODE:
+ case nsINode::DOCUMENT_FRAGMENT_NODE:
+ return HTMLEditUtils::CanNodeContain(aParentNodeName,
+ *aChild.NodeInfo()->NameAtom());
+ }
+ return false;
+ }
+
+ // XXX Only this overload does not check the node type. Therefore, only this
+ // handle Document and ProcessingInstructionTagName.
+ static bool CanNodeContain(const nsAtom& aParentNodeName,
+ const nsAtom& aChildNodeName) {
+ nsHTMLTag childTagEnum;
+ if (&aChildNodeName == nsGkAtoms::textTagName) {
+ childTagEnum = eHTMLTag_text;
+ } else if (&aChildNodeName == nsGkAtoms::commentTagName ||
+ &aChildNodeName == nsGkAtoms::cdataTagName) {
+ childTagEnum = eHTMLTag_comment;
+ } else {
+ childTagEnum =
+ nsHTMLTags::AtomTagToId(const_cast<nsAtom*>(&aChildNodeName));
+ }
+
+ nsHTMLTag parentTagEnum =
+ nsHTMLTags::AtomTagToId(const_cast<nsAtom*>(&aParentNodeName));
+ return HTMLEditUtils::CanNodeContain(parentTagEnum, childTagEnum);
+ }
+
+ /**
+ * CanElementContainParagraph() returns true if aElement can have a <p>
+ * element as its child or its descendant.
+ */
+ static bool CanElementContainParagraph(const Element& aElement) {
+ if (HTMLEditUtils::CanNodeContain(aElement, *nsGkAtoms::p)) {
+ return true;
+ }
+
+ // Even if the element cannot have a <p> element as a child, it can contain
+ // <p> element as a descendant if it's one of the following elements.
+ if (aElement.IsAnyOfHTMLElements(nsGkAtoms::ol, nsGkAtoms::ul,
+ nsGkAtoms::dl, nsGkAtoms::table,
+ nsGkAtoms::thead, nsGkAtoms::tbody,
+ nsGkAtoms::tfoot, nsGkAtoms::tr)) {
+ return true;
+ }
+
+ // XXX Otherwise, Chromium checks the CSS box is a block, but we don't do it
+ // for now.
+ return false;
+ }
+
+ /**
+ * Return a point which can insert a node whose name is aTagName scanning
+ * from aPoint to its ancestor points.
+ */
+ template <typename EditorDOMPointType>
+ static EditorDOMPoint GetInsertionPointInInclusiveAncestor(
+ const nsAtom& aTagName, const EditorDOMPointType& aPoint,
+ const Element* aAncestorLimit = nullptr) {
+ if (MOZ_UNLIKELY(!aPoint.IsInContentNode())) {
+ return EditorDOMPoint();
+ }
+ Element* lastChild = nullptr;
+ for (Element* containerElement :
+ aPoint.template ContainerAs<nsIContent>()
+ ->template InclusiveAncestorsOfType<Element>()) {
+ if (!HTMLEditUtils::IsSimplyEditableNode(*containerElement)) {
+ return EditorDOMPoint();
+ }
+ if (HTMLEditUtils::CanNodeContain(*containerElement, aTagName)) {
+ return lastChild ? EditorDOMPoint(lastChild)
+ : aPoint.template To<EditorDOMPoint>();
+ }
+ if (containerElement == aAncestorLimit) {
+ return EditorDOMPoint();
+ }
+ lastChild = containerElement;
+ }
+ return EditorDOMPoint();
+ }
+
+ /**
+ * IsContainerNode() returns true if aContent is a container node.
+ */
+ static bool IsContainerNode(const nsIContent& aContent) {
+ nsHTMLTag tagEnum;
+ // XXX Should this handle #cdata-section too?
+ if (aContent.IsText()) {
+ tagEnum = eHTMLTag_text;
+ } else {
+ // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some
+ // difference?
+ tagEnum = nsHTMLTags::StringTagToId(aContent.NodeName());
+ }
+ return HTMLEditUtils::IsContainerNode(tagEnum);
+ }
+
+ /**
+ * IsSplittableNode() returns true if aContent can split.
+ */
+ static bool IsSplittableNode(const nsIContent& aContent) {
+ if (!EditorUtils::IsEditableContent(aContent,
+ EditorUtils::EditorType::HTML) ||
+ !HTMLEditUtils::IsRemovableFromParentNode(aContent)) {
+ return false;
+ }
+ if (aContent.IsElement()) {
+ // XXX Perhaps, instead of using container, we should have "splittable"
+ // information in the DB. E.g., `<template>`, `<script>` elements
+ // can have children, but shouldn't be split.
+ return HTMLEditUtils::IsContainerNode(aContent) &&
+ !aContent.IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::button,
+ nsGkAtoms::caption, nsGkAtoms::table,
+ nsGkAtoms::tbody, nsGkAtoms::tfoot,
+ nsGkAtoms::thead, nsGkAtoms::tr) &&
+ !HTMLEditUtils::IsNeverElementContentsEditableByUser(aContent) &&
+ !HTMLEditUtils::IsNonEditableReplacedContent(aContent);
+ }
+ return aContent.IsText() && aContent.Length() > 0;
+ }
+
+ /**
+ * See execCommand spec:
+ * https://w3c.github.io/editing/execCommand.html#non-list-single-line-container
+ * https://w3c.github.io/editing/execCommand.html#single-line-container
+ */
+ static bool IsNonListSingleLineContainer(const nsINode& aNode);
+ static bool IsSingleLineContainer(const nsINode& aNode);
+
+ /**
+ * IsVisibleTextNode() returns true if aText has visible text. If it has
+ * only white-spaces and they are collapsed, returns false.
+ */
+ [[nodiscard]] static bool IsVisibleTextNode(const Text& aText);
+
+ /**
+ * IsInVisibleTextFrames() returns true if any text in aText is in visible
+ * text frames. Callers have to guarantee that there is no pending reflow.
+ */
+ static bool IsInVisibleTextFrames(nsPresContext* aPresContext,
+ const Text& aText);
+
+ /**
+ * IsVisibleBRElement() and IsInvisibleBRElement() return true if aContent is
+ * a visible HTML <br> element, i.e., not a padding <br> element for making
+ * last line in a block element visible, or an invisible <br> element.
+ */
+ static bool IsVisibleBRElement(const nsIContent& aContent) {
+ if (const dom::HTMLBRElement* brElement =
+ dom::HTMLBRElement::FromNode(&aContent)) {
+ return IsVisibleBRElement(*brElement);
+ }
+ return false;
+ }
+ static bool IsVisibleBRElement(const dom::HTMLBRElement& aBRElement) {
+ // If followed by a block boundary without visible content, it's invisible
+ // <br> element.
+ return !HTMLEditUtils::GetElementOfImmediateBlockBoundary(
+ aBRElement, WalkTreeDirection::Forward);
+ }
+ static bool IsInvisibleBRElement(const nsIContent& aContent) {
+ if (const dom::HTMLBRElement* brElement =
+ dom::HTMLBRElement::FromNode(&aContent)) {
+ return IsInvisibleBRElement(*brElement);
+ }
+ return false;
+ }
+ static bool IsInvisibleBRElement(const dom::HTMLBRElement& aBRElement) {
+ return !HTMLEditUtils::IsVisibleBRElement(aBRElement);
+ }
+
+ /**
+ * Return true if `display` of inclusive ancestor of aContent is `none`.
+ */
+ static bool IsInclusiveAncestorCSSDisplayNone(const nsIContent& aContent);
+
+ /**
+ * IsVisiblePreformattedNewLine() and IsInvisiblePreformattedNewLine() return
+ * true if the point is preformatted linefeed and it's visible or invisible.
+ * If linefeed is immediately before a block boundary, it's invisible.
+ *
+ * @param aFollowingBlockElement [out] If the node is followed by a block
+ * boundary, this is set to the element
+ * creating the block boundary.
+ */
+ template <typename EditorDOMPointType>
+ static bool IsVisiblePreformattedNewLine(
+ const EditorDOMPointType& aPoint,
+ Element** aFollowingBlockElement = nullptr) {
+ if (aFollowingBlockElement) {
+ *aFollowingBlockElement = nullptr;
+ }
+ if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() ||
+ !aPoint.IsCharPreformattedNewLine()) {
+ return false;
+ }
+ // If there are some other characters in the text node, it's a visible
+ // linefeed.
+ if (!aPoint.IsAtLastContent()) {
+ if (EditorUtils::IsWhiteSpacePreformatted(
+ *aPoint.template ContainerAs<Text>())) {
+ return true;
+ }
+ const nsTextFragment& textFragment =
+ aPoint.template ContainerAs<Text>()->TextFragment();
+ for (uint32_t offset = aPoint.Offset() + 1;
+ offset < textFragment.GetLength(); ++offset) {
+ char16_t ch = textFragment.CharAt(AssertedCast<int32_t>(offset));
+ if (nsCRT::IsAsciiSpace(ch) && ch != HTMLEditUtils::kNewLine) {
+ continue; // ASCII white-space which is collapsed into the linefeed.
+ }
+ return true; // There is a visible character after it.
+ }
+ }
+ // If followed by a block boundary without visible content, it's invisible
+ // linefeed.
+ Element* followingBlockElement =
+ HTMLEditUtils::GetElementOfImmediateBlockBoundary(
+ *aPoint.template ContainerAs<Text>(), WalkTreeDirection::Forward);
+ if (aFollowingBlockElement) {
+ *aFollowingBlockElement = followingBlockElement;
+ }
+ return !followingBlockElement;
+ }
+ template <typename EditorDOMPointType>
+ static bool IsInvisiblePreformattedNewLine(
+ const EditorDOMPointType& aPoint,
+ Element** aFollowingBlockElement = nullptr) {
+ if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() ||
+ !aPoint.IsCharPreformattedNewLine()) {
+ if (aFollowingBlockElement) {
+ *aFollowingBlockElement = nullptr;
+ }
+ return false;
+ }
+ return !IsVisiblePreformattedNewLine(aPoint, aFollowingBlockElement);
+ }
+
+ /**
+ * ShouldInsertLinefeedCharacter() returns true if the caller should insert
+ * a linefeed character instead of <br> element.
+ */
+ static bool ShouldInsertLinefeedCharacter(
+ const EditorDOMPoint& aPointToInsert, const Element& aEditingHost);
+
+ /**
+ * IsEmptyNode() returns false if aNode has some visible content nodes,
+ * list elements or table elements.
+ *
+ * @param aPresContext Must not be nullptr if
+ * EmptyCheckOption::SafeToAskLayout is set.
+ * @param aNode The node to check whether it's empty.
+ * @param aOptions You can specify which type of elements are visible
+ * and/or whether this can access layout information.
+ * @param aSeenBR [Out] Set to true if this meets an <br> element
+ * before meeting visible things.
+ */
+ enum class EmptyCheckOption {
+ TreatSingleBRElementAsVisible,
+ TreatListItemAsVisible,
+ TreatTableCellAsVisible,
+ TreatNonEditableContentAsInvisible,
+ SafeToAskLayout,
+ };
+ using EmptyCheckOptions = EnumSet<EmptyCheckOption, uint32_t>;
+ static bool IsEmptyNode(nsPresContext* aPresContext, const nsINode& aNode,
+ const EmptyCheckOptions& aOptions = {},
+ bool* aSeenBR = nullptr);
+ static bool IsEmptyNode(const nsINode& aNode,
+ const EmptyCheckOptions& aOptions = {},
+ bool* aSeenBR = nullptr) {
+ MOZ_ASSERT(!aOptions.contains(EmptyCheckOption::SafeToAskLayout));
+ return IsEmptyNode(nullptr, aNode, aOptions, aSeenBR);
+ }
+
+ /**
+ * IsEmptyInlineContainer() returns true if aContent is an inline element
+ * which can have children and does not have meaningful content.
+ */
+ static bool IsEmptyInlineContainer(const nsIContent& aContent,
+ const EmptyCheckOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck) {
+ return HTMLEditUtils::IsInlineContent(aContent, aBlockInlineCheck) &&
+ HTMLEditUtils::IsContainerNode(aContent) &&
+ HTMLEditUtils::IsEmptyNode(aContent, aOptions);
+ }
+
+ /**
+ * IsEmptyBlockElement() returns true if aElement is a block level element
+ * and it doesn't have any visible content.
+ */
+ static bool IsEmptyBlockElement(const Element& aElement,
+ const EmptyCheckOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck) {
+ return HTMLEditUtils::IsBlockElement(aElement, aBlockInlineCheck) &&
+ HTMLEditUtils::IsEmptyNode(aElement, aOptions);
+ }
+
+ /**
+ * Return true if aListElement is completely empty or it has only one list
+ * item element which is empty.
+ */
+ [[nodiscard]] static bool IsEmptyAnyListElement(const Element& aListElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ bool foundListItem = false;
+ for (nsIContent* child = aListElement.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (HTMLEditUtils::IsListItem(child)) {
+ if (foundListItem) {
+ return false; // 2 list items found.
+ }
+ if (!IsEmptyNode(*child, {})) {
+ return false; // found non-empty list item.
+ }
+ foundListItem = true;
+ continue;
+ }
+ if (child->IsElement()) {
+ return false; // found sublist or illegal child.
+ }
+ if (child->IsText() &&
+ HTMLEditUtils::IsVisibleTextNode(*child->AsText())) {
+ return false; // found illegal visible text node.
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Return true if aListElement does not have invalid child.
+ */
+ enum class TreatSubListElementAs { Invalid, Valid };
+ [[nodiscard]] static bool IsValidListElement(
+ const Element& aListElement,
+ TreatSubListElementAs aTreatSubListElementAs) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ for (nsIContent* child = aListElement.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (HTMLEditUtils::IsAnyListElement(child)) {
+ if (aTreatSubListElementAs == TreatSubListElementAs::Invalid) {
+ return false;
+ }
+ continue;
+ }
+ if (child->IsHTMLElement(nsGkAtoms::li)) {
+ if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::ol,
+ nsGkAtoms::ul))) {
+ return false;
+ }
+ continue;
+ }
+ if (child->IsAnyOfHTMLElements(nsGkAtoms::dt, nsGkAtoms::dd)) {
+ if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::dl))) {
+ return false;
+ }
+ continue;
+ }
+ if (MOZ_UNLIKELY(child->IsElement())) {
+ return false;
+ }
+ if (MOZ_LIKELY(child->IsText())) {
+ if (MOZ_UNLIKELY(HTMLEditUtils::IsVisibleTextNode(*child->AsText()))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * IsEmptyOneHardLine() returns true if aArrayOfContents does not represent
+ * 2 or more lines and have meaningful content.
+ */
+ static bool IsEmptyOneHardLine(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ BlockInlineCheck aBlockInlineCheck) {
+ if (NS_WARN_IF(aArrayOfContents.IsEmpty())) {
+ return true;
+ }
+
+ bool brElementHasFound = false;
+ for (OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ if (!EditorUtils::IsEditableContent(content,
+ EditorUtils::EditorType::HTML)) {
+ continue;
+ }
+ if (content->IsHTMLElement(nsGkAtoms::br)) {
+ // If there are 2 or more `<br>` elements, it's not empty line since
+ // there may be only one `<br>` element in a hard line.
+ if (brElementHasFound) {
+ return false;
+ }
+ brElementHasFound = true;
+ continue;
+ }
+ if (!HTMLEditUtils::IsEmptyInlineContainer(
+ content,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ aBlockInlineCheck)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * IsPointAtEdgeOfLink() returns true if aPoint is at start or end of a
+ * link.
+ */
+ template <typename PT, typename CT>
+ static bool IsPointAtEdgeOfLink(const EditorDOMPointBase<PT, CT>& aPoint,
+ Element** aFoundLinkElement = nullptr) {
+ if (aFoundLinkElement) {
+ *aFoundLinkElement = nullptr;
+ }
+ if (!aPoint.IsInContentNode()) {
+ return false;
+ }
+ if (!aPoint.IsStartOfContainer() && !aPoint.IsEndOfContainer()) {
+ return false;
+ }
+ // XXX Assuming it's not in an empty text node because it's unrealistic edge
+ // case.
+ bool maybeStartOfAnchor = aPoint.IsStartOfContainer();
+ for (EditorRawDOMPoint point(aPoint.template ContainerAs<nsIContent>());
+ point.IsSet() && (maybeStartOfAnchor ? point.IsStartOfContainer()
+ : point.IsAtLastContent());
+ point = point.ParentPoint()) {
+ if (HTMLEditUtils::IsLink(point.GetContainer())) {
+ // Now, we're at start or end of <a href>.
+ if (aFoundLinkElement) {
+ *aFoundLinkElement =
+ do_AddRef(point.template ContainerAs<Element>()).take();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * IsContentInclusiveDescendantOfLink() returns true if aContent is a
+ * descendant of a link element.
+ * Note that this returns true even if editing host of aContent is in a link
+ * element.
+ */
+ static bool IsContentInclusiveDescendantOfLink(
+ nsIContent& aContent, Element** aFoundLinkElement = nullptr) {
+ if (aFoundLinkElement) {
+ *aFoundLinkElement = nullptr;
+ }
+ for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsLink(element)) {
+ if (aFoundLinkElement) {
+ *aFoundLinkElement = do_AddRef(element).take();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * IsRangeEntirelyInLink() returns true if aRange is entirely in a link
+ * element.
+ * Note that this returns true even if editing host of the range is in a link
+ * element.
+ */
+ template <typename EditorDOMRangeType>
+ static bool IsRangeEntirelyInLink(const EditorDOMRangeType& aRange,
+ Element** aFoundLinkElement = nullptr) {
+ MOZ_ASSERT(aRange.IsPositionedAndValid());
+ if (aFoundLinkElement) {
+ *aFoundLinkElement = nullptr;
+ }
+ nsINode* commonAncestorNode =
+ nsContentUtils::GetClosestCommonInclusiveAncestor(
+ aRange.StartRef().GetContainer(), aRange.EndRef().GetContainer());
+ if (NS_WARN_IF(!commonAncestorNode) || !commonAncestorNode->IsContent()) {
+ return false;
+ }
+ return IsContentInclusiveDescendantOfLink(*commonAncestorNode->AsContent(),
+ aFoundLinkElement);
+ }
+
+ /**
+ * Get adjacent content node of aNode if there is (even if one is in different
+ * parent element).
+ *
+ * @param aNode The node from which we start to walk the DOM
+ * tree.
+ * @param aOptions See WalkTreeOption for the detail.
+ * @param aBlockInlineCheck Whether considering block vs. inline with the
+ * computed style or the HTML default style.
+ * @param aAncestorLimiter Ancestor limiter element which these methods
+ * never cross its boundary. This is typically
+ * the editing host.
+ */
+ enum class WalkTreeOption {
+ IgnoreNonEditableNode, // Ignore non-editable nodes and their children.
+ IgnoreDataNodeExceptText, // Ignore data nodes which are not text node.
+ IgnoreWhiteSpaceOnlyText, // Ignore text nodes having only white-spaces.
+ StopAtBlockBoundary, // Stop waking the tree at a block boundary.
+ };
+ using WalkTreeOptions = EnumSet<WalkTreeOption>;
+ static nsIContent* GetPreviousContent(
+ const nsINode& aNode, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr) {
+ if (&aNode == aAncestorLimiter ||
+ (aAncestorLimiter &&
+ !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) {
+ return nullptr;
+ }
+ return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Backward,
+ aOptions, aBlockInlineCheck,
+ aAncestorLimiter);
+ }
+ static nsIContent* GetNextContent(const nsINode& aNode,
+ const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr) {
+ if (&aNode == aAncestorLimiter ||
+ (aAncestorLimiter &&
+ !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) {
+ return nullptr;
+ }
+ return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Forward,
+ aOptions, aBlockInlineCheck,
+ aAncestorLimiter);
+ }
+
+ /**
+ * And another version that takes a point in DOM tree rather than a node.
+ */
+ template <typename PT, typename CT>
+ static nsIContent* GetPreviousContent(
+ const EditorDOMPointBase<PT, CT>& aPoint, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr);
+
+ /**
+ * And another version that takes a point in DOM tree rather than a node.
+ *
+ * Note that this may return the child at the offset. E.g., following code
+ * causes infinite loop.
+ *
+ * EditorRawDOMPoint point(aEditableNode);
+ * while (nsIContent* content =
+ * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode})) {
+ * // Do something...
+ * point.Set(content);
+ * }
+ *
+ * Following code must be you expected:
+ *
+ * while (nsIContent* content =
+ * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode}) {
+ * // Do something...
+ * DebugOnly<bool> advanced = point.Advanced();
+ * MOZ_ASSERT(advanced);
+ * point.Set(point.GetChild());
+ * }
+ */
+ template <typename PT, typename CT>
+ static nsIContent* GetNextContent(const EditorDOMPointBase<PT, CT>& aPoint,
+ const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr);
+
+ /**
+ * GetPreviousSibling() return the preceding sibling of aContent which matches
+ * with aOption.
+ *
+ * @param aBlockInlineCheck Can be Unused if aOptions does not contain
+ * StopAtBlockBoundary.
+ */
+ static nsIContent* GetPreviousSibling(
+ const nsIContent& aContent, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) {
+ for (nsIContent* sibling = aContent.GetPreviousSibling(); sibling;
+ sibling = sibling->GetPreviousSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) {
+ return nullptr;
+ }
+ return sibling;
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetNextSibling() return the following sibling of aContent which matches
+ * with aOption.
+ *
+ * @param aBlockInlineCheck Can be Unused if aOptions does not contain
+ * StopAtBlockBoundary.
+ */
+ static nsIContent* GetNextSibling(
+ const nsIContent& aContent, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) {
+ for (nsIContent* sibling = aContent.GetNextSibling(); sibling;
+ sibling = sibling->GetNextSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) {
+ return nullptr;
+ }
+ return sibling;
+ }
+ return nullptr;
+ }
+
+ /**
+ * Return the last child of aNode which matches with aOption.
+ *
+ * @param aBlockInlineCheck Can be unused if aOptions does not contain
+ * StopAtBlockBoundary.
+ */
+ static nsIContent* GetLastChild(
+ const nsINode& aNode, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) {
+ for (nsIContent* child = aNode.GetLastChild(); child;
+ child = child->GetPreviousSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) {
+ return nullptr;
+ }
+ return child;
+ }
+ return nullptr;
+ }
+
+ /**
+ * Return the first child of aNode which matches with aOption.
+ *
+ * @param aBlockInlineCheck Can be unused if aOptions does not contain
+ * StopAtBlockBoundary.
+ */
+ static nsIContent* GetFirstChild(
+ const nsINode& aNode, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) {
+ for (nsIContent* child = aNode.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) {
+ return nullptr;
+ }
+ return child;
+ }
+ return nullptr;
+ }
+
+ /**
+ * Return true if aContent is the last child of aNode with ignoring all
+ * children which do not match with aOption.
+ *
+ * @param aBlockInlineCheck Can be unused if aOptions does not contain
+ * StopAtBlockBoundary.
+ */
+ static bool IsLastChild(
+ const nsIContent& aContent, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) {
+ nsINode* parentNode = aContent.GetParentNode();
+ if (!parentNode) {
+ return false;
+ }
+ return HTMLEditUtils::GetLastChild(*parentNode, aOptions,
+ aBlockInlineCheck) == &aContent;
+ }
+
+ /**
+ * Return true if aContent is the first child of aNode with ignoring all
+ * children which do not match with aOption.
+ *
+ * @param aBlockInlineCheck Can be unused if aOptions does not contain
+ * StopAtBlockBoundary.
+ */
+ static bool IsFirstChild(
+ const nsIContent& aContent, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) {
+ nsINode* parentNode = aContent.GetParentNode();
+ if (!parentNode) {
+ return false;
+ }
+ return HTMLEditUtils::GetFirstChild(*parentNode, aOptions,
+ aBlockInlineCheck) == &aContent;
+ }
+
+ /**
+ * GetAdjacentContentToPutCaret() walks the DOM tree to find an editable node
+ * near aPoint where may be a good point to put caret and keep typing or
+ * deleting.
+ *
+ * @param aPoint The DOM point where to start to search from.
+ * @return If found, returns non-nullptr. Otherwise, nullptr.
+ * Note that if found node is in different table structure
+ * element, this returns nullptr.
+ */
+ enum class WalkTreeDirection { Forward, Backward };
+ template <typename PT, typename CT>
+ static nsIContent* GetAdjacentContentToPutCaret(
+ const EditorDOMPointBase<PT, CT>& aPoint,
+ WalkTreeDirection aWalkTreeDirection, const Element& aEditingHost) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ nsIContent* editableContent = nullptr;
+ if (aWalkTreeDirection == WalkTreeDirection::Backward) {
+ editableContent = HTMLEditUtils::GetPreviousContent(
+ aPoint, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle, &aEditingHost);
+ if (!editableContent) {
+ return nullptr; // Not illegal.
+ }
+ } else {
+ editableContent = HTMLEditUtils::GetNextContent(
+ aPoint, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle, &aEditingHost);
+ if (NS_WARN_IF(!editableContent)) {
+ // Perhaps, illegal because the node pointed by aPoint isn't editable
+ // and nobody of previous nodes is editable.
+ return nullptr;
+ }
+ }
+
+ // scan in the right direction until we find an eligible text node,
+ // but don't cross any breaks, images, or table elements.
+ // XXX This comment sounds odd. editableContent may have already crossed
+ // breaks and/or images if they are non-editable.
+ while (editableContent && !editableContent->IsText() &&
+ !editableContent->IsHTMLElement(nsGkAtoms::br) &&
+ !HTMLEditUtils::IsImage(editableContent)) {
+ if (aWalkTreeDirection == WalkTreeDirection::Backward) {
+ editableContent = HTMLEditUtils::GetPreviousContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle, &aEditingHost);
+ if (NS_WARN_IF(!editableContent)) {
+ return nullptr;
+ }
+ } else {
+ editableContent = HTMLEditUtils::GetNextContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle, &aEditingHost);
+ if (NS_WARN_IF(!editableContent)) {
+ return nullptr;
+ }
+ }
+ }
+
+ // don't cross any table elements
+ if ((!aPoint.IsInContentNode() &&
+ !!HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ *editableContent)) ||
+ (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*editableContent) !=
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ *aPoint.template ContainerAs<nsIContent>()))) {
+ return nullptr;
+ }
+
+ // otherwise, ok, we have found a good spot to put the selection
+ return editableContent;
+ }
+
+ enum class LeafNodeType {
+ // Even if there is a child block, keep scanning a leaf content in it.
+ OnlyLeafNode,
+ // If there is a child block, return it too. Note that this does not
+ // mean that block siblings are not treated as leaf nodes.
+ LeafNodeOrChildBlock,
+ // If there is a non-editable element if and only if scanning from editable
+ // node, return it too.
+ LeafNodeOrNonEditableNode,
+ // Ignore non-editable content at walking the tree.
+ OnlyEditableLeafNode,
+ };
+ using LeafNodeTypes = EnumSet<LeafNodeType>;
+
+ /**
+ * GetLastLeafContent() returns rightmost leaf content in aNode. It depends
+ * on aLeafNodeTypes whether this which types of nodes are treated as leaf
+ * nodes.
+ *
+ * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain
+ * LeafNodeOrCHildBlock.
+ */
+ static nsIContent* GetLastLeafContent(
+ const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes,
+ BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ // editor shouldn't touch child nodes which are replaced with native
+ // anonymous nodes.
+ if (aNode.IsElement() &&
+ HTMLEditUtils::IsNeverElementContentsEditableByUser(
+ *aNode.AsElement())) {
+ return nullptr;
+ }
+ for (nsIContent* content = aNode.GetLastChild(); content;) {
+ if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) &&
+ !EditorUtils::IsEditableContent(*content,
+ EditorUtils::EditorType::HTML)) {
+ content = HTMLEditUtils::GetPreviousContent(
+ *content, {WalkTreeOption::IgnoreNonEditableNode},
+ aBlockInlineCheck, aAncestorLimiter);
+ continue;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
+ HTMLEditUtils::IsBlockElement(*content, aBlockInlineCheck)) {
+ return content;
+ }
+ if (!content->HasChildren() ||
+ HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) {
+ return content;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aNode.IsEditable() && !content->IsEditable()) {
+ return content;
+ }
+ content = content->GetLastChild();
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetFirstLeafContent() returns leftmost leaf content in aNode. It depends
+ * on aLeafNodeTypes whether this scans into a block child or treat block as a
+ * leaf.
+ *
+ * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain
+ * LeafNodeOrCHildBlock.
+ */
+ static nsIContent* GetFirstLeafContent(
+ const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes,
+ BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ // editor shouldn't touch child nodes which are replaced with native
+ // anonymous nodes.
+ if (aNode.IsElement() &&
+ HTMLEditUtils::IsNeverElementContentsEditableByUser(
+ *aNode.AsElement())) {
+ return nullptr;
+ }
+ for (nsIContent* content = aNode.GetFirstChild(); content;) {
+ if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) &&
+ !EditorUtils::IsEditableContent(*content,
+ EditorUtils::EditorType::HTML)) {
+ content = HTMLEditUtils::GetNextContent(
+ *content, {WalkTreeOption::IgnoreNonEditableNode},
+ aBlockInlineCheck, aAncestorLimiter);
+ continue;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
+ HTMLEditUtils::IsBlockElement(*content, aBlockInlineCheck)) {
+ return content;
+ }
+ if (!content->HasChildren() ||
+ HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) {
+ return content;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aNode.IsEditable() && !content->IsEditable()) {
+ return content;
+ }
+ content = content->GetFirstChild();
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetNextLeafContentOrNextBlockElement() returns next leaf content or
+ * next block element of aStartContent inside aAncestorLimiter.
+ * Note that the result may be a contet outside aCurrentBlock if
+ * aStartContent equals aCurrentBlock.
+ *
+ * @param aStartContent The start content to scan next content.
+ * @param aCurrentBlock Must be ancestor of aStartContent. Dispite
+ * the name, inline content is allowed if
+ * aStartContent is in an inline editing host.
+ * @param aLeafNodeTypes See LeafNodeType.
+ * @param aAncestorLimiter Optional, setting this guarantees the
+ * result is in aAncestorLimiter unless
+ * aStartContent is not a descendant of this.
+ */
+ static nsIContent* GetNextLeafContentOrNextBlockElement(
+ const nsIContent& aStartContent, const nsIContent& aCurrentBlock,
+ const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+
+ if (&aStartContent == aAncestorLimiter) {
+ return nullptr;
+ }
+
+ nsIContent* nextContent = aStartContent.GetNextSibling();
+ if (!nextContent) {
+ if (!aStartContent.GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) {
+ if (parentElement == &aCurrentBlock) {
+ return nullptr;
+ }
+ if (parentElement == aAncestorLimiter) {
+ NS_WARNING("Reached editing host while climbing up the DOM tree");
+ return nullptr;
+ }
+ nextContent = parentElement->GetNextSibling();
+ if (nextContent) {
+ break;
+ }
+ if (!parentElement->GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ }
+ MOZ_ASSERT(nextContent);
+ aBlockInlineCheck = IgnoreInsideBlockBoundary(aBlockInlineCheck);
+ }
+
+ // We have a next content. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*nextContent, aBlockInlineCheck)) {
+ return nextContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aStartContent.IsEditable() && !nextContent->IsEditable()) {
+ return nextContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*nextContent)) {
+ // Else if it's a container, get deep leftmost child
+ if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *nextContent, aLeafNodeTypes, aBlockInlineCheck)) {
+ return child;
+ }
+ }
+ // Else return the next content itself.
+ return nextContent;
+ }
+
+ /**
+ * Similar to the above method, but take a DOM point to specify scan start
+ * point.
+ */
+ template <typename PT, typename CT>
+ static nsIContent* GetNextLeafContentOrNextBlockElement(
+ const EditorDOMPointBase<PT, CT>& aStartPoint,
+ const nsIContent& aCurrentBlock, const LeafNodeTypes& aLeafNodeTypes,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT(aStartPoint.IsSet());
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ "Not implemented yet");
+
+ if (!aStartPoint.IsInContentNode()) {
+ return nullptr;
+ }
+ if (aStartPoint.IsInTextNode()) {
+ return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *aStartPoint.template ContainerAs<Text>(), aCurrentBlock,
+ aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter);
+ }
+ if (!HTMLEditUtils::IsContainerNode(
+ *aStartPoint.template ContainerAs<nsIContent>())) {
+ return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter);
+ }
+
+ nsCOMPtr<nsIContent> nextContent = aStartPoint.GetChild();
+ if (!nextContent) {
+ if (aStartPoint.GetContainer() == &aCurrentBlock) {
+ // We are at end of the block.
+ return nullptr;
+ }
+
+ // We are at end of non-block container
+ return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, IgnoreInsideBlockBoundary(aBlockInlineCheck),
+ aAncestorLimiter);
+ }
+
+ // We have a next node. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*nextContent, aBlockInlineCheck)) {
+ return nextContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aStartPoint.GetContainer()->IsEditable() &&
+ !nextContent->IsEditable()) {
+ return nextContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*nextContent)) {
+ // else if it's a container, get deep leftmost child
+ if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *nextContent, aLeafNodeTypes,
+ IgnoreInsideBlockBoundary(aBlockInlineCheck))) {
+ return child;
+ }
+ }
+ // Else return the node itself
+ return nextContent;
+ }
+
+ /**
+ * GetPreviousLeafContentOrPreviousBlockElement() returns previous leaf
+ * content or previous block element of aStartContent inside
+ * aAncestorLimiter.
+ * Note that the result may be a content outside aCurrentBlock if
+ * aStartContent equals aCurrentBlock.
+ *
+ * @param aStartContent The start content to scan previous content.
+ * @param aCurrentBlock Must be ancestor of aStartContent. Despite
+ * the name, inline content is allowed if
+ * aStartContent is in an inline editing host.
+ * @param aLeafNodeTypes See LeafNodeType.
+ * @param aAncestorLimiter Optional, setting this guarantees the
+ * result is in aAncestorLimiter unless
+ * aStartContent is not a descendant of this.
+ */
+ static nsIContent* GetPreviousLeafContentOrPreviousBlockElement(
+ const nsIContent& aStartContent, const nsIContent& aCurrentBlock,
+ const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ "Not implemented yet");
+
+ if (&aStartContent == aAncestorLimiter) {
+ return nullptr;
+ }
+
+ nsIContent* previousContent = aStartContent.GetPreviousSibling();
+ if (!previousContent) {
+ if (!aStartContent.GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) {
+ if (parentElement == &aCurrentBlock) {
+ return nullptr;
+ }
+ if (parentElement == aAncestorLimiter) {
+ NS_WARNING("Reached editing host while climbing up the DOM tree");
+ return nullptr;
+ }
+ previousContent = parentElement->GetPreviousSibling();
+ if (previousContent) {
+ break;
+ }
+ if (!parentElement->GetParentElement()) {
+ NS_WARNING("Reached orphan node while climbing up the DOM tree");
+ return nullptr;
+ }
+ }
+ MOZ_ASSERT(previousContent);
+ aBlockInlineCheck = IgnoreInsideBlockBoundary(aBlockInlineCheck);
+ }
+
+ // We have a next content. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*previousContent, aBlockInlineCheck)) {
+ return previousContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aStartContent.IsEditable() && !previousContent->IsEditable()) {
+ return previousContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*previousContent)) {
+ // Else if it's a container, get deep rightmost child
+ if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
+ *previousContent, aLeafNodeTypes, aBlockInlineCheck)) {
+ return child;
+ }
+ }
+ // Else return the next content itself.
+ return previousContent;
+ }
+
+ /**
+ * Similar to the above method, but take a DOM point to specify scan start
+ * point.
+ */
+ template <typename PT, typename CT>
+ static nsIContent* GetPreviousLeafContentOrPreviousBlockElement(
+ const EditorDOMPointBase<PT, CT>& aStartPoint,
+ const nsIContent& aCurrentBlock, const LeafNodeTypes& aLeafNodeTypes,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr) {
+ MOZ_ASSERT(aStartPoint.IsSet());
+ MOZ_ASSERT_IF(
+ aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
+ NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
+ "Not implemented yet");
+
+ if (!aStartPoint.IsInContentNode()) {
+ return nullptr;
+ }
+ if (aStartPoint.IsInTextNode()) {
+ return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *aStartPoint.template ContainerAs<Text>(), aCurrentBlock,
+ aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter);
+ }
+ if (!HTMLEditUtils::IsContainerNode(
+ *aStartPoint.template ContainerAs<nsIContent>())) {
+ return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter);
+ }
+
+ if (aStartPoint.IsStartOfContainer()) {
+ if (aStartPoint.GetContainer() == &aCurrentBlock) {
+ // We are at start of the block.
+ return nullptr;
+ }
+
+ // We are at start of non-block container
+ return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, IgnoreInsideBlockBoundary(aBlockInlineCheck),
+ aAncestorLimiter);
+ }
+
+ nsCOMPtr<nsIContent> previousContent =
+ aStartPoint.GetPreviousSiblingOfChild();
+ if (NS_WARN_IF(!previousContent)) {
+ return nullptr;
+ }
+
+ // We have a prior node. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*previousContent, aBlockInlineCheck)) {
+ return previousContent;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
+ aStartPoint.GetContainer()->IsEditable() &&
+ !previousContent->IsEditable()) {
+ return previousContent;
+ }
+ if (HTMLEditUtils::IsContainerNode(*previousContent)) {
+ // Else if it's a container, get deep rightmost child
+ if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
+ *previousContent, aLeafNodeTypes,
+ IgnoreInsideBlockBoundary(aBlockInlineCheck))) {
+ return child;
+ }
+ }
+ // Else return the node itself
+ return previousContent;
+ }
+
+ /**
+ * Returns a content node whose inline styles should be preserved after
+ * deleting content in a range. Typically, you should set aPoint to start
+ * boundary of the range to delete.
+ */
+ template <typename EditorDOMPointType>
+ static nsIContent* GetContentToPreserveInlineStyles(
+ const EditorDOMPointType& aPoint, const Element& aEditingHost);
+
+ /**
+ * Get previous/next editable point from start or end of aContent.
+ */
+ enum class InvisibleWhiteSpaces {
+ Ignore, // Ignore invisible white-spaces, i.e., don't return middle of
+ // them.
+ Preserve, // Preserve invisible white-spaces, i.e., result may be start or
+ // end of a text node even if it begins or ends with invisible
+ // white-spaces.
+ };
+ enum class TableBoundary {
+ Ignore, // May cross any table element boundary.
+ NoCrossTableElement, // Won't cross `<table>` element boundary.
+ NoCrossAnyTableElement, // Won't cross any table element boundary.
+ };
+ template <typename EditorDOMPointType>
+ static EditorDOMPointType GetPreviousEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary);
+ template <typename EditorDOMPointType>
+ static EditorDOMPointType GetNextEditablePoint(
+ nsIContent& aContent, const Element* aAncestorLimiter,
+ InvisibleWhiteSpaces aInvisibleWhiteSpaces,
+ TableBoundary aHowToTreatTableBoundary);
+
+ /**
+ * GetAncestorElement() and GetInclusiveAncestorElement() return
+ * (inclusive) block ancestor element of aContent whose time matches
+ * aAncestorTypes.
+ */
+ enum class AncestorType {
+ ClosestBlockElement,
+ MostDistantInlineElementInBlock,
+ EditableElement,
+ IgnoreHRElement, // Ignore ancestor <hr> element since invalid structure
+ ButtonElement,
+ };
+ using AncestorTypes = EnumSet<AncestorType>;
+ constexpr static AncestorTypes
+ ClosestEditableBlockElementOrInlineEditingHost = {
+ AncestorType::ClosestBlockElement,
+ AncestorType::MostDistantInlineElementInBlock,
+ AncestorType::EditableElement};
+ constexpr static AncestorTypes ClosestBlockElement = {
+ AncestorType::ClosestBlockElement};
+ constexpr static AncestorTypes ClosestEditableBlockElement = {
+ AncestorType::ClosestBlockElement, AncestorType::EditableElement};
+ constexpr static AncestorTypes ClosestEditableBlockElementExceptHRElement = {
+ AncestorType::ClosestBlockElement, AncestorType::IgnoreHRElement,
+ AncestorType::EditableElement};
+ constexpr static AncestorTypes ClosestEditableBlockElementOrButtonElement = {
+ AncestorType::ClosestBlockElement, AncestorType::EditableElement,
+ AncestorType::ButtonElement};
+ static Element* GetAncestorElement(const nsIContent& aContent,
+ const AncestorTypes& aAncestorTypes,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr);
+ static Element* GetInclusiveAncestorElement(
+ const nsIContent& aContent, const AncestorTypes& aAncestorTypes,
+ BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr);
+
+ /**
+ * GetClosestAncestorTableElement() returns the nearest inclusive ancestor
+ * <table> element of aContent.
+ */
+ static Element* GetClosestAncestorTableElement(const nsIContent& aContent) {
+ // TODO: the method name and its documentation clash with the
+ // implementation. Split this method into
+ // `GetClosestAncestorTableElement` and
+ // `GetClosestInclusiveAncestorTableElement`.
+ if (!aContent.GetParent()) {
+ return nullptr;
+ }
+ for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsTable(element)) {
+ return element;
+ }
+ }
+ return nullptr;
+ }
+
+ static Element* GetInclusiveAncestorAnyTableElement(
+ const nsIContent& aContent) {
+ for (Element* parent : aContent.InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsAnyTableElement(parent)) {
+ return parent;
+ }
+ }
+ return nullptr;
+ }
+
+ [[nodiscard]] static Element* GetClosestAncestorAnyListElement(
+ const nsIContent& aContent);
+ [[nodiscard]] static Element* GetClosestInclusiveAncestorAnyListElement(
+ const nsIContent& aContent);
+
+ /**
+ * GetClosestAncestorListItemElement() returns a list item element if
+ * aContent or its ancestor in editing host is one. However, this won't
+ * cross table related element.
+ */
+ static Element* GetClosestAncestorListItemElement(
+ const nsIContent& aContent, const Element* aAncestorLimit = nullptr) {
+ MOZ_ASSERT_IF(aAncestorLimit,
+ aContent.IsInclusiveDescendantOf(aAncestorLimit));
+
+ if (HTMLEditUtils::IsListItem(&aContent)) {
+ return const_cast<Element*>(aContent.AsElement());
+ }
+
+ for (Element* parentElement : aContent.AncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsAnyTableElement(parentElement)) {
+ return nullptr;
+ }
+ if (HTMLEditUtils::IsListItem(parentElement)) {
+ return parentElement;
+ }
+ if (parentElement == aAncestorLimit) {
+ return nullptr;
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetRangeSelectingAllContentInAllListItems() returns a range which selects
+ * from start of the first list item to end of the last list item of
+ * aListElement. Note that the result may be in different list element if
+ * aListElement has child list element(s) directly.
+ */
+ template <typename EditorDOMRangeType>
+ static EditorDOMRangeType GetRangeSelectingAllContentInAllListItems(
+ const Element& aListElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ Element* firstListItem =
+ HTMLEditUtils::GetFirstListItemElement(aListElement);
+ Element* lastListItem = HTMLEditUtils::GetLastListItemElement(aListElement);
+ MOZ_ASSERT_IF(firstListItem, lastListItem);
+ MOZ_ASSERT_IF(!firstListItem, !lastListItem);
+ if (!firstListItem || !lastListItem) {
+ return EditorDOMRangeType();
+ }
+ return EditorDOMRangeType(
+ typename EditorDOMRangeType::PointType(firstListItem, 0u),
+ EditorDOMRangeType::PointType::AtEndOf(*lastListItem));
+ }
+
+ /**
+ * GetFirstListItemElement() returns the first list item element in the
+ * pre-order tree traversal of the DOM.
+ */
+ static Element* GetFirstListItemElement(const Element& aListElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ for (nsIContent* maybeFirstListItem = aListElement.GetFirstChild();
+ maybeFirstListItem;
+ maybeFirstListItem = maybeFirstListItem->GetNextNode(&aListElement)) {
+ if (HTMLEditUtils::IsListItem(maybeFirstListItem)) {
+ return maybeFirstListItem->AsElement();
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetLastListItemElement() returns the last list item element in the
+ * post-order tree traversal of the DOM. I.e., returns the last list
+ * element whose close tag appears at last.
+ */
+ static Element* GetLastListItemElement(const Element& aListElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ for (nsIContent* maybeLastListItem = aListElement.GetLastChild();
+ maybeLastListItem;) {
+ if (HTMLEditUtils::IsListItem(maybeLastListItem)) {
+ return maybeLastListItem->AsElement();
+ }
+ if (maybeLastListItem->HasChildren()) {
+ maybeLastListItem = maybeLastListItem->GetLastChild();
+ continue;
+ }
+ if (maybeLastListItem->GetPreviousSibling()) {
+ maybeLastListItem = maybeLastListItem->GetPreviousSibling();
+ continue;
+ }
+ for (Element* parent = maybeLastListItem->GetParentElement(); parent;
+ parent = parent->GetParentElement()) {
+ maybeLastListItem = nullptr;
+ if (parent == &aListElement) {
+ return nullptr;
+ }
+ if (parent->GetPreviousSibling()) {
+ maybeLastListItem = parent->GetPreviousSibling();
+ break;
+ }
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetFirstTableCellElementChild() and GetLastTableCellElementChild()
+ * return the first/last element child of <tr> element if it's a table
+ * cell element.
+ */
+ static Element* GetFirstTableCellElementChild(
+ const Element& aTableRowElement) {
+ MOZ_ASSERT(aTableRowElement.IsHTMLElement(nsGkAtoms::tr));
+ Element* firstElementChild = aTableRowElement.GetFirstElementChild();
+ return firstElementChild && HTMLEditUtils::IsTableCell(firstElementChild)
+ ? firstElementChild
+ : nullptr;
+ }
+ static Element* GetLastTableCellElementChild(
+ const Element& aTableRowElement) {
+ MOZ_ASSERT(aTableRowElement.IsHTMLElement(nsGkAtoms::tr));
+ Element* lastElementChild = aTableRowElement.GetLastElementChild();
+ return lastElementChild && HTMLEditUtils::IsTableCell(lastElementChild)
+ ? lastElementChild
+ : nullptr;
+ }
+
+ /**
+ * GetPreviousTableCellElementSibling() and GetNextTableCellElementSibling()
+ * return a table cell element of previous/next element sibling of given
+ * content node if and only if the element sibling is a table cell element.
+ */
+ static Element* GetPreviousTableCellElementSibling(
+ const nsIContent& aChildOfTableRow) {
+ MOZ_ASSERT(aChildOfTableRow.GetParentNode());
+ MOZ_ASSERT(aChildOfTableRow.GetParentNode()->IsHTMLElement(nsGkAtoms::tr));
+ Element* previousElementSibling =
+ aChildOfTableRow.GetPreviousElementSibling();
+ return previousElementSibling &&
+ HTMLEditUtils::IsTableCell(previousElementSibling)
+ ? previousElementSibling
+ : nullptr;
+ }
+ static Element* GetNextTableCellElementSibling(
+ const nsIContent& aChildOfTableRow) {
+ MOZ_ASSERT(aChildOfTableRow.GetParentNode());
+ MOZ_ASSERT(aChildOfTableRow.GetParentNode()->IsHTMLElement(nsGkAtoms::tr));
+ Element* nextElementSibling = aChildOfTableRow.GetNextElementSibling();
+ return nextElementSibling && HTMLEditUtils::IsTableCell(nextElementSibling)
+ ? nextElementSibling
+ : nullptr;
+ }
+
+ /**
+ * GetMostDistantAncestorInlineElement() returns the most distant ancestor
+ * inline element between aContent and the aEditingHost. Even if aEditingHost
+ * is an inline element, this method never returns aEditingHost as the result.
+ * Optionally, you can specify ancestor limiter content node. This guarantees
+ * that the result is a descendant of aAncestorLimiter if aContent is a
+ * descendant of aAncestorLimiter.
+ */
+ static nsIContent* GetMostDistantAncestorInlineElement(
+ const nsIContent& aContent, BlockInlineCheck aBlockInlineCheck,
+ const Element* aEditingHost = nullptr,
+ const nsIContent* aAncestorLimiter = nullptr) {
+ if (HTMLEditUtils::IsBlockElement(aContent, aBlockInlineCheck)) {
+ return nullptr;
+ }
+
+ // If aNode is the editing host itself, there is no modifiable inline
+ // parent.
+ if (&aContent == aEditingHost || &aContent == aAncestorLimiter) {
+ return nullptr;
+ }
+
+ // If aNode is outside of the <body> element, we don't support to edit
+ // such elements for now.
+ // XXX This should be MOZ_ASSERT after fixing bug 1413131 for avoiding
+ // calling this expensive method.
+ if (aEditingHost && !aContent.IsInclusiveDescendantOf(aEditingHost)) {
+ return nullptr;
+ }
+
+ if (!aContent.GetParent()) {
+ return const_cast<nsIContent*>(&aContent);
+ }
+
+ // Looks for the highest inline parent in the editing host.
+ nsIContent* topMostInlineContent = const_cast<nsIContent*>(&aContent);
+ for (Element* element : aContent.AncestorsOfType<Element>()) {
+ if (element == aEditingHost || element == aAncestorLimiter ||
+ HTMLEditUtils::IsBlockElement(*element, aBlockInlineCheck)) {
+ break;
+ }
+ topMostInlineContent = element;
+ }
+ return topMostInlineContent;
+ }
+
+ /**
+ * GetMostDistantAncestorEditableEmptyInlineElement() returns most distant
+ * ancestor which only has aEmptyContent or its ancestor, editable and
+ * inline element.
+ */
+ static Element* GetMostDistantAncestorEditableEmptyInlineElement(
+ const nsIContent& aEmptyContent, BlockInlineCheck aBlockInlineCheck,
+ const Element* aEditingHost = nullptr,
+ const nsIContent* aAncestorLimiter = nullptr) {
+ if (&aEmptyContent == aEditingHost || &aEmptyContent == aAncestorLimiter) {
+ return nullptr;
+ }
+ nsIContent* lastEmptyContent = const_cast<nsIContent*>(&aEmptyContent);
+ for (Element* element : aEmptyContent.AncestorsOfType<Element>()) {
+ if (element == aEditingHost || element == aAncestorLimiter) {
+ break;
+ }
+ if (!HTMLEditUtils::IsInlineContent(*element, aBlockInlineCheck) ||
+ !HTMLEditUtils::IsSimplyEditableNode(*element)) {
+ break;
+ }
+ if (element->GetChildCount() > 1) {
+ for (const nsIContent* child = element->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child == lastEmptyContent || child->IsComment()) {
+ continue;
+ }
+ return lastEmptyContent != &aEmptyContent
+ ? lastEmptyContent->AsElement()
+ : nullptr;
+ }
+ }
+ lastEmptyContent = element;
+ }
+ return lastEmptyContent != &aEmptyContent ? lastEmptyContent->AsElement()
+ : nullptr;
+ }
+
+ /**
+ * GetElementIfOnlyOneSelected() returns an element if aRange selects only
+ * the element node (and its descendants).
+ */
+ static Element* GetElementIfOnlyOneSelected(const AbstractRange& aRange) {
+ return GetElementIfOnlyOneSelected(EditorRawDOMRange(aRange));
+ }
+ template <typename EditorDOMPointType>
+ static Element* GetElementIfOnlyOneSelected(
+ const EditorDOMRangeBase<EditorDOMPointType>& aRange) {
+ if (!aRange.IsPositioned() || aRange.Collapsed()) {
+ return nullptr;
+ }
+ const auto& start = aRange.StartRef();
+ const auto& end = aRange.EndRef();
+ if (NS_WARN_IF(!start.IsSetAndValid()) ||
+ NS_WARN_IF(!end.IsSetAndValid()) ||
+ start.GetContainer() != end.GetContainer()) {
+ return nullptr;
+ }
+ nsIContent* childAtStart = start.GetChild();
+ if (!childAtStart || !childAtStart->IsElement()) {
+ return nullptr;
+ }
+ // If start child is not the last sibling and only if end child is its
+ // next sibling, the start child is selected.
+ if (childAtStart->GetNextSibling()) {
+ return childAtStart->GetNextSibling() == end.GetChild()
+ ? childAtStart->AsElement()
+ : nullptr;
+ }
+ // If start child is the last sibling and only if no child at the end,
+ // the start child is selected.
+ return !end.GetChild() ? childAtStart->AsElement() : nullptr;
+ }
+
+ static Element* GetTableCellElementIfOnlyOneSelected(
+ const AbstractRange& aRange) {
+ Element* element = HTMLEditUtils::GetElementIfOnlyOneSelected(aRange);
+ return element && HTMLEditUtils::IsTableCell(element) ? element : nullptr;
+ }
+
+ /**
+ * GetFirstSelectedTableCellElement() returns a table cell element (i.e.,
+ * `<td>` or `<th>` if and only if first selection range selects only a
+ * table cell element.
+ */
+ static Element* GetFirstSelectedTableCellElement(
+ const Selection& aSelection) {
+ if (!aSelection.RangeCount()) {
+ return nullptr;
+ }
+ const nsRange* firstRange = aSelection.GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange) || NS_WARN_IF(!firstRange->IsPositioned())) {
+ return nullptr;
+ }
+ return GetTableCellElementIfOnlyOneSelected(*firstRange);
+ }
+
+ /**
+ * GetInclusiveFirstChildWhichHasOneChild() returns the deepest element whose
+ * tag name is one of `aFirstElementName` and `aOtherElementNames...` if and
+ * only if the elements have only one child node. In other words, when
+ * this method meets an element which does not matches any of the tag name
+ * or it has no children or 2+ children.
+ *
+ * XXX This method must be implemented without treating edge cases. So, the
+ * behavior is odd. E.g., why can we ignore non-editable node at counting
+ * each children? Why do we dig non-editable aNode or first child of its
+ * descendants?
+ */
+ template <typename FirstElementName, typename... OtherElementNames>
+ static Element* GetInclusiveDeepestFirstChildWhichHasOneChild(
+ const nsINode& aNode, const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck, FirstElementName aFirstElementName,
+ OtherElementNames... aOtherElementNames) {
+ if (!aNode.IsElement()) {
+ return nullptr;
+ }
+ Element* parentElement = nullptr;
+ for (nsIContent* content = const_cast<nsIContent*>(aNode.AsContent());
+ content && content->IsElement() &&
+ content->IsAnyOfHTMLElements(aFirstElementName, aOtherElementNames...);
+ // XXX Why do we scan only the first child of every element? If it's
+ // not editable, why do we ignore it when aOptions specifies so.
+ content = content->GetFirstChild()) {
+ if (HTMLEditUtils::CountChildren(*content, aOptions, aBlockInlineCheck) !=
+ 1) {
+ return content->AsElement();
+ }
+ parentElement = content->AsElement();
+ }
+ return parentElement;
+ }
+
+ /**
+ * Get the first <br> element in aElement. This scans only leaf nodes so
+ * if a <br> element has children illegally, it'll be ignored.
+ *
+ * @param aElement The element which may have a <br> element.
+ * @return First <br> element node in aElement if there is.
+ */
+ static dom::HTMLBRElement* GetFirstBRElement(const dom::Element& aElement) {
+ for (nsIContent* content = HTMLEditUtils::GetFirstLeafContent(
+ aElement, {LeafNodeType::OnlyLeafNode});
+ content; content = HTMLEditUtils::GetNextContent(
+ *content,
+ {WalkTreeOption::IgnoreDataNodeExceptText,
+ WalkTreeOption::IgnoreWhiteSpaceOnlyText},
+ BlockInlineCheck::Unused, &aElement)) {
+ if (auto* brElement = dom::HTMLBRElement::FromNode(*content)) {
+ return brElement;
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * Return last <br> element or last text node ending with a preserved line
+ * break of/before aBlockElement.
+ */
+ enum ScanLineBreak {
+ AtEndOfBlock,
+ BeforeBlock,
+ };
+ static nsIContent* GetUnnecessaryLineBreakContent(
+ const Element& aBlockElement, ScanLineBreak aScanLineBreak);
+
+ /**
+ * IsInTableCellSelectionMode() returns true when Gecko's editor thinks that
+ * selection is in a table cell selection mode.
+ * Note that Gecko's editor traditionally treats selection as in table cell
+ * selection mode when first range selects a table cell element. I.e., even
+ * if `nsFrameSelection` is not in table cell selection mode, this may return
+ * true.
+ */
+ static bool IsInTableCellSelectionMode(const Selection& aSelection) {
+ return GetFirstSelectedTableCellElement(aSelection) != nullptr;
+ }
+
+ static EditAction GetEditActionForInsert(const nsAtom& aTagName);
+ static EditAction GetEditActionForRemoveList(const nsAtom& aTagName);
+ static EditAction GetEditActionForInsert(const Element& aElement);
+ static EditAction GetEditActionForFormatText(const nsAtom& aProperty,
+ const nsAtom* aAttribute,
+ bool aToSetStyle);
+ static EditAction GetEditActionForAlignment(const nsAString& aAlignType);
+
+ /**
+ * GetPreviousNonCollapsibleCharOffset() returns offset of previous
+ * character which is not collapsible white-space characters.
+ */
+ enum class WalkTextOption {
+ TreatNBSPsCollapsible,
+ };
+ using WalkTextOptions = EnumSet<WalkTextOption>;
+ static Maybe<uint32_t> GetPreviousNonCollapsibleCharOffset(
+ const EditorDOMPointInText& aPoint,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ return GetPreviousNonCollapsibleCharOffset(
+ *aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions);
+ }
+ static Maybe<uint32_t> GetPreviousNonCollapsibleCharOffset(
+ const Text& aTextNode, uint32_t aOffset,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ const bool isWhiteSpaceCollapsible =
+ !EditorUtils::IsWhiteSpacePreformatted(aTextNode);
+ const bool isNewLineCollapsible =
+ !EditorUtils::IsNewLinePreformatted(aTextNode);
+ const bool isNBSPCollapsible =
+ isWhiteSpaceCollapsible &&
+ aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible);
+ const nsTextFragment& textFragment = aTextNode.TextFragment();
+ MOZ_ASSERT(aOffset <= textFragment.GetLength());
+ for (uint32_t i = aOffset; i; i--) {
+ // TODO: Perhaps, nsTextFragment should have scanner methods because
+ // the text may be in per-one-byte storage or per-two-byte storage,
+ // and `CharAt` needs to check it everytime.
+ switch (textFragment.CharAt(i - 1)) {
+ case HTMLEditUtils::kSpace:
+ case HTMLEditUtils::kCarriageReturn:
+ case HTMLEditUtils::kTab:
+ if (!isWhiteSpaceCollapsible) {
+ return Some(i - 1);
+ }
+ break;
+ case HTMLEditUtils::kNewLine:
+ if (!isNewLineCollapsible) {
+ return Some(i - 1);
+ }
+ break;
+ case HTMLEditUtils::kNBSP:
+ if (!isNBSPCollapsible) {
+ return Some(i - 1);
+ }
+ break;
+ default:
+ MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i - 1)));
+ return Some(i - 1);
+ }
+ }
+ return Nothing();
+ }
+
+ /**
+ * GetNextNonCollapsibleCharOffset() returns offset of next character which is
+ * not collapsible white-space characters.
+ */
+ static Maybe<uint32_t> GetNextNonCollapsibleCharOffset(
+ const EditorDOMPointInText& aPoint,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ return GetNextNonCollapsibleCharOffset(*aPoint.ContainerAs<Text>(),
+ aPoint.Offset(), aWalkTextOptions);
+ }
+ static Maybe<uint32_t> GetNextNonCollapsibleCharOffset(
+ const Text& aTextNode, uint32_t aOffset,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ return GetInclusiveNextNonCollapsibleCharOffset(aTextNode, aOffset + 1,
+ aWalkTextOptions);
+ }
+
+ /**
+ * GetInclusiveNextNonCollapsibleCharOffset() returns offset of inclusive next
+ * character which is not collapsible white-space characters.
+ */
+ static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
+ const EditorDOMPointInText& aPoint,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ return GetInclusiveNextNonCollapsibleCharOffset(
+ *aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions);
+ }
+ static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
+ const Text& aTextNode, uint32_t aOffset,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ const bool isWhiteSpaceCollapsible =
+ !EditorUtils::IsWhiteSpacePreformatted(aTextNode);
+ const bool isNewLineCollapsible =
+ !EditorUtils::IsNewLinePreformatted(aTextNode);
+ const bool isNBSPCollapsible =
+ isWhiteSpaceCollapsible &&
+ aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible);
+ const nsTextFragment& textFragment = aTextNode.TextFragment();
+ MOZ_ASSERT(aOffset <= textFragment.GetLength());
+ for (uint32_t i = aOffset; i < textFragment.GetLength(); i++) {
+ // TODO: Perhaps, nsTextFragment should have scanner methods because
+ // the text may be in per-one-byte storage or per-two-byte storage,
+ // and `CharAt` needs to check it everytime.
+ switch (textFragment.CharAt(i)) {
+ case HTMLEditUtils::kSpace:
+ case HTMLEditUtils::kCarriageReturn:
+ case HTMLEditUtils::kTab:
+ if (!isWhiteSpaceCollapsible) {
+ return Some(i);
+ }
+ break;
+ case HTMLEditUtils::kNewLine:
+ if (!isNewLineCollapsible) {
+ return Some(i);
+ }
+ break;
+ case HTMLEditUtils::kNBSP:
+ if (!isNBSPCollapsible) {
+ return Some(i);
+ }
+ break;
+ default:
+ MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i)));
+ return Some(i);
+ }
+ }
+ return Nothing();
+ }
+
+ /**
+ * GetFirstWhiteSpaceOffsetCollapsedWith() returns first collapsible
+ * white-space offset which is collapsed with a white-space at the given
+ * position. I.e., the character at the position must be a collapsible
+ * white-space.
+ */
+ static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
+ const EditorDOMPointInText& aPoint,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ MOZ_ASSERT(!aPoint.IsEndOfContainer());
+ MOZ_ASSERT_IF(
+ aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
+ aPoint.IsCharCollapsibleASCIISpaceOrNBSP());
+ MOZ_ASSERT_IF(
+ !aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
+ aPoint.IsCharCollapsibleASCIISpace());
+ return GetFirstWhiteSpaceOffsetCollapsedWith(
+ *aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions);
+ }
+ static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
+ const Text& aTextNode, uint32_t aOffset,
+ const WalkTextOptions& aWalkTextOptions = {}) {
+ MOZ_ASSERT(aOffset < aTextNode.TextLength());
+ MOZ_ASSERT_IF(
+ aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
+ EditorRawDOMPoint(&aTextNode, aOffset)
+ .IsCharCollapsibleASCIISpaceOrNBSP());
+ MOZ_ASSERT_IF(
+ !aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
+ EditorRawDOMPoint(&aTextNode, aOffset).IsCharCollapsibleASCIISpace());
+ if (!aOffset) {
+ return 0;
+ }
+ Maybe<uint32_t> previousVisibleCharOffset =
+ GetPreviousNonCollapsibleCharOffset(aTextNode, aOffset,
+ aWalkTextOptions);
+ return previousVisibleCharOffset.isSome()
+ ? previousVisibleCharOffset.value() + 1
+ : 0;
+ }
+
+ /**
+ * GetPreviousPreformattedNewLineInTextNode() returns a point which points
+ * previous preformatted linefeed if there is and aPoint is in a text node.
+ * If the node's linefeed characters are not preformatted or aPoint is not
+ * in a text node, this returns unset DOM point.
+ */
+ template <typename EditorDOMPointType, typename ArgEditorDOMPointType>
+ static EditorDOMPointType GetPreviousPreformattedNewLineInTextNode(
+ const ArgEditorDOMPointType& aPoint) {
+ if (!aPoint.IsInTextNode() || aPoint.IsStartOfContainer() ||
+ !EditorUtils::IsNewLinePreformatted(
+ *aPoint.template ContainerAs<Text>())) {
+ return EditorDOMPointType();
+ }
+ Text* textNode = aPoint.template ContainerAs<Text>();
+ const nsTextFragment& textFragment = textNode->TextFragment();
+ MOZ_ASSERT(aPoint.Offset() <= textFragment.GetLength());
+ for (uint32_t offset = aPoint.Offset(); offset; --offset) {
+ if (textFragment.CharAt(offset - 1) == HTMLEditUtils::kNewLine) {
+ return EditorDOMPointType(textNode, offset - 1);
+ }
+ }
+ return EditorDOMPointType();
+ }
+
+ /**
+ * GetInclusiveNextPreformattedNewLineInTextNode() returns a point which
+ * points inclusive next preformatted linefeed if there is and aPoint is in a
+ * text node. If the node's linefeed characters are not preformatted or aPoint
+ * is not in a text node, this returns unset DOM point.
+ */
+ template <typename EditorDOMPointType, typename ArgEditorDOMPointType>
+ static EditorDOMPointType GetInclusiveNextPreformattedNewLineInTextNode(
+ const ArgEditorDOMPointType& aPoint) {
+ if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() ||
+ !EditorUtils::IsNewLinePreformatted(
+ *aPoint.template ContainerAs<Text>())) {
+ return EditorDOMPointType();
+ }
+ Text* textNode = aPoint.template ContainerAs<Text>();
+ const nsTextFragment& textFragment = textNode->TextFragment();
+ for (uint32_t offset = aPoint.Offset(); offset < textFragment.GetLength();
+ ++offset) {
+ if (textFragment.CharAt(offset) == HTMLEditUtils::kNewLine) {
+ return EditorDOMPointType(textNode, offset);
+ }
+ }
+ return EditorDOMPointType();
+ }
+
+ /**
+ * GetGoodCaretPointFor() returns a good point to collapse `Selection`
+ * after handling edit action with aDirectionAndAmount.
+ *
+ * @param aContent The content where you want to put caret
+ * around.
+ * @param aDirectionAndAmount Muse be one of eNext, eNextWord, eToEndOfLine,
+ * ePrevious, ePreviousWord and eToBeggingOfLine.
+ * Set the direction of handled edit action.
+ */
+ template <typename EditorDOMPointType>
+ static EditorDOMPointType GetGoodCaretPointFor(
+ nsIContent& aContent, nsIEditor::EDirection aDirectionAndAmount) {
+ MOZ_ASSERT(nsIEditor::EDirectionIsValidExceptNone(aDirectionAndAmount));
+
+ // XXX Why don't we check whether the candidate position is enable or not?
+ // When the result is not editable point, caret will be enclosed in
+ // the non-editable content.
+
+ // If we can put caret in aContent, return start or end in it.
+ if (aContent.IsText() || HTMLEditUtils::IsContainerNode(aContent) ||
+ NS_WARN_IF(!aContent.GetParentNode())) {
+ return EditorDOMPointType(
+ &aContent, nsIEditor::DirectionIsDelete(aDirectionAndAmount)
+ ? 0
+ : aContent.Length());
+ }
+
+ // If we are going forward, put caret at aContent itself.
+ if (nsIEditor::DirectionIsDelete(aDirectionAndAmount)) {
+ return EditorDOMPointType(&aContent);
+ }
+
+ // If we are going backward, put caret to next node unless aContent is an
+ // invisible `<br>` element.
+ // XXX Shouldn't we put caret to first leaf of the next node?
+ if (!HTMLEditUtils::IsInvisibleBRElement(aContent)) {
+ EditorDOMPointType ret(EditorDOMPointType::After(aContent));
+ NS_WARNING_ASSERTION(ret.IsSet(), "Failed to set after aContent");
+ return ret;
+ }
+
+ // Otherwise, we should put caret at the invisible `<br>` element.
+ return EditorDOMPointType(&aContent);
+ }
+
+ /**
+ * GetBetterInsertionPointFor() returns better insertion point to insert
+ * aContentToInsert.
+ *
+ * @param aContentToInsert The content to insert.
+ * @param aPointToInsert A candidate point to insert the node.
+ * @param aEditingHost The editing host containing aPointToInsert.
+ * @return Better insertion point if next visible node
+ * is a <br> element and previous visible node
+ * is neither none, another <br> element nor
+ * different block level element.
+ */
+ template <typename EditorDOMPointType, typename EditorDOMPointTypeInput>
+ static EditorDOMPointType GetBetterInsertionPointFor(
+ const nsIContent& aContentToInsert,
+ const EditorDOMPointTypeInput& aPointToInsert,
+ const Element& aEditingHost);
+
+ /**
+ * GetBetterCaretPositionToInsertText() returns better point to put caret
+ * if aPoint is near a text node or in non-container node.
+ */
+ template <typename EditorDOMPointType, typename EditorDOMPointTypeInput>
+ static EditorDOMPointType GetBetterCaretPositionToInsertText(
+ const EditorDOMPointTypeInput& aPoint, const Element& aEditingHost);
+
+ /**
+ * ComputePointToPutCaretInElementIfOutside() returns a good point in aElement
+ * to put caret if aCurrentPoint is outside of aElement.
+ *
+ * @param aElement The result is a point in aElement.
+ * @param aCurrentPoint The current (candidate) caret point. Only if this
+ * is outside aElement, returns a point in aElement.
+ */
+ template <typename EditorDOMPointType, typename EditorDOMPointTypeInput>
+ static Result<EditorDOMPointType, nsresult>
+ ComputePointToPutCaretInElementIfOutside(
+ const Element& aElement, const EditorDOMPointTypeInput& aCurrentPoint);
+
+ /**
+ * Content-based query returns true if
+ * <mHTMLProperty mAttribute=mAttributeValue> effects aContent. If there is
+ * such a element, but another element whose attribute value does not match
+ * with mAttributeValue is closer ancestor of aContent, then the distant
+ * ancestor does not effect aContent.
+ *
+ * @param aContent The target of the query
+ * @param aStyle The style which queries a representing element.
+ * @param aValue Optional, the value of aStyle.mAttribute, example: blue
+ * in <font color="blue"> May be null. Ignored if
+ * aStyle.mAttribute is null.
+ * @param aOutValue [OUT] the value of the attribute, if returns true
+ * @return true if <mHTMLProperty mAttribute=mAttributeValue>
+ * effects aContent.
+ */
+ [[nodiscard]] static bool IsInlineStyleSetByElement(
+ const nsIContent& aContent, const EditorInlineStyle& aStyle,
+ const nsAString* aValue, nsAString* aOutValue = nullptr);
+
+ /**
+ * CollectAllChildren() collects all child nodes of aParentNode.
+ */
+ static void CollectAllChildren(
+ const nsINode& aParentNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) {
+ MOZ_ASSERT(aOutArrayOfContents.IsEmpty());
+ aOutArrayOfContents.SetCapacity(aParentNode.GetChildCount());
+ for (nsIContent* childContent = aParentNode.GetFirstChild(); childContent;
+ childContent = childContent->GetNextSibling()) {
+ aOutArrayOfContents.AppendElement(*childContent);
+ }
+ }
+
+ /**
+ * CollectChildren() collects child nodes of aNode (starting from
+ * first editable child, but may return non-editable children after it).
+ *
+ * @param aNode Parent node of retrieving children.
+ * @param aOutArrayOfContents [out] This method will inserts found children
+ * into this array.
+ * @param aIndexToInsertChildren Starting from this index, found
+ * children will be inserted to the array.
+ * @param aOptions Options to scan the children.
+ * @return Number of found children.
+ */
+ static size_t CollectChildren(
+ const nsINode& aNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ const CollectChildrenOptions& aOptions) {
+ return HTMLEditUtils::CollectChildren(aNode, aOutArrayOfContents, 0u,
+ aOptions);
+ }
+ static size_t CollectChildren(
+ const nsINode& aNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ size_t aIndexToInsertChildren, const CollectChildrenOptions& aOptions);
+
+ /**
+ * CollectEmptyInlineContainerDescendants() appends empty inline elements in
+ * aNode to aOutArrayOfContents. Although it's array of nsIContent, the
+ * instance will be elements.
+ *
+ * @param aNode The node whose descendants may have empty inline
+ * elements.
+ * @param aOutArrayOfContents [out] This method will append found descendants
+ * into this array.
+ * @param aOptions The option which element should be treated as
+ * empty.
+ * @param aBlockInlineCheck Whether use computed style or HTML default style
+ * when consider block vs. inline.
+ * @return Number of found elements.
+ */
+ static size_t CollectEmptyInlineContainerDescendants(
+ const nsINode& aNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ const EmptyCheckOptions& aOptions, BlockInlineCheck aBlockInlineCheck);
+
+ /**
+ * Check whether aElement has attributes except the name aAttribute and
+ * "_moz_*" attributes.
+ */
+ [[nodiscard]] static bool ElementHasAttribute(const Element& aElement) {
+ return ElementHasAttributeExcept(aElement, *nsGkAtoms::_empty,
+ *nsGkAtoms::empty, *nsGkAtoms::_empty);
+ }
+ [[nodiscard]] static bool ElementHasAttributeExcept(
+ const Element& aElement, const nsAtom& aAttribute) {
+ return ElementHasAttributeExcept(aElement, aAttribute, *nsGkAtoms::_empty,
+ *nsGkAtoms::empty);
+ }
+ [[nodiscard]] static bool ElementHasAttributeExcept(
+ const Element& aElement, const nsAtom& aAttribute1,
+ const nsAtom& aAttribute2) {
+ return ElementHasAttributeExcept(aElement, aAttribute1, aAttribute2,
+ *nsGkAtoms::empty);
+ }
+ [[nodiscard]] static bool ElementHasAttributeExcept(
+ const Element& aElement, const nsAtom& aAttribute1,
+ const nsAtom& aAttribute2, const nsAtom& aAttribute3);
+
+ /**
+ * Returns EditorDOMPoint which points deepest editable start/end point of
+ * aNode. If a node is a container node and first/last child is editable,
+ * returns the child's start or last point recursively.
+ */
+ enum class InvisibleText { Recognize, Skip };
+ template <typename EditorDOMPointType>
+ [[nodiscard]] static EditorDOMPointType GetDeepestEditableStartPointOf(
+ const nsIContent& aContent,
+ InvisibleText aInvisibleText = InvisibleText::Recognize) {
+ if (NS_WARN_IF(!EditorUtils::IsEditableContent(
+ aContent, EditorBase::EditorType::HTML))) {
+ return EditorDOMPointType();
+ }
+ EditorDOMPointType result(&aContent, 0u);
+ while (true) {
+ nsIContent* firstChild = result.GetContainer()->GetFirstChild();
+ if (!firstChild) {
+ break;
+ }
+ // If the caller wants to skip invisible white-spaces, we should skip
+ // invisible text nodes.
+ if (aInvisibleText == InvisibleText::Skip && firstChild->IsText() &&
+ EditorUtils::IsEditableContent(*firstChild,
+ EditorBase::EditorType::HTML) &&
+ !HTMLEditUtils::IsVisibleTextNode(*firstChild->AsText())) {
+ for (nsIContent* nextSibling = firstChild->GetNextSibling();
+ nextSibling; nextSibling = nextSibling->GetNextSibling()) {
+ if (!nextSibling->IsText() ||
+ // We know its previous sibling is very start of a block.
+ // Therefore, we only need to scan the text here.
+ HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
+ *firstChild->AsText(), 0u)
+ .isSome()) {
+ firstChild = nextSibling;
+ break;
+ }
+ }
+ }
+ if ((!firstChild->IsText() &&
+ !HTMLEditUtils::IsContainerNode(*firstChild)) ||
+ !EditorUtils::IsEditableContent(*firstChild,
+ EditorBase::EditorType::HTML)) {
+ break;
+ }
+ if (aInvisibleText == InvisibleText::Skip && firstChild->IsText()) {
+ result.Set(firstChild,
+ HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
+ *firstChild->AsText(), 0u)
+ .valueOr(0u));
+ break;
+ }
+ result.Set(firstChild, 0u);
+ }
+ return result;
+ }
+ template <typename EditorDOMPointType>
+ [[nodiscard]] static EditorDOMPointType GetDeepestEditableEndPointOf(
+ const nsIContent& aContent,
+ InvisibleText aInvisibleText = InvisibleText::Recognize) {
+ if (NS_WARN_IF(!EditorUtils::IsEditableContent(
+ aContent, EditorBase::EditorType::HTML))) {
+ return EditorDOMPointType();
+ }
+ auto result = EditorDOMPointType::AtEndOf(aContent);
+ while (true) {
+ nsIContent* lastChild = result.GetContainer()->GetLastChild();
+ if (!lastChild) {
+ break;
+ }
+ // If the caller wants to skip invisible white-spaces, we should skip
+ // invisible text nodes.
+ if (aInvisibleText == InvisibleText::Skip && lastChild->IsText() &&
+ EditorUtils::IsEditableContent(*lastChild,
+ EditorBase::EditorType::HTML) &&
+ !HTMLEditUtils::IsVisibleTextNode(*lastChild->AsText())) {
+ for (nsIContent* nextSibling = lastChild->GetPreviousSibling();
+ nextSibling; nextSibling = nextSibling->GetPreviousSibling()) {
+ if (!nextSibling->IsText() ||
+ // We know its previous sibling is very start of a block.
+ // Therefore, we only need to scan the text here.
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
+ *lastChild->AsText(), lastChild->AsText()->TextDataLength())
+ .isSome()) {
+ lastChild = nextSibling;
+ break;
+ }
+ }
+ }
+ if ((!lastChild->IsText() &&
+ !HTMLEditUtils::IsContainerNode(*lastChild)) ||
+ !EditorUtils::IsEditableContent(*lastChild,
+ EditorBase::EditorType::HTML)) {
+ break;
+ }
+ if (aInvisibleText == InvisibleText::Skip && lastChild->IsText()) {
+ Maybe<uint32_t> visibleCharOffset =
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
+ *lastChild->AsText(), lastChild->AsText()->TextDataLength());
+ if (visibleCharOffset.isNothing()) {
+ result = EditorDOMPointType::AtEndOf(*lastChild);
+ break;
+ }
+ result.Set(lastChild, visibleCharOffset.value() + 1u);
+ break;
+ }
+ result = EditorDOMPointType::AtEndOf(*lastChild);
+ }
+ return result;
+ }
+
+ /**
+ * Get `#[0-9a-f]{6}` style HTML color value if aColorValue is valid value
+ * for color-specifying attribute. The result is useful to set attributes
+ * of HTML elements which take a color value.
+ *
+ * @param aColorValue [in] Should be one of `#[0-9a-fA-Z]{3}`,
+ * `#[0-9a-fA-Z]{3}` or a color name.
+ * @param aNormalizedValue [out] Set to `#[0-9a-f]{6}` style color code
+ * if this returns true. Otherwise, returns
+ * aColorValue as-is.
+ * @return true if aColorValue is valid. Otherwise, false.
+ */
+ static bool GetNormalizedHTMLColorValue(const nsAString& aColorValue,
+ nsAString& aNormalizedValue);
+
+ /**
+ * Return true if aColorValue may be a CSS specific color value or general
+ * keywords of CSS.
+ */
+ [[nodiscard]] static bool MaybeCSSSpecificColorValue(
+ const nsAString& aColorValue);
+
+ /**
+ * Return true if aColorValue can be specified to `color` value of <font>.
+ */
+ [[nodiscard]] static bool CanConvertToHTMLColorValue(
+ const nsAString& aColorValue);
+
+ /**
+ * Convert aColorValue to `#[0-9a-f]{6}` style HTML color value.
+ */
+ static bool ConvertToNormalizedHTMLColorValue(const nsAString& aColorValue,
+ nsAString& aNormalizedValue);
+
+ /**
+ * Get serialized color value (`rgb(...)` or `rgba(...)`) or "currentcolor"
+ * if aColorValue is valid. The result is useful to set CSS color property.
+ *
+ * @param aColorValue [in] Should be valid CSS color value.
+ * @param aZeroAlphaColor [in] If TransparentKeyword, aNormalizedValue is
+ * set to "transparent" if the alpha value is 0.
+ * Otherwise, `rgba(...)` value is set.
+ * @param aNormalizedValue [out] Serialized color value or "currentcolor".
+ * @return true if aColorValue is valid. Otherwise, false.
+ */
+ enum class ZeroAlphaColor { RGBAValue, TransparentKeyword };
+ static bool GetNormalizedCSSColorValue(const nsAString& aColorValue,
+ ZeroAlphaColor aZeroAlphaColor,
+ nsAString& aNormalizedValue);
+
+ /**
+ * Check whether aColorA and aColorB are same color.
+ *
+ * @param aTransparentKeyword Whether allow to treat "transparent" keyword
+ * as a valid value or an invalid value.
+ * @return If aColorA and aColorB are valid values and
+ * mean same color, returns true.
+ */
+ enum class TransparentKeyword { Invalid, Allowed };
+ static bool IsSameHTMLColorValue(const nsAString& aColorA,
+ const nsAString& aColorB,
+ TransparentKeyword aTransparentKeyword);
+
+ /**
+ * Check whether aColorA and aColorB are same color.
+ *
+ * @return If aColorA and aColorB are valid values and
+ * mean same color, returns true.
+ */
+ template <typename CharType>
+ static bool IsSameCSSColorValue(const nsTSubstring<CharType>& aColorA,
+ const nsTSubstring<CharType>& aColorB);
+
+ private:
+ static bool CanNodeContain(nsHTMLTag aParentTagId, nsHTMLTag aChildTagId);
+ static bool IsContainerNode(nsHTMLTag aTagId);
+
+ static bool CanCrossContentBoundary(nsIContent& aContent,
+ TableBoundary aHowToTreatTableBoundary) {
+ const bool cannotCrossBoundary =
+ (aHowToTreatTableBoundary == TableBoundary::NoCrossAnyTableElement &&
+ HTMLEditUtils::IsAnyTableElement(&aContent)) ||
+ (aHowToTreatTableBoundary == TableBoundary::NoCrossTableElement &&
+ aContent.IsHTMLElement(nsGkAtoms::table));
+ return !cannotCrossBoundary;
+ }
+
+ static bool IsContentIgnored(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ if (aOptions.contains(WalkTreeOption::IgnoreNonEditableNode) &&
+ !EditorUtils::IsEditableContent(aContent,
+ EditorUtils::EditorType::HTML)) {
+ return true;
+ }
+ if (aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) &&
+ !EditorUtils::IsElementOrText(aContent)) {
+ return true;
+ }
+ if (aOptions.contains(WalkTreeOption::IgnoreWhiteSpaceOnlyText) &&
+ aContent.IsText() &&
+ const_cast<Text*>(aContent.AsText())->TextIsOnlyWhitespace()) {
+ return true;
+ }
+ return false;
+ }
+
+ static uint32_t CountChildren(const nsINode& aNode,
+ const WalkTreeOptions& aOptions,
+ BlockInlineCheck aBlockInlineCheck) {
+ uint32_t count = 0;
+ for (nsIContent* child = aNode.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) {
+ break;
+ }
+ ++count;
+ }
+ return count;
+ }
+
+ /**
+ * Helper for GetPreviousContent() and GetNextContent().
+ */
+ static nsIContent* GetAdjacentLeafContent(
+ const nsINode& aNode, WalkTreeDirection aWalkTreeDirection,
+ const WalkTreeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr);
+ static nsIContent* GetAdjacentContent(
+ const nsINode& aNode, WalkTreeDirection aWalkTreeDirection,
+ const WalkTreeOptions& aOptions, BlockInlineCheck aBlockInlineCheck,
+ const Element* aAncestorLimiter = nullptr);
+
+ /**
+ * GetElementOfImmediateBlockBoundary() returns a block element if its
+ * block boundary and aContent may be first visible thing before/after the
+ * boundary. And it may return a <br> element only when aContent is a
+ * text node and follows a <br> element because only in this case, the
+ * start white-spaces are invisible. So the <br> element works same as
+ * a block boundary.
+ */
+ static Element* GetElementOfImmediateBlockBoundary(
+ const nsIContent& aContent, const WalkTreeDirection aDirection);
+};
+
+/**
+ * DefinitionListItemScanner() scans given `<dl>` element's children.
+ * Then, you can check whether `<dt>` and/or `<dd>` elements are in it.
+ */
+class MOZ_STACK_CLASS DefinitionListItemScanner final {
+ using Element = dom::Element;
+
+ public:
+ DefinitionListItemScanner() = delete;
+ explicit DefinitionListItemScanner(Element& aDLElement) {
+ MOZ_ASSERT(aDLElement.IsHTMLElement(nsGkAtoms::dl));
+ for (nsIContent* child = aDLElement.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::dt)) {
+ mDTFound = true;
+ if (mDDFound) {
+ break;
+ }
+ continue;
+ }
+ if (child->IsHTMLElement(nsGkAtoms::dd)) {
+ mDDFound = true;
+ if (mDTFound) {
+ break;
+ }
+ continue;
+ }
+ }
+ }
+
+ bool DTElementFound() const { return mDTFound; }
+ bool DDElementFound() const { return mDDFound; }
+
+ private:
+ bool mDTFound = false;
+ bool mDDFound = false;
+};
+
+/**
+ * SelectedTableCellScanner() scans all table cell elements which are selected
+ * by each selection range. Note that if 2nd or later ranges do not select
+ * only one table cell element, the ranges are just ignored.
+ */
+class MOZ_STACK_CLASS SelectedTableCellScanner final {
+ using Element = dom::Element;
+ using Selection = dom::Selection;
+
+ public:
+ SelectedTableCellScanner() = delete;
+ explicit SelectedTableCellScanner(const Selection& aSelection) {
+ Element* firstSelectedCellElement =
+ HTMLEditUtils::GetFirstSelectedTableCellElement(aSelection);
+ if (!firstSelectedCellElement) {
+ return; // We're not in table cell selection mode.
+ }
+ mSelectedCellElements.SetCapacity(aSelection.RangeCount());
+ mSelectedCellElements.AppendElement(*firstSelectedCellElement);
+ const uint32_t rangeCount = aSelection.RangeCount();
+ for (const uint32_t i : IntegerRange(1u, rangeCount)) {
+ MOZ_ASSERT(aSelection.RangeCount() == rangeCount);
+ nsRange* range = aSelection.GetRangeAt(i);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!range)) ||
+ MOZ_UNLIKELY(NS_WARN_IF(!range->IsPositioned()))) {
+ continue; // Shouldn't occur in normal conditions.
+ }
+ // Just ignore selection ranges which do not select only one table
+ // cell element. This is possible case if web apps sets multiple
+ // selections and first range selects a table cell element.
+ if (Element* selectedCellElement =
+ HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(*range)) {
+ mSelectedCellElements.AppendElement(*selectedCellElement);
+ }
+ }
+ }
+
+ explicit SelectedTableCellScanner(const AutoRangeArray& aRanges);
+
+ bool IsInTableCellSelectionMode() const {
+ return !mSelectedCellElements.IsEmpty();
+ }
+
+ const nsTArray<OwningNonNull<Element>>& ElementsRef() const {
+ return mSelectedCellElements;
+ }
+
+ /**
+ * GetFirstElement() and GetNextElement() are stateful iterator methods.
+ * This is useful to port legacy code which used old `nsITableEditor` API.
+ */
+ Element* GetFirstElement() const {
+ MOZ_ASSERT(!mSelectedCellElements.IsEmpty());
+ mIndex = 0;
+ return !mSelectedCellElements.IsEmpty() ? mSelectedCellElements[0].get()
+ : nullptr;
+ }
+ Element* GetNextElement() const {
+ MOZ_ASSERT(mIndex < mSelectedCellElements.Length());
+ return ++mIndex < mSelectedCellElements.Length()
+ ? mSelectedCellElements[mIndex].get()
+ : nullptr;
+ }
+
+ private:
+ AutoTArray<OwningNonNull<Element>, 16> mSelectedCellElements;
+ mutable size_t mIndex = 0;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef HTMLEditUtils_h
diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp
new file mode 100644
index 0000000000..9f4aab3dab
--- /dev/null
+++ b/editor/libeditor/HTMLEditor.cpp
@@ -0,0 +1,7324 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditor.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditorInlines.h"
+
+#include "AutoRangeArray.h"
+#include "CSSEditUtils.h"
+#include "EditAction.h"
+#include "EditorBase.h"
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "ErrorList.h"
+#include "HTMLEditorEventListener.h"
+#include "HTMLEditUtils.h"
+#include "InsertNodeTransaction.h"
+#include "JoinNodesTransaction.h"
+#include "MoveNodeTransaction.h"
+#include "PendingStyles.h"
+#include "ReplaceTextTransaction.h"
+#include "SplitNodeTransaction.h"
+#include "WSRunObject.h"
+
+#include "mozilla/ComposerCommandsUpdater.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/Encoding.h" // for Encoding
+#include "mozilla/FlushType.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/IntegerRange.h" // for IntegerRange
+#include "mozilla/InternalMutationEvent.h"
+#include "mozilla/mozInlineSpellChecker.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/StyleSheet.h"
+#include "mozilla/StyleSheetInlines.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TextServicesDocument.h"
+#include "mozilla/ToString.h"
+#include "mozilla/css/Loader.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/Attr.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventTarget.h"
+#include "mozilla/dom/HTMLAnchorElement.h"
+#include "mozilla/dom/HTMLBodyElement.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/HTMLButtonElement.h"
+#include "mozilla/dom/HTMLSummaryElement.h"
+#include "mozilla/dom/NameSpaceConstants.h"
+#include "mozilla/dom/Selection.h"
+
+#include "nsContentList.h"
+#include "nsContentUtils.h"
+#include "nsCRT.h"
+#include "nsDebug.h"
+#include "nsDOMAttributeMap.h"
+#include "nsElementTable.h"
+#include "nsFocusManager.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "nsHTMLDocument.h"
+#include "nsIContent.h"
+#include "nsIContentInlines.h"
+#include "nsIEditActionListener.h"
+#include "nsIFrame.h"
+#include "nsIPrincipal.h"
+#include "nsISelectionController.h"
+#include "nsIURI.h"
+#include "nsIWidget.h"
+#include "nsNetUtil.h"
+#include "nsPresContext.h"
+#include "nsPrintfCString.h"
+#include "nsPIDOMWindow.h"
+#include "nsStyledElement.h"
+#include "nsTextFragment.h"
+#include "nsUnicharUtils.h"
+
+namespace mozilla {
+
+using namespace dom;
+using namespace widget;
+
+using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
+using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
+
+// Some utilities to handle overloading of "A" tag for link and named anchor.
+static bool IsLinkTag(const nsAtom& aTagName) {
+ return &aTagName == nsGkAtoms::href;
+}
+
+static bool IsNamedAnchorTag(const nsAtom& aTagName) {
+ return &aTagName == nsGkAtoms::anchor;
+}
+
+// Helper struct for DoJoinNodes() and DoSplitNode().
+struct MOZ_STACK_CLASS SavedRange final {
+ RefPtr<Selection> mSelection;
+ nsCOMPtr<nsINode> mStartContainer;
+ nsCOMPtr<nsINode> mEndContainer;
+ uint32_t mStartOffset = 0;
+ uint32_t mEndOffset = 0;
+};
+
+/******************************************************************************
+ * HTMLEditor::AutoSelectionRestorer
+ *****************************************************************************/
+
+HTMLEditor::AutoSelectionRestorer::AutoSelectionRestorer(
+ HTMLEditor& aHTMLEditor)
+ : mHTMLEditor(nullptr) {
+ if (aHTMLEditor.ArePreservingSelection()) {
+ // We already have initialized mParentData::mSavedSelection, so this must
+ // be nested call.
+ return;
+ }
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ mHTMLEditor = &aHTMLEditor;
+ mHTMLEditor->PreserveSelectionAcrossActions();
+}
+
+HTMLEditor::AutoSelectionRestorer::~AutoSelectionRestorer() {
+ if (!mHTMLEditor || !mHTMLEditor->ArePreservingSelection()) {
+ return;
+ }
+ DebugOnly<nsresult> rvIgnored = mHTMLEditor->RestorePreservedSelection();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::RestorePreservedSelection() failed, but ignored");
+}
+
+void HTMLEditor::AutoSelectionRestorer::Abort() {
+ if (mHTMLEditor) {
+ mHTMLEditor->StopPreservingSelection();
+ }
+}
+
+/******************************************************************************
+ * HTMLEditor
+ *****************************************************************************/
+
+template Result<CreateContentResult, nsresult>
+HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(
+ nsIContent& aContentToInsert, const EditorDOMPoint& aPointToInsert,
+ SplitAtEdges aSplitAtEdges);
+template Result<CreateElementResult, nsresult>
+HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(
+ Element& aContentToInsert, const EditorDOMPoint& aPointToInsert,
+ SplitAtEdges aSplitAtEdges);
+template Result<CreateTextResult, nsresult>
+HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(
+ Text& aContentToInsert, const EditorDOMPoint& aPointToInsert,
+ SplitAtEdges aSplitAtEdges);
+
+HTMLEditor::InitializeInsertingElement HTMLEditor::DoNothingForNewElement =
+ [](HTMLEditor&, Element&, const EditorDOMPoint&) { return NS_OK; };
+
+HTMLEditor::InitializeInsertingElement HTMLEditor::InsertNewBRElement =
+ [](HTMLEditor& aHTMLEditor, Element& aNewElement, const EditorDOMPoint&)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ MOZ_ASSERT(!aNewElement.IsInComposedDoc());
+ Result<CreateElementResult, nsresult> createBRElementResult =
+ aHTMLEditor.InsertBRElement(WithTransaction::No,
+ EditorDOMPoint(&aNewElement, 0u));
+ if (MOZ_UNLIKELY(createBRElementResult.isErr())) {
+ NS_WARNING_ASSERTION(
+ createBRElementResult.isOk(),
+ "HTMLEditor::InsertBRElement(WithTransaction::No) failed");
+ return createBRElementResult.unwrapErr();
+ }
+ createBRElementResult.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+ };
+
+// static
+Result<CreateElementResult, nsresult>
+HTMLEditor::AppendNewElementToInsertingElement(
+ HTMLEditor& aHTMLEditor, const nsStaticAtom& aTagName, Element& aNewElement,
+ const InitializeInsertingElement& aInitializer) {
+ MOZ_ASSERT(!aNewElement.IsInComposedDoc());
+ Result<CreateElementResult, nsresult> createNewElementResult =
+ aHTMLEditor.CreateAndInsertElement(
+ WithTransaction::No, const_cast<nsStaticAtom&>(aTagName),
+ EditorDOMPoint(&aNewElement, 0u), aInitializer);
+ NS_WARNING_ASSERTION(
+ createNewElementResult.isOk(),
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::No) failed");
+ return createNewElementResult;
+}
+
+// static
+Result<CreateElementResult, nsresult>
+HTMLEditor::AppendNewElementWithBRToInsertingElement(
+ HTMLEditor& aHTMLEditor, const nsStaticAtom& aTagName,
+ Element& aNewElement) {
+ MOZ_ASSERT(!aNewElement.IsInComposedDoc());
+ Result<CreateElementResult, nsresult> createNewElementWithBRResult =
+ HTMLEditor::AppendNewElementToInsertingElement(
+ aHTMLEditor, aTagName, aNewElement, HTMLEditor::InsertNewBRElement);
+ NS_WARNING_ASSERTION(
+ createNewElementWithBRResult.isOk(),
+ "HTMLEditor::AppendNewElementToInsertingElement() failed");
+ return createNewElementWithBRResult;
+}
+
+HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributes =
+ [](HTMLEditor&, const Element&, const Element&, const Attr&, nsString&) {
+ return true;
+ };
+HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptId =
+ [](HTMLEditor&, const Element&, const Element&, const Attr& aAttr,
+ nsString&) {
+ return aAttr.NodeInfo()->NamespaceID() != kNameSpaceID_None ||
+ aAttr.NodeInfo()->NameAtom() != nsGkAtoms::id;
+ };
+HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptDir =
+ [](HTMLEditor&, const Element&, const Element&, const Attr& aAttr,
+ nsString&) {
+ return aAttr.NodeInfo()->NamespaceID() != kNameSpaceID_None ||
+ aAttr.NodeInfo()->NameAtom() != nsGkAtoms::dir;
+ };
+HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptIdAndDir =
+ [](HTMLEditor&, const Element&, const Element&, const Attr& aAttr,
+ nsString&) {
+ return !(aAttr.NodeInfo()->NamespaceID() == kNameSpaceID_None &&
+ (aAttr.NodeInfo()->NameAtom() == nsGkAtoms::id ||
+ aAttr.NodeInfo()->NameAtom() == nsGkAtoms::dir));
+ };
+
+HTMLEditor::HTMLEditor(const Document& aDocument)
+ : EditorBase(EditorBase::EditorType::HTML),
+ mCRInParagraphCreatesParagraph(false),
+ mIsObjectResizingEnabled(
+ StaticPrefs::editor_resizing_enabled_by_default()),
+ mIsResizing(false),
+ mPreserveRatio(false),
+ mResizedObjectIsAnImage(false),
+ mIsAbsolutelyPositioningEnabled(
+ StaticPrefs::editor_positioning_enabled_by_default()),
+ mResizedObjectIsAbsolutelyPositioned(false),
+ mGrabberClicked(false),
+ mIsMoving(false),
+ mSnapToGridEnabled(false),
+ mIsInlineTableEditingEnabled(
+ StaticPrefs::editor_inline_table_editing_enabled_by_default()),
+ mIsCSSPrefChecked(StaticPrefs::editor_use_css()),
+ mOriginalX(0),
+ mOriginalY(0),
+ mResizedObjectX(0),
+ mResizedObjectY(0),
+ mResizedObjectWidth(0),
+ mResizedObjectHeight(0),
+ mResizedObjectMarginLeft(0),
+ mResizedObjectMarginTop(0),
+ mResizedObjectBorderLeft(0),
+ mResizedObjectBorderTop(0),
+ mXIncrementFactor(0),
+ mYIncrementFactor(0),
+ mWidthIncrementFactor(0),
+ mHeightIncrementFactor(0),
+ mInfoXIncrement(20),
+ mInfoYIncrement(20),
+ mPositionedObjectX(0),
+ mPositionedObjectY(0),
+ mPositionedObjectWidth(0),
+ mPositionedObjectHeight(0),
+ mPositionedObjectMarginLeft(0),
+ mPositionedObjectMarginTop(0),
+ mPositionedObjectBorderLeft(0),
+ mPositionedObjectBorderTop(0),
+ mGridSize(0),
+ mDefaultParagraphSeparator(ParagraphSeparator::div) {}
+
+HTMLEditor::~HTMLEditor() {
+ Telemetry::Accumulate(Telemetry::HTMLEDITORS_WITH_BEFOREINPUT_LISTENERS,
+ MayHaveBeforeInputEventListenersForTelemetry() ? 1 : 0);
+ Telemetry::Accumulate(
+ Telemetry::HTMLEDITORS_OVERRIDDEN_BY_BEFOREINPUT_LISTENERS,
+ mHasBeforeInputBeenCanceled ? 1 : 0);
+ Telemetry::Accumulate(
+ Telemetry::
+ HTMLEDITORS_WITH_MUTATION_LISTENERS_WITHOUT_BEFOREINPUT_LISTENERS,
+ !MayHaveBeforeInputEventListenersForTelemetry() &&
+ MayHaveMutationEventListeners()
+ ? 1
+ : 0);
+ Telemetry::Accumulate(
+ Telemetry::
+ HTMLEDITORS_WITH_MUTATION_OBSERVERS_WITHOUT_BEFOREINPUT_LISTENERS,
+ !MayHaveBeforeInputEventListenersForTelemetry() &&
+ MutationObserverHasObservedNodeForTelemetry()
+ ? 1
+ : 0);
+
+ mPendingStylesToApplyToNewContent = nullptr;
+
+ if (mDisabledLinkHandling) {
+ if (Document* doc = GetDocument()) {
+ doc->SetLinkHandlingEnabled(mOldLinkHandlingEnabled);
+ }
+ }
+
+ RemoveEventListeners();
+
+ HideAnonymousEditingUIs();
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEditor)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLEditor, EditorBase)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingStylesToApplyToNewContent)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mComposerCommandsUpdater)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mChangedRangeForTopLevelEditSubAction)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPaddingBRElementForEmptyEditor)
+ tmp->HideAnonymousEditingUIs();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLEditor, EditorBase)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingStylesToApplyToNewContent)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mComposerCommandsUpdater)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChangedRangeForTopLevelEditSubAction)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPaddingBRElementForEmptyEditor)
+
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopLeftHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopRightHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLeftHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRightHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomLeftHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomRightHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActivatedHandle)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingShadow)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingInfo)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizedObject)
+
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbsolutelyPositionedObject)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGrabber)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPositioningShadow)
+
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineEditedCell)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnBeforeButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveColumnButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnAfterButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowBeforeButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveRowButton)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowAfterButton)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(HTMLEditor, EditorBase)
+NS_IMPL_RELEASE_INHERITED(HTMLEditor, EditorBase)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIHTMLEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIHTMLObjectResizer)
+ NS_INTERFACE_MAP_ENTRY(nsIHTMLAbsPosEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIHTMLInlineTableEditor)
+ NS_INTERFACE_MAP_ENTRY(nsITableEditor)
+ NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIEditorMailSupport)
+NS_INTERFACE_MAP_END_INHERITING(EditorBase)
+
+nsresult HTMLEditor::Init(Document& aDocument,
+ ComposerCommandsUpdater& aComposerCommandsUpdater,
+ uint32_t aFlags) {
+ MOZ_ASSERT(!mInitSucceeded,
+ "HTMLEditor::Init() called again without calling PreDestroy()?");
+
+ MOZ_DIAGNOSTIC_ASSERT(!mComposerCommandsUpdater ||
+ mComposerCommandsUpdater == &aComposerCommandsUpdater);
+ mComposerCommandsUpdater = &aComposerCommandsUpdater;
+
+ RefPtr<PresShell> presShell = aDocument.GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = InitInternal(aDocument, nullptr, *presShell, aFlags);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InitInternal() failed");
+ return rv;
+ }
+
+ // Init mutation observer
+ aDocument.AddMutationObserverUnlessExists(this);
+
+ if (!mRootElement) {
+ UpdateRootElement();
+ }
+
+ // disable Composer-only features
+ if (IsMailEditor()) {
+ DebugOnly<nsresult> rvIgnored = SetAbsolutePositioningEnabled(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SetAbsolutePositioningEnabled(false) failed, but ignored");
+ rvIgnored = SetSnapToGridEnabled(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SetSnapToGridEnabled(false) failed, but ignored");
+ }
+
+ // disable links
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!IsPlaintextMailComposer() && !IsInteractionAllowed()) {
+ mDisabledLinkHandling = true;
+ mOldLinkHandlingEnabled = document->LinkHandlingEnabled();
+ document->SetLinkHandlingEnabled(false);
+ }
+
+ // init the type-in state
+ mPendingStylesToApplyToNewContent = new PendingStyles();
+
+ if (!IsInteractionAllowed()) {
+ nsCOMPtr<nsIURI> uaURI;
+ rv = NS_NewURI(getter_AddRefs(uaURI),
+ "resource://gre/res/EditorOverride.css");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = document->LoadAdditionalStyleSheet(Document::eAgentSheet, uaURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = InitEditorContentAndSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::InitEditorContentAndSelection() failed");
+ // XXX Sholdn't we expose `NS_ERROR_EDITOR_DESTROYED` even though this
+ // is a public method?
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Throw away the old transaction manager if this is not the first time that
+ // we're initializing the editor.
+ ClearUndoRedo();
+ EnableUndoRedo(); // FYI: Creating mTransactionManager in this call
+
+ if (mTransactionManager) {
+ mTransactionManager->Attach(*this);
+ }
+
+ MOZ_ASSERT(!mInitSucceeded, "HTMLEditor::Init() shouldn't be nested");
+ mInitSucceeded = true;
+ return NS_OK;
+}
+
+nsresult HTMLEditor::PostCreate() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = PostCreateInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::PostCreatInternal() failed");
+ return rv;
+}
+
+void HTMLEditor::PreDestroy() {
+ if (mDidPreDestroy) {
+ return;
+ }
+
+ mInitSucceeded = false;
+
+ // FYI: Cannot create AutoEditActionDataSetter here. However, it does not
+ // necessary for the methods called by the following code.
+
+ RefPtr<Document> document = GetDocument();
+ if (document) {
+ document->RemoveMutationObserver(this);
+
+ if (!IsInteractionAllowed()) {
+ nsCOMPtr<nsIURI> uaURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(uaURI),
+ "resource://gre/res/EditorOverride.css");
+ if (NS_SUCCEEDED(rv)) {
+ document->RemoveAdditionalStyleSheet(Document::eAgentSheet, uaURI);
+ }
+ }
+ }
+
+ // Clean up after our anonymous content -- we don't want these nodes to
+ // stay around (which they would, since the frames have an owning reference).
+ PresShell* presShell = GetPresShell();
+ if (presShell && presShell->IsDestroying()) {
+ // Just destroying PresShell now.
+ // We have to keep UI elements of anonymous content until PresShell
+ // is destroyed.
+ RefPtr<HTMLEditor> self = this;
+ nsContentUtils::AddScriptRunner(
+ NS_NewRunnableFunction("HTMLEditor::PreDestroy",
+ [self]() { self->HideAnonymousEditingUIs(); }));
+ } else {
+ // PresShell is alive or already gone.
+ HideAnonymousEditingUIs();
+ }
+
+ mPaddingBRElementForEmptyEditor = nullptr;
+
+ PreDestroyInternal();
+}
+
+NS_IMETHODIMP HTMLEditor::GetDocumentCharacterSet(nsACString& aCharacterSet) {
+ nsresult rv = GetDocumentCharsetInternal(aCharacterSet);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetDocumentCharsetInternal() failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::SetDocumentCharacterSet(
+ const nsACString& aCharacterSet) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetCharacterSet);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_NOT_INITIALIZED);
+ }
+ // This method is scriptable, so add-ons could pass in something other
+ // than a canonical name.
+ const Encoding* encoding = Encoding::ForLabelNoReplacement(aCharacterSet);
+ if (!encoding) {
+ NS_WARNING("Encoding::ForLabelNoReplacement() failed");
+ return EditorBase::ToGenericNSResult(NS_ERROR_INVALID_ARG);
+ }
+ document->SetDocumentCharacterSet(WrapNotNull(encoding));
+
+ // Update META charset element.
+ if (UpdateMetaCharsetWithTransaction(*document, aCharacterSet)) {
+ return NS_OK;
+ }
+
+ // Set attributes to the created element
+ if (aCharacterSet.IsEmpty()) {
+ return NS_OK;
+ }
+
+ RefPtr<nsContentList> headElementList =
+ document->GetElementsByTagName(u"head"_ns);
+ if (NS_WARN_IF(!headElementList)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIContent> primaryHeadElement = headElementList->Item(0);
+ if (NS_WARN_IF(!primaryHeadElement)) {
+ return NS_OK;
+ }
+
+ // Create a new meta charset tag
+ Result<CreateElementResult, nsresult> createNewMetaElementResult =
+ CreateAndInsertElement(
+ WithTransaction::Yes, *nsGkAtoms::meta,
+ EditorDOMPoint(primaryHeadElement, 0),
+ [&aCharacterSet](HTMLEditor&, Element& aMetaElement,
+ const EditorDOMPoint&) {
+ MOZ_ASSERT(!aMetaElement.IsInComposedDoc());
+ DebugOnly<nsresult> rvIgnored =
+ aMetaElement.SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv,
+ u"Content-Type"_ns, false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::httpEquiv, \"Content-Type\", "
+ "false) failed, but ignored");
+ rvIgnored =
+ aMetaElement.SetAttr(kNameSpaceID_None, nsGkAtoms::content,
+ u"text/html;charset="_ns +
+ NS_ConvertASCIItoUTF16(aCharacterSet),
+ false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ nsPrintfCString(
+ "Element::SetAttr(nsGkAtoms::content, "
+ "\"text/html;charset=%s\", false) failed, but ignored",
+ nsPromiseFlatCString(aCharacterSet).get())
+ .get());
+ return NS_OK;
+ });
+ NS_WARNING_ASSERTION(createNewMetaElementResult.isOk(),
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::"
+ "Yes, nsGkAtoms::meta) failed, but ignored");
+ // Probably, we don't need to update selection in this case since we should
+ // not put selection into <head> element.
+ createNewMetaElementResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+bool HTMLEditor::UpdateMetaCharsetWithTransaction(
+ Document& aDocument, const nsACString& aCharacterSet) {
+ // get a list of META tags
+ RefPtr<nsContentList> metaElementList =
+ aDocument.GetElementsByTagName(u"meta"_ns);
+ if (NS_WARN_IF(!metaElementList)) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < metaElementList->Length(true); ++i) {
+ RefPtr<Element> metaElement = metaElementList->Item(i)->AsElement();
+ MOZ_ASSERT(metaElement);
+
+ nsAutoString currentValue;
+ metaElement->GetAttr(nsGkAtoms::httpEquiv, currentValue);
+
+ if (!FindInReadable(u"content-type"_ns, currentValue,
+ nsCaseInsensitiveStringComparator)) {
+ continue;
+ }
+
+ metaElement->GetAttr(nsGkAtoms::content, currentValue);
+
+ constexpr auto charsetEquals = u"charset="_ns;
+ nsAString::const_iterator originalStart, start, end;
+ originalStart = currentValue.BeginReading(start);
+ currentValue.EndReading(end);
+ if (!FindInReadable(charsetEquals, start, end,
+ nsCaseInsensitiveStringComparator)) {
+ continue;
+ }
+
+ // set attribute to <original prefix> charset=text/html
+ nsresult rv = SetAttributeWithTransaction(
+ *metaElement, *nsGkAtoms::content,
+ Substring(originalStart, start) + charsetEquals +
+ NS_ConvertASCIItoUTF16(aCharacterSet));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::content) failed");
+ return NS_SUCCEEDED(rv);
+ }
+ return false;
+}
+
+NS_IMETHODIMP HTMLEditor::NotifySelectionChanged(Document* aDocument,
+ Selection* aSelection,
+ int16_t aReason,
+ int32_t aAmount) {
+ if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (mPendingStylesToApplyToNewContent) {
+ RefPtr<PendingStyles> pendingStyles = mPendingStylesToApplyToNewContent;
+ pendingStyles->OnSelectionChange(*this, aReason);
+
+ // We used a class which derived from nsISelectionListener to call
+ // HTMLEditor::RefreshEditingUI(). The lifetime of the class was
+ // exactly same as mPendingStylesToApplyToNewContent. So, call it only when
+ // mPendingStylesToApplyToNewContent is not nullptr.
+ if ((aReason & (nsISelectionListener::MOUSEDOWN_REASON |
+ nsISelectionListener::KEYPRESS_REASON |
+ nsISelectionListener::SELECTALL_REASON)) &&
+ aSelection) {
+ // the selection changed and we need to check if we have to
+ // hide and/or redisplay resizing handles
+ // FYI: This is an XPCOM method. So, the caller, Selection, guarantees
+ // the lifetime of this instance. So, don't need to grab this with
+ // local variable.
+ DebugOnly<nsresult> rv = RefreshEditingUI();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::RefreshEditingUI() failed, but ignored");
+ }
+ }
+
+ if (mComposerCommandsUpdater) {
+ RefPtr<ComposerCommandsUpdater> updater = mComposerCommandsUpdater;
+ updater->OnSelectionChange();
+ }
+
+ nsresult rv = EditorBase::NotifySelectionChanged(aDocument, aSelection,
+ aReason, aAmount);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::NotifySelectionChanged() failed");
+ return rv;
+}
+
+void HTMLEditor::UpdateRootElement() {
+ // Use the HTML documents body element as the editor root if we didn't
+ // get a root element during initialization.
+
+ mRootElement = GetBodyElement();
+ if (!mRootElement) {
+ RefPtr<Document> doc = GetDocument();
+ if (doc) {
+ // If there is no HTML body element,
+ // we should use the document root element instead.
+ mRootElement = doc->GetDocumentElement();
+ }
+ // else leave it null, for lack of anything better.
+ }
+}
+
+nsresult HTMLEditor::FocusedElementOrDocumentBecomesEditable(
+ Document& aDocument, Element* aElement) {
+ // If we should've already handled focus event, selection limiter should not
+ // be set. Therefore, if it's set, we should do nothing here.
+ if (GetSelectionAncestorLimiter()) {
+ return NS_OK;
+ }
+ // If we should be in the design mode, we want to handle focus event fired
+ // on the document node. Therefore, we should emulate it here.
+ if (IsInDesignMode() && (!aElement || aElement->IsInDesignMode())) {
+ MOZ_ASSERT(&aDocument == GetDocument());
+ nsresult rv = OnFocus(aDocument);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnFocus() failed");
+ return rv;
+ }
+
+ if (NS_WARN_IF(!aElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Otherwise, we should've already handled focus event on the element,
+ // therefore, we need to emulate it here.
+ MOZ_ASSERT(nsFocusManager::GetFocusManager()->GetFocusedElement() ==
+ aElement);
+ nsresult rv = OnFocus(*aElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnFocus() failed");
+
+ // Note that we don't need to call
+ // IMEStateManager::MaybeOnEditableStateDisabled here because
+ // EditorBase::OnFocus must have already been called IMEStateManager::OnFocus
+ // if succeeded. And perhaps, it's okay that IME is not enabled when
+ // HTMLEditor fails to start handling since nobody can handle composition
+ // events anyway...
+
+ return rv;
+}
+
+nsresult HTMLEditor::OnFocus(const nsINode& aOriginalEventTargetNode) {
+ // Before doing anything, we should check whether the original target is still
+ // valid focus event target because it may have already lost focus.
+ if (!CanKeepHandlingFocusEvent(aOriginalEventTargetNode)) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return EditorBase::OnFocus(aOriginalEventTargetNode);
+}
+
+nsresult HTMLEditor::FocusedElementOrDocumentBecomesNotEditable(
+ HTMLEditor* aHTMLEditor, Document& aDocument, Element* aElement) {
+ nsresult rv = [&]() MOZ_CAN_RUN_SCRIPT {
+ // If HTMLEditor has not been created yet, we just need to adjust
+ // IMEStateManager. So, don't return error.
+ if (!aHTMLEditor) {
+ return NS_OK;
+ }
+
+ nsIContent* const limiter = aHTMLEditor->GetSelectionAncestorLimiter();
+ // The HTMLEditor has not received `focus` event so that it does not need to
+ // emulate `blur`.
+ if (!limiter) {
+ return NS_OK;
+ }
+
+ // If we should be in the design mode, we should treat it as blur from
+ // the document node.
+ if (aHTMLEditor->IsInDesignMode() &&
+ (!aElement || aElement->IsInDesignMode())) {
+ MOZ_ASSERT(aHTMLEditor->GetDocument() == &aDocument);
+ nsresult rv = aHTMLEditor->OnBlur(&aDocument);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnBlur() failed");
+ return rv;
+ }
+ // If the HTMLEditor has already received `focus` event for different
+ // element than aElement, we'll receive `blur` event later so that we need
+ // to do nothing here.
+ if (aElement != limiter) {
+ return NS_OK;
+ }
+
+ // Otherwise, even though the limiter keeps having focus but becomes not
+ // editable. From HTMLEditor point of view, this is equivalent to the
+ // elements gets blurred. Therefore, we should treat it as losing
+ // focus.
+ nsresult rv = aHTMLEditor->OnBlur(aElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnBlur() failed");
+ return rv;
+ }();
+
+ // If the element becomes not editable without focus change, IMEStateManager
+ // does not have a chance to disable IME. Therefore, (even if we fail to
+ // handle the emulated blur above,) we should notify IMEStateManager of the
+ // editing state change.
+ RefPtr<Element> focusedElement = aElement ? aElement
+ : aHTMLEditor
+ ? aHTMLEditor->GetFocusedElement()
+ : nullptr;
+ RefPtr<nsPresContext> presContext =
+ focusedElement ? focusedElement->GetPresContext(
+ Element::PresContextFor::eForComposedDoc)
+ : aDocument.GetPresContext();
+ if (presContext) {
+ IMEStateManager::MaybeOnEditableStateDisabled(*presContext, focusedElement);
+ }
+
+ return rv;
+}
+
+nsresult HTMLEditor::OnBlur(const EventTarget* aEventTarget) {
+ // check if something else is focused. If another element is focused, then
+ // we should not change the selection.
+ nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
+ if (MOZ_UNLIKELY(!focusManager)) {
+ return NS_OK;
+ }
+
+ // If another element already has focus, we should not maintain the selection
+ // because we may not have the rights doing it.
+ if (focusManager->GetFocusedElement()) {
+ return NS_OK;
+ }
+
+ // If it's in the designMode, and blur occurs, the target must be the
+ // document node. If a blur event is fired and the target is an element, it
+ // must be delayed blur event at initializing the `HTMLEditor`.
+ if (IsInDesignMode() && Element::FromEventTargetOrNull(aEventTarget)) {
+ return NS_OK;
+ }
+ nsresult rv = FinalizeSelection();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::FinalizeSelection() failed");
+ return rv;
+}
+
+Element* HTMLEditor::FindSelectionRoot(const nsINode& aNode) const {
+ MOZ_ASSERT(aNode.IsDocument() || aNode.IsContent(),
+ "aNode must be content or document node");
+
+ if (NS_WARN_IF(!aNode.IsInComposedDoc())) {
+ return nullptr;
+ }
+
+ if (aNode.IsInDesignMode()) {
+ return GetDocument()->GetRootElement();
+ }
+
+ nsIContent* content = const_cast<nsIContent*>(aNode.AsContent());
+ if (!content->HasFlag(NODE_IS_EDITABLE)) {
+ // If the content is in read-write state but is not editable itself,
+ // return it as the selection root.
+ if (content->IsElement() &&
+ content->AsElement()->State().HasState(ElementState::READWRITE)) {
+ return content->AsElement();
+ }
+ return nullptr;
+ }
+
+ // For non-readonly editors we want to find the root of the editable subtree
+ // containing aContent.
+ return content->GetEditingHost();
+}
+
+bool HTMLEditor::IsInDesignMode() const {
+ // TODO: If active editing host is in a shadow tree, it means that we should
+ // behave exactly same as contenteditable mode because shadow tree
+ // content is not editable even if composed document is in design mode,
+ // but contenteditable elements in shoadow trees are focusable and
+ // their content is editable. Changing this affects to drop event
+ // handler and blur event handler, so please add new tests for them
+ // when you change here.
+ Document* document = GetDocument();
+ return document && document->IsInDesignMode();
+}
+
+bool HTMLEditor::EntireDocumentIsEditable() const {
+ Document* document = GetDocument();
+ return document && document->GetDocumentElement() &&
+ (document->GetDocumentElement()->IsEditable() ||
+ (document->GetBody() && document->GetBody()->IsEditable()));
+}
+
+void HTMLEditor::CreateEventListeners() {
+ // Don't create the handler twice
+ if (!mEventListener) {
+ mEventListener = new HTMLEditorEventListener();
+ }
+}
+
+nsresult HTMLEditor::InstallEventListeners() {
+ // FIXME InstallEventListeners() should not be called if we failed to set
+ // document or create an event listener. So, these checks should be
+ // MOZ_DIAGNOSTIC_ASSERT instead.
+ MOZ_ASSERT(GetDocument());
+ if (MOZ_UNLIKELY(!GetDocument()) || NS_WARN_IF(!mEventListener)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // NOTE: HTMLEditor doesn't need to initialize mEventTarget here because
+ // the target must be document node and it must be referenced as weak pointer.
+
+ HTMLEditorEventListener* listener =
+ reinterpret_cast<HTMLEditorEventListener*>(mEventListener.get());
+ nsresult rv = listener->Connect(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditorEventListener::Connect() failed");
+ return rv;
+}
+
+void HTMLEditor::Detach(
+ const ComposerCommandsUpdater& aComposerCommandsUpdater) {
+ MOZ_DIAGNOSTIC_ASSERT_IF(
+ mComposerCommandsUpdater,
+ &aComposerCommandsUpdater == mComposerCommandsUpdater);
+ if (mComposerCommandsUpdater == &aComposerCommandsUpdater) {
+ mComposerCommandsUpdater = nullptr;
+ if (mTransactionManager) {
+ mTransactionManager->Detach(*this);
+ }
+ }
+}
+
+NS_IMETHODIMP HTMLEditor::BeginningOfDocument() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = MaybeCollapseSelectionAtFirstEditableNode(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::EndOfDocument() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() failed");
+ // This is low level API for embedders and chrome script so that we can return
+ // raw error code here.
+ return rv;
+}
+
+nsresult HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // We should do nothing with the result of GetRoot() if only a part of the
+ // document is editable.
+ if (!EntireDocumentIsEditable()) {
+ return NS_OK;
+ }
+
+ RefPtr<Element> bodyOrDocumentElement = GetRoot();
+ if (NS_WARN_IF(!bodyOrDocumentElement)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ auto pointToPutCaret = [&]() -> EditorRawDOMPoint {
+ nsCOMPtr<nsIContent> lastLeafContent = HTMLEditUtils::GetLastLeafContent(
+ *bodyOrDocumentElement, {LeafNodeType::OnlyLeafNode});
+ if (!lastLeafContent) {
+ return EditorRawDOMPoint::AtEndOf(*bodyOrDocumentElement);
+ }
+ // TODO: We should put caret into text node if it's visible.
+ return lastLeafContent->IsText() ||
+ HTMLEditUtils::IsContainerNode(*lastLeafContent)
+ ? EditorRawDOMPoint::AtEndOf(*lastLeafContent)
+ : EditorRawDOMPoint(lastLeafContent);
+ }();
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+void HTMLEditor::InitializeSelectionAncestorLimit(
+ nsIContent& aAncestorLimit) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Hack for initializing selection.
+ // HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode() will try to
+ // collapse selection at first editable text node or inline element which
+ // cannot have text nodes as its children. However, selection has already
+ // set into the new editing host by user, we should not change it. For
+ // solving this issue, we should do nothing if selection range is in active
+ // editing host except it's not collapsed at start of the editing host since
+ // aSelection.SetAncestorLimiter(aAncestorLimit) will collapse selection
+ // at start of the new limiter if focus node of aSelection is outside of the
+ // editing host. However, we need to check here if selection is already
+ // collapsed at start of the editing host because it's possible JS to do it.
+ // In such case, we should not modify selection with calling
+ // MaybeCollapseSelectionAtFirstEditableNode().
+
+ // Basically, we should try to collapse selection at first editable node
+ // in HTMLEditor.
+ bool tryToCollapseSelectionAtFirstEditableNode = true;
+ if (SelectionRef().RangeCount() == 1 && SelectionRef().IsCollapsed()) {
+ Element* editingHost = ComputeEditingHost();
+ const nsRange* range = SelectionRef().GetRangeAt(0);
+ if (range->GetStartContainer() == editingHost && !range->StartOffset()) {
+ // JS or user operation has already collapsed selection at start of
+ // the editing host. So, we don't need to try to change selection
+ // in this case.
+ tryToCollapseSelectionAtFirstEditableNode = false;
+ }
+ }
+
+ EditorBase::InitializeSelectionAncestorLimit(aAncestorLimit);
+
+ // XXX Do we need to check if we still need to change selection? E.g.,
+ // we could have already lost focus while we're changing the ancestor
+ // limiter because it may causes "selectionchange" event.
+ if (tryToCollapseSelectionAtFirstEditableNode) {
+ DebugOnly<nsresult> rvIgnored =
+ MaybeCollapseSelectionAtFirstEditableNode(true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(true) failed, "
+ "but ignored");
+ }
+
+ // If the target is a text control element, we won't handle user input
+ // for the `TextEditor` in it. However, we need to be open for `execCommand`.
+ // Therefore, we shouldn't set ancestor limit in this case.
+ // Note that we should do this once setting ancestor limiter for backward
+ // compatiblity of select events, etc. (Selection should be collapsed into
+ // the text control element.)
+ if (aAncestorLimit.HasIndependentSelection()) {
+ SelectionRef().SetAncestorLimiter(nullptr);
+ }
+}
+
+nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(
+ bool aIgnoreIfSelectionInEditingHost) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Element> editingHost = ComputeEditingHost(LimitInBodyElement::No);
+ if (NS_WARN_IF(!editingHost)) {
+ return NS_OK;
+ }
+
+ // If selection range is already in the editing host and the range is not
+ // start of the editing host, we shouldn't reset selection. E.g., window
+ // is activated when the editor had focus before inactivated.
+ if (aIgnoreIfSelectionInEditingHost && SelectionRef().RangeCount() == 1) {
+ const nsRange* range = SelectionRef().GetRangeAt(0);
+ if (!range->Collapsed() ||
+ range->GetStartContainer() != editingHost.get() ||
+ range->StartOffset()) {
+ return NS_OK;
+ }
+ }
+
+ for (nsIContent* leafContent = HTMLEditUtils::GetFirstLeafContent(
+ *editingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost);
+ leafContent;) {
+ // If we meet a non-editable node first, we should move caret to start
+ // of the container block or editing host.
+ if (!EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) {
+ MOZ_ASSERT(leafContent->GetParent());
+ MOZ_ASSERT(EditorUtils::IsEditableContent(*leafContent->GetParent(),
+ EditorType::HTML));
+ if (const Element* editableBlockElementOrInlineEditingHost =
+ HTMLEditUtils::GetAncestorElement(
+ *leafContent,
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ nsresult rv = CollapseSelectionTo(
+ EditorDOMPoint(editableBlockElementOrInlineEditingHost, 0));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ NS_WARNING("Found leaf content did not have editable parent, why?");
+ return NS_ERROR_FAILURE;
+ }
+
+ // When we meet an empty inline element, we should look for a next sibling.
+ // For example, if current editor is:
+ // <div contenteditable><span></span><b><br></b></div>
+ // then, we should put caret at the <br> element. So, let's check if found
+ // node is an empty inline container element.
+ if (Element* leafElement = Element::FromNode(leafContent)) {
+ if (HTMLEditUtils::IsInlineContent(
+ *leafElement, BlockInlineCheck::UseComputedDisplayStyle) &&
+ !HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafElement) &&
+ HTMLEditUtils::CanNodeContain(*leafElement,
+ *nsGkAtoms::textTagName)) {
+ // Chromium collapses selection to start of the editing host when this
+ // is the last leaf content. So, we don't need special handling here.
+ leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *leafElement, *editingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost);
+ continue;
+ }
+ }
+
+ if (Text* text = leafContent->GetAsText()) {
+ // If there is editable and visible text node, move caret at first of
+ // the visible character.
+ WSScanResult scanResultInTextNode =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ editingHost, EditorRawDOMPoint(text, 0),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if ((scanResultInTextNode.InVisibleOrCollapsibleCharacters() ||
+ scanResultInTextNode.ReachedPreformattedLineBreak()) &&
+ scanResultInTextNode.TextPtr() == text) {
+ nsresult rv = CollapseSelectionTo(
+ scanResultInTextNode.Point<EditorRawDOMPoint>());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ // If it's an invisible text node, keep scanning next leaf.
+ leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *leafContent, *editingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost);
+ continue;
+ }
+
+ // If there is editable <br> or something void element like <img>, <input>,
+ // <hr> etc, move caret before it.
+ if (!HTMLEditUtils::CanNodeContain(*leafContent, *nsGkAtoms::textTagName) ||
+ HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) {
+ MOZ_ASSERT(leafContent->GetParent());
+ if (EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) {
+ nsresult rv = CollapseSelectionTo(EditorDOMPoint(leafContent));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ MOZ_ASSERT_UNREACHABLE(
+ "How do we reach editable leaf in non-editable element?");
+ // But if it's not editable, let's put caret at start of editing host
+ // for now.
+ nsresult rv = CollapseSelectionTo(EditorDOMPoint(editingHost, 0));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+
+ // If we meet non-empty block element, we need to scan its child too.
+ if (HTMLEditUtils::IsBlockElement(
+ *leafContent, BlockInlineCheck::UseComputedDisplayStyle) &&
+ !HTMLEditUtils::IsEmptyNode(
+ *leafContent,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible}) &&
+ !HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) {
+ leafContent = HTMLEditUtils::GetFirstLeafContent(
+ *leafContent,
+ {LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost);
+ continue;
+ }
+
+ // Otherwise, we must meet an empty block element or a data node like
+ // comment node. Let's ignore it.
+ leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *leafContent, *editingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost);
+ }
+
+ // If there is no visible/editable node except another block element in
+ // current editing host, we should move caret to very first of the editing
+ // host.
+ // XXX This may not make sense, but Chromium behaves so. Therefore, the
+ // reason why we do this is just compatibility with Chromium.
+ nsresult rv = CollapseSelectionTo(EditorDOMPoint(editingHost, 0));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+bool HTMLEditor::ArePreservingSelection() const {
+ return IsEditActionDataAvailable() && SavedSelectionRef().RangeCount();
+}
+
+void HTMLEditor::PreserveSelectionAcrossActions() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ SavedSelectionRef().SaveSelection(SelectionRef());
+ RangeUpdaterRef().RegisterSelectionState(SavedSelectionRef());
+}
+
+nsresult HTMLEditor::RestorePreservedSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!SavedSelectionRef().RangeCount()) {
+ // XXX Returning error when it does not store is odd because no selection
+ // ranges is not illegal case in general.
+ return NS_ERROR_FAILURE;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ SavedSelectionRef().RestoreSelection(SelectionRef());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "SelectionState::RestoreSelection() failed, but ignored");
+ StopPreservingSelection();
+ return NS_OK;
+}
+
+void HTMLEditor::StopPreservingSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RangeUpdaterRef().DropSelectionState(SavedSelectionRef());
+ SavedSelectionRef().RemoveAllRanges();
+}
+
+void HTMLEditor::PreHandleMouseDown(const MouseEvent& aMouseDownEvent) {
+ if (mPendingStylesToApplyToNewContent) {
+ // mPendingStylesToApplyToNewContent will be notified of selection change
+ // even if aMouseDownEvent is not an acceptable event for this editor.
+ // Therefore, we need to notify it of this event too.
+ mPendingStylesToApplyToNewContent->PreHandleMouseEvent(aMouseDownEvent);
+ }
+}
+
+void HTMLEditor::PreHandleMouseUp(const MouseEvent& aMouseUpEvent) {
+ if (mPendingStylesToApplyToNewContent) {
+ // mPendingStylesToApplyToNewContent will be notified of selection change
+ // even if aMouseUpEvent is not an acceptable event for this editor.
+ // Therefore, we need to notify it of this event too.
+ mPendingStylesToApplyToNewContent->PreHandleMouseEvent(aMouseUpEvent);
+ }
+}
+
+void HTMLEditor::PreHandleSelectionChangeCommand(Command aCommand) {
+ if (mPendingStylesToApplyToNewContent) {
+ mPendingStylesToApplyToNewContent->PreHandleSelectionChangeCommand(
+ aCommand);
+ }
+}
+
+void HTMLEditor::PostHandleSelectionChangeCommand(Command aCommand) {
+ if (!mPendingStylesToApplyToNewContent) {
+ return;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (!editActionData.CanHandle()) {
+ return;
+ }
+ mPendingStylesToApplyToNewContent->PostHandleSelectionChangeCommand(*this,
+ aCommand);
+}
+
+nsresult HTMLEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) {
+ // NOTE: When you change this method, you should also change:
+ // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
+ if (NS_WARN_IF(!aKeyboardEvent)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (IsReadonly()) {
+ HandleKeyPressEventInReadOnlyMode(*aKeyboardEvent);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress,
+ "HandleKeyPressEvent gets non-keypress event");
+
+ switch (aKeyboardEvent->mKeyCode) {
+ case NS_VK_META:
+ case NS_VK_WIN:
+ case NS_VK_SHIFT:
+ case NS_VK_CONTROL:
+ case NS_VK_ALT:
+ // FYI: This shouldn't occur since modifier key shouldn't cause eKeyPress
+ // event.
+ aKeyboardEvent->PreventDefault();
+ return NS_OK;
+
+ case NS_VK_BACK:
+ case NS_VK_DELETE: {
+ nsresult rv = EditorBase::HandleKeyPressEvent(aKeyboardEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::HandleKeyPressEvent() failed");
+ return rv;
+ }
+ case NS_VK_TAB: {
+ // Basically, "Tab" key be used only for focus navigation.
+ // FYI: In web apps, this is always true.
+ if (IsTabbable()) {
+ return NS_OK;
+ }
+
+ // If we're in the plaintext mode, and not tabbable editor, let's
+ // insert a horizontal tabulation.
+ if (IsPlaintextMailComposer()) {
+ if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() ||
+ aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta()) {
+ return NS_OK;
+ }
+
+ // else we insert the tab straight through
+ aKeyboardEvent->PreventDefault();
+ nsresult rv = OnInputText(u"\t"_ns);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::OnInputText(\\t) failed");
+ return rv;
+ }
+
+ // Otherwise, e.g., we're an embedding editor in chrome, we can handle
+ // "Tab" key as an input.
+ if (aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() ||
+ aKeyboardEvent->IsMeta()) {
+ return NS_OK;
+ }
+
+ RefPtr<Selection> selection = GetSelection();
+ if (NS_WARN_IF(!selection) || NS_WARN_IF(!selection->RangeCount())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsINode* startContainer = selection->GetRangeAt(0)->GetStartContainer();
+ MOZ_ASSERT(startContainer);
+ if (!startContainer->IsContent()) {
+ break;
+ }
+
+ const Element* editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *startContainer->AsContent(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (!editableBlockElement) {
+ break;
+ }
+
+ // If selection is in a table element, we need special handling.
+ if (HTMLEditUtils::IsAnyTableElement(editableBlockElement)) {
+ Result<EditActionResult, nsresult> result =
+ HandleTabKeyPressInTable(aKeyboardEvent);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::HandleTabKeyPressInTable() failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ if (!result.inspect().Handled()) {
+ return NS_OK;
+ }
+ nsresult rv = ScrollSelectionFocusIntoView();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::ScrollSelectionFocusIntoView() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // If selection is in an list item element, treat it as indent or outdent.
+ if (HTMLEditUtils::IsListItem(editableBlockElement)) {
+ aKeyboardEvent->PreventDefault();
+ if (!aKeyboardEvent->IsShift()) {
+ nsresult rv = IndentAsAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::IndentAsAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ nsresult rv = OutdentAsAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::OutdentAsAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // If only "Tab" key is pressed in normal context, just treat it as
+ // horizontal tab character input.
+ if (aKeyboardEvent->IsShift()) {
+ return NS_OK;
+ }
+ aKeyboardEvent->PreventDefault();
+ nsresult rv = OnInputText(u"\t"_ns);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::OnInputText(\\t) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ case NS_VK_RETURN:
+ if (!aKeyboardEvent->IsInputtingLineBreak()) {
+ return NS_OK;
+ }
+ aKeyboardEvent->PreventDefault(); // consumed
+ if (aKeyboardEvent->IsShift()) {
+ // Only inserts a <br> element.
+ nsresult rv = InsertLineBreakAsAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertLineBreakAsAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ // uses rules to figure out what to insert
+ nsresult rv = InsertParagraphSeparatorAsAction();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertParagraphSeparatorAsAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (!aKeyboardEvent->IsInputtingText()) {
+ // we don't PreventDefault() here or keybindings like control-x won't work
+ return NS_OK;
+ }
+ aKeyboardEvent->PreventDefault();
+ // If we dispatch 2 keypress events for a surrogate pair and we set only
+ // first `.key` value to the surrogate pair, the preceding one has it and the
+ // other has empty string. In this case, we should handle only the first one
+ // with the key value.
+ if (!StaticPrefs::dom_event_keypress_dispatch_once_per_surrogate_pair() &&
+ !StaticPrefs::dom_event_keypress_key_allow_lone_surrogate() &&
+ aKeyboardEvent->mKeyValue.IsEmpty() &&
+ IS_SURROGATE(aKeyboardEvent->mCharCode)) {
+ return NS_OK;
+ }
+ nsAutoString str(aKeyboardEvent->mKeyValue);
+ if (str.IsEmpty()) {
+ str.Assign(static_cast<char16_t>(aKeyboardEvent->mCharCode));
+ }
+ // FYI: DIfferent from TextEditor, we can treat \r (CR) as-is in HTMLEditor.
+ nsresult rv = OnInputText(str);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::NodeIsBlock(nsINode* aNode, bool* aIsBlock) {
+ if (NS_WARN_IF(!aNode)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (MOZ_UNLIKELY(!aNode->IsElement())) {
+ *aIsBlock = false;
+ return NS_OK;
+ }
+ // If the node is in composed doc, we'll refer its style. If we don't flush
+ // pending style here, another API call may change the style. Therefore,
+ // let's flush the pending style changes right now.
+ if (aNode->IsInComposedDoc()) {
+ if (RefPtr<PresShell> presShell = GetPresShell()) {
+ presShell->FlushPendingNotifications(FlushType::Style);
+ }
+ }
+ *aIsBlock = HTMLEditUtils::IsBlockElement(
+ *aNode->AsElement(), BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::UpdateBaseURL() {
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Look for an HTML <base> tag
+ RefPtr<nsContentList> baseElementList =
+ document->GetElementsByTagName(u"base"_ns);
+
+ // If no base tag, then set baseURL to the document's URL. This is very
+ // important, else relative URLs for links and images are wrong
+ if (!baseElementList || !baseElementList->Item(0)) {
+ document->SetBaseURI(document->GetDocumentURI());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::InsertLineBreak() {
+ // XPCOM method's InsertLineBreak() should insert paragraph separator in
+ // HTMLEditor.
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eInsertParagraphSeparator);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ Result<EditActionResult, nsresult> result =
+ InsertParagraphSeparatorAsSubAction(*editingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::InsertParagraphSeparatorAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::InsertLineBreakAsAction(nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLineBreak,
+ aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (IsSelectionRangeContainerNotContent()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ rv = InsertLineBreakAsSubAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertLineBreakAsSubAction() failed");
+ // Don't return NS_SUCCESS_DOM_NO_OPERATION for compatibility of `execCommand`
+ // result of Chrome.
+ return NS_FAILED(rv) ? rv : NS_OK;
+}
+
+nsresult HTMLEditor::InsertParagraphSeparatorAsAction(
+ nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eInsertParagraphSeparator, aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ Result<EditActionResult, nsresult> result =
+ InsertParagraphSeparatorAsSubAction(*editingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::InsertParagraphSeparatorAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::HandleTabKeyPressInTable(
+ WidgetKeyboardEvent* aKeyboardEvent) {
+ MOZ_ASSERT(aKeyboardEvent);
+
+ AutoEditActionDataSetter dummyEditActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!dummyEditActionData.CanHandle())) {
+ // Do nothing if we didn't find a table cell.
+ return EditActionResult::IgnoredResult();
+ }
+
+ // Find enclosing table cell from selection (cell may be selected element)
+ const RefPtr<Element> cellElement =
+ GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td);
+ if (!cellElement) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td) "
+ "returned nullptr");
+ // Do nothing if we didn't find a table cell.
+ return EditActionResult::IgnoredResult();
+ }
+
+ // find enclosing table
+ RefPtr<Element> table =
+ HTMLEditUtils::GetClosestAncestorTableElement(*cellElement);
+ if (!table) {
+ NS_WARNING("HTMLEditor::GetClosestAncestorTableElement() failed");
+ return EditActionResult::IgnoredResult();
+ }
+
+ // advance to next cell
+ // first create an iterator over the table
+ PostContentIterator postOrderIter;
+ nsresult rv = postOrderIter.Init(table);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("PostContentIterator::Init() failed");
+ return Err(rv);
+ }
+ // position postOrderIter at block
+ rv = postOrderIter.PositionAt(cellElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("PostContentIterator::PositionAt() failed");
+ return Err(rv);
+ }
+
+ do {
+ if (aKeyboardEvent->IsShift()) {
+ postOrderIter.Prev();
+ } else {
+ postOrderIter.Next();
+ }
+
+ nsCOMPtr<nsINode> node = postOrderIter.GetCurrentNode();
+ if (node && HTMLEditUtils::IsTableCell(node) &&
+ HTMLEditUtils::GetClosestAncestorTableElement(*node->AsElement()) ==
+ table) {
+ aKeyboardEvent->PreventDefault();
+ CollapseSelectionToDeepestNonTableFirstChild(node);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ return EditActionResult::HandledResult();
+ }
+ } while (!postOrderIter.IsDone());
+
+ if (aKeyboardEvent->IsShift()) {
+ return EditActionResult::IgnoredResult();
+ }
+
+ // If we haven't handled it yet, then we must have run off the end of the
+ // table. Insert a new row.
+ // XXX We should investigate whether this behavior is supported by other
+ // browsers later.
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertTableRowElement);
+ rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return Err(rv);
+ }
+ rv = InsertTableRowsWithTransaction(*cellElement, 1,
+ InsertPosition::eAfterSelectedCell);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::InsertTableRowsWithTransaction(*cellElement, 1, "
+ "InsertPosition::eAfterSelectedCell) failed");
+ return Err(rv);
+ }
+ aKeyboardEvent->PreventDefault();
+ // Put selection in right place. Use table code to get selection and index
+ // to new row...
+ RefPtr<Element> tblElement, cell;
+ int32_t row;
+ rv = GetCellContext(getter_AddRefs(tblElement), getter_AddRefs(cell), nullptr,
+ nullptr, &row, nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return Err(rv);
+ }
+ if (!tblElement) {
+ NS_WARNING("HTMLEditor::GetCellContext() didn't return table element");
+ return Err(NS_ERROR_FAILURE);
+ }
+ // ...so that we can ask for first cell in that row...
+ cell = GetTableCellElementAt(*tblElement, row, 0);
+ // ...and then set selection there. (Note that normally you should use
+ // CollapseSelectionToDeepestNonTableFirstChild(), but we know cell is an
+ // empty new cell, so this works fine)
+ if (cell) {
+ nsresult rv = CollapseSelectionToStartOf(*cell);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionToStartOf() failed");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ }
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ return EditActionResult::HandledResult();
+}
+
+void HTMLEditor::CollapseSelectionToDeepestNonTableFirstChild(nsINode* aNode) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ MOZ_ASSERT(aNode);
+
+ nsCOMPtr<nsINode> node = aNode;
+
+ for (nsIContent* child = node->GetFirstChild(); child;
+ child = child->GetFirstChild()) {
+ // Stop if we find a table, don't want to go into nested tables
+ if (HTMLEditUtils::IsTable(child) ||
+ !HTMLEditUtils::IsContainerNode(*child)) {
+ break;
+ }
+ node = child;
+ }
+
+ DebugOnly<nsresult> rvIgnored = CollapseSelectionToStartOf(*node);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+}
+
+nsresult HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction(
+ const nsAString& aSourceToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // don't do any post processing, rules get confused
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eReplaceHeadWithHTMLSource, nsIEditor::eNone,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ CommitComposition();
+
+ // Do not use AutoEditSubActionNotifier -- rules code won't let us insert in
+ // <head>. Use the head node as a parent and delete/insert directly.
+ // XXX We're using AutoEditSubActionNotifier above...
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ RefPtr<nsContentList> headElementList =
+ document->GetElementsByTagName(u"head"_ns);
+ if (NS_WARN_IF(!headElementList)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Element> primaryHeadElement = headElementList->Item(0)->AsElement();
+ if (NS_WARN_IF(!primaryHeadElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // First, make sure there are no return chars in the source. Bad things
+ // happen if you insert returns (instead of dom newlines, \n) into an editor
+ // document.
+ nsAutoString inputString(aSourceToInsert);
+
+ // Windows linebreaks: Map CRLF to LF:
+ inputString.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
+
+ // Mac linebreaks: Map any remaining CR to LF:
+ inputString.ReplaceSubstring(u"\r"_ns, u"\n"_ns);
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ // Get the first range in the selection, for context:
+ RefPtr<const nsRange> range = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult error;
+ RefPtr<DocumentFragment> documentFragment =
+ range->CreateContextualFragment(inputString, error);
+
+ // XXXX BUG 50965: This is not returning the text between <title>...</title>
+ // Special code is needed in JS to handle title anyway, so it doesn't matter!
+
+ if (error.Failed()) {
+ NS_WARNING("nsRange::CreateContextualFragment() failed");
+ return error.StealNSResult();
+ }
+ if (NS_WARN_IF(!documentFragment)) {
+ NS_WARNING(
+ "nsRange::CreateContextualFragment() didn't create DocumentFragment");
+ return NS_ERROR_FAILURE;
+ }
+
+ // First delete all children in head
+ while (nsCOMPtr<nsIContent> child = primaryHeadElement->GetFirstChild()) {
+ nsresult rv = DeleteNodeWithTransaction(*child);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+
+ // Now insert the new nodes
+ int32_t offsetOfNewNode = 0;
+
+ // Loop over the contents of the fragment and move into the document
+ while (nsCOMPtr<nsIContent> child = documentFragment->GetFirstChild()) {
+ Result<CreateContentResult, nsresult> insertChildContentResult =
+ InsertNodeWithTransaction(
+ *child, EditorDOMPoint(primaryHeadElement, offsetOfNewNode++));
+ if (MOZ_UNLIKELY(insertChildContentResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertChildContentResult.unwrapErr();
+ }
+ // We probably don't need to adjust selection here, although we've done it
+ // unless AutoTransactionsConserveSelection is created in a caller.
+ insertChildContentResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::RebuildDocumentFromSource(
+ const nsAString& aSourceString) {
+ CommitComposition();
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetHTML);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Element> rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // Find where the <body> tag starts.
+ nsReadingIterator<char16_t> beginbody;
+ nsReadingIterator<char16_t> endbody;
+ aSourceString.BeginReading(beginbody);
+ aSourceString.EndReading(endbody);
+ bool foundbody =
+ CaseInsensitiveFindInReadable(u"<body"_ns, beginbody, endbody);
+
+ nsReadingIterator<char16_t> beginhead;
+ nsReadingIterator<char16_t> endhead;
+ aSourceString.BeginReading(beginhead);
+ aSourceString.EndReading(endhead);
+ bool foundhead =
+ CaseInsensitiveFindInReadable(u"<head"_ns, beginhead, endhead);
+ // a valid head appears before the body
+ if (foundbody && beginhead.get() > beginbody.get()) {
+ foundhead = false;
+ }
+
+ nsReadingIterator<char16_t> beginclosehead;
+ nsReadingIterator<char16_t> endclosehead;
+ aSourceString.BeginReading(beginclosehead);
+ aSourceString.EndReading(endclosehead);
+
+ // Find the index after "<head>"
+ bool foundclosehead = CaseInsensitiveFindInReadable(
+ u"</head>"_ns, beginclosehead, endclosehead);
+ // a valid close head appears after a found head
+ if (foundhead && beginhead.get() > beginclosehead.get()) {
+ foundclosehead = false;
+ }
+ // a valid close head appears before a found body
+ if (foundbody && beginclosehead.get() > beginbody.get()) {
+ foundclosehead = false;
+ }
+
+ // Time to change the document
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ nsReadingIterator<char16_t> endtotal;
+ aSourceString.EndReading(endtotal);
+
+ if (foundhead) {
+ if (foundclosehead) {
+ nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
+ Substring(beginhead, beginclosehead));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
+ "failed");
+ return rv;
+ }
+ } else if (foundbody) {
+ nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
+ Substring(beginhead, beginbody));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
+ "failed");
+ return rv;
+ }
+ } else {
+ // XXX Without recourse to some parser/content sink/docshell hackery we
+ // don't really know where the head ends and the body begins so we assume
+ // that there is no body
+ nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
+ Substring(beginhead, endtotal));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
+ "failed");
+ return rv;
+ }
+ }
+ } else {
+ nsReadingIterator<char16_t> begintotal;
+ aSourceString.BeginReading(begintotal);
+ constexpr auto head = u"<head>"_ns;
+ if (foundclosehead) {
+ nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
+ head + Substring(begintotal, beginclosehead));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
+ "failed");
+ return rv;
+ }
+ } else if (foundbody) {
+ nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(
+ head + Substring(begintotal, beginbody));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
+ "failed");
+ return rv;
+ }
+ } else {
+ // XXX Without recourse to some parser/content sink/docshell hackery we
+ // don't really know where the head ends and the body begins so we assume
+ // that there is no head
+ nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(head);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() "
+ "failed");
+ return rv;
+ }
+ }
+ }
+
+ rv = SelectAll();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::SelectAll() failed");
+ return rv;
+ }
+
+ if (!foundbody) {
+ constexpr auto body = u"<body>"_ns;
+ // XXX Without recourse to some parser/content sink/docshell hackery we
+ // don't really know where the head ends and the body begins
+ if (foundclosehead) {
+ // assume body starts after the head ends
+ nsresult rv = LoadHTML(body + Substring(endclosehead, endtotal));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::LoadHTML() failed");
+ return rv;
+ }
+ } else if (foundhead) {
+ // assume there is no body
+ nsresult rv = LoadHTML(body);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::LoadHTML() failed");
+ return rv;
+ }
+ } else {
+ // assume there is no head, the entire source is body
+ nsresult rv = LoadHTML(body + aSourceString);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::LoadHTML() failed");
+ return rv;
+ }
+ }
+
+ RefPtr<Element> divElement = CreateElementWithDefaults(*nsGkAtoms::div);
+ if (!divElement) {
+ NS_WARNING(
+ "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::div) failed");
+ return NS_ERROR_FAILURE;
+ }
+ CloneAttributesWithTransaction(*rootElement, *divElement);
+
+ nsresult rv = MaybeCollapseSelectionAtFirstEditableNode(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed");
+ return rv;
+ }
+
+ rv = LoadHTML(Substring(beginbody, endtotal));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::LoadHTML() failed");
+ return rv;
+ }
+
+ // Now we must copy attributes user might have edited on the <body> tag
+ // because InsertHTML (actually, CreateContextualFragment()) will never
+ // return a body node in the DOM fragment
+
+ // We already know where "<body" begins
+ nsReadingIterator<char16_t> beginclosebody = beginbody;
+ nsReadingIterator<char16_t> endclosebody;
+ aSourceString.EndReading(endclosebody);
+ if (!FindInReadable(u">"_ns, beginclosebody, endclosebody)) {
+ NS_WARNING("'>' was not found");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Truncate at the end of the body tag. Kludge of the year: fool the parser
+ // by replacing "body" with "div" so we get a node
+ nsAutoString bodyTag;
+ bodyTag.AssignLiteral("<div ");
+ bodyTag.Append(Substring(endbody, endclosebody));
+
+ RefPtr<const nsRange> range = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult error;
+ RefPtr<DocumentFragment> documentFragment =
+ range->CreateContextualFragment(bodyTag, error);
+ if (error.Failed()) {
+ NS_WARNING("nsRange::CreateContextualFragment() failed");
+ return error.StealNSResult();
+ }
+ if (!documentFragment) {
+ NS_WARNING(
+ "nsRange::CreateContextualFragment() didn't create DocumentFagement");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIContent> firstChild = documentFragment->GetFirstChild();
+ if (!firstChild || !firstChild->IsElement()) {
+ NS_WARNING("First child of DocumentFragment was not an Element node");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Copy all attributes from the div child to current body element
+ CloneAttributesWithTransaction(*rootElement,
+ MOZ_KnownLive(*firstChild->AsElement()));
+
+ // place selection at first editable content
+ rv = MaybeCollapseSelectionAtFirstEditableNode(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::InsertElementAtSelection(Element* aElement,
+ bool aDeleteSelection) {
+ nsresult rv = InsertElementAtSelectionAsAction(aElement, aDeleteSelection);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertElementAtSelectionAsAction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::InsertElementAtSelectionAsAction(
+ Element* aElement, bool aDeleteSelection, nsIPrincipal* aPrincipal) {
+ if (NS_WARN_IF(!aElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (IsReadonly()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(
+ *this, HTMLEditUtils::GetEditActionForInsert(*aElement), aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ DebugOnly<nsresult> rvIgnored = CommitComposition();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CommitComposition() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertElement, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ if (aDeleteSelection) {
+ if (!HTMLEditUtils::IsBlockElement(
+ *aElement, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ // E.g., inserting an image. In this case we don't need to delete any
+ // inline wrappers before we do the insertion. Otherwise we let
+ // DeleteSelectionAndPrepareToCreateNode do the deletion for us, which
+ // calls DeleteSelection with aStripWrappers = eStrip.
+ nsresult rv = DeleteSelectionAsSubAction(eNone, eNoStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eNoStrip) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+
+ nsresult rv = DeleteSelectionAndPrepareToCreateNode();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteSelectionAndPrepareToCreateNode() failed");
+ return rv;
+ }
+ }
+ // If deleting, selection will be collapsed.
+ // so if not, we collapse it
+ else {
+ // Named Anchor is a special case,
+ // We collapse to insert element BEFORE the selection
+ // For all other tags, we insert AFTER the selection
+ if (HTMLEditUtils::IsNamedAnchor(aElement)) {
+ IgnoredErrorResult ignoredError;
+ SelectionRef().CollapseToStart(ignoredError);
+ if (NS_WARN_IF(Destroyed())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "Selection::CollapseToStart() failed, but ignored");
+ } else {
+ IgnoredErrorResult ignoredError;
+ SelectionRef().CollapseToEnd(ignoredError);
+ if (NS_WARN_IF(Destroyed())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "Selection::CollapseToEnd() failed, but ignored");
+ }
+ }
+
+ if (!SelectionRef().GetAnchorNode()) {
+ return NS_OK;
+ }
+
+ Element* editingHost = ComputeEditingHost(LimitInBodyElement::No);
+ if (NS_WARN_IF(!editingHost)) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_FAILURE);
+ }
+
+ EditorRawDOMPoint atAnchor(SelectionRef().AnchorRef());
+ // Adjust position based on the node we are going to insert.
+ EditorDOMPoint pointToInsert =
+ HTMLEditUtils::GetBetterInsertionPointFor<EditorDOMPoint>(
+ *aElement, atAnchor, *editingHost);
+ if (!pointToInsert.IsSet()) {
+ NS_WARNING("HTMLEditUtils::GetBetterInsertionPointFor() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ {
+ Result<CreateElementResult, nsresult> insertElementResult =
+ InsertNodeIntoProperAncestorWithTransaction<Element>(
+ *aElement, pointToInsert,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(insertElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction("
+ "SplitAtEdges::eAllowToCreateEmptyContainer) failed");
+ return EditorBase::ToGenericNSResult(insertElementResult.unwrapErr());
+ }
+ insertElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+ // Set caret after element, but check for special case
+ // of inserting table-related elements: set in first cell instead
+ if (!SetCaretInTableCell(aElement)) {
+ if (NS_WARN_IF(Destroyed())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ nsresult rv = CollapseSelectionTo(EditorRawDOMPoint::After(*aElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::CollapseSelectionTo() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+
+ // check for inserting a whole table at the end of a block. If so insert
+ // a br after it.
+ if (!HTMLEditUtils::IsTable(aElement) ||
+ !HTMLEditUtils::IsLastChild(*aElement,
+ {WalkTreeOption::IgnoreNonEditableNode})) {
+ return NS_OK;
+ }
+
+ const auto afterElement = EditorDOMPoint::After(*aElement);
+ // Collapse selection to the new `<br>` element node after creating it.
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, afterElement, ePrevious);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes, ePrevious) failed");
+ return EditorBase::ToGenericNSResult(insertBRElementResult.unwrapErr());
+ }
+ rv = insertBRElementResult.inspect().SuggestCaretPointTo(*this, {});
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+template <typename NodeType>
+Result<CreateNodeResultBase<NodeType>, nsresult>
+HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(
+ NodeType& aContentToInsert, const EditorDOMPoint& aPointToInsert,
+ SplitAtEdges aSplitAtEdges) {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValidInComposedDoc());
+ if (NS_WARN_IF(!aPointToInsert.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ if (aContentToInsert.NodeType() == nsINode::DOCUMENT_TYPE_NODE ||
+ aContentToInsert.NodeType() == nsINode::PROCESSING_INSTRUCTION_NODE) {
+ return CreateNodeResultBase<NodeType>::NotHandled();
+ }
+
+ // Search up the parent chain to find a suitable container.
+ EditorDOMPoint pointToInsert(aPointToInsert);
+ MOZ_ASSERT(pointToInsert.IsSet());
+ while (!HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(),
+ aContentToInsert)) {
+ // If the current parent is a root (body or table element)
+ // then go no further - we can't insert.
+ if (MOZ_UNLIKELY(
+ pointToInsert.IsContainerHTMLElement(nsGkAtoms::body) ||
+ HTMLEditUtils::IsAnyTableElement(pointToInsert.GetContainer()))) {
+ NS_WARNING(
+ "There was no proper container element to insert the content node in "
+ "the document");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Get the next point.
+ pointToInsert = pointToInsert.ParentPoint();
+
+ if (MOZ_UNLIKELY(
+ !pointToInsert.IsInContentNode() ||
+ !EditorUtils::IsEditableContent(
+ *pointToInsert.ContainerAs<nsIContent>(), EditorType::HTML))) {
+ NS_WARNING(
+ "There was no proper container element to insert the content node in "
+ "the editing host");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ if (pointToInsert != aPointToInsert) {
+ // We need to split some levels above the original selection parent.
+ MOZ_ASSERT(pointToInsert.GetChild());
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitNodeDeepWithTransaction(MOZ_KnownLive(*pointToInsert.GetChild()),
+ aPointToInsert, aSplitAtEdges);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitNodeResult.propagateErr();
+ }
+ pointToInsert =
+ splitNodeResult.inspect().template AtSplitPoint<EditorDOMPoint>();
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ // Caret should be set by the caller of this method so that we don't
+ // need to handle it here.
+ splitNodeResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ // Now we can insert the new node.
+ Result<CreateNodeResultBase<NodeType>, nsresult> insertContentNodeResult =
+ InsertNodeWithTransaction<NodeType>(aContentToInsert, pointToInsert);
+ if (MOZ_LIKELY(insertContentNodeResult.isOk()) &&
+ MOZ_UNLIKELY(NS_WARN_IF(!aContentToInsert.GetParentNode()) ||
+ NS_WARN_IF(aContentToInsert.GetParentNode() !=
+ pointToInsert.GetContainer()))) {
+ NS_WARNING(
+ "EditorBase::InsertNodeWithTransaction() succeeded, but the inserted "
+ "node was moved or removed by the web app");
+ insertContentNodeResult.inspect().IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ NS_WARNING_ASSERTION(insertContentNodeResult.isOk(),
+ "EditorBase::InsertNodeWithTransaction() failed");
+ return insertContentNodeResult;
+}
+
+NS_IMETHODIMP HTMLEditor::SelectElement(Element* aElement) {
+ if (NS_WARN_IF(!aElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = SelectContentInternal(MOZ_KnownLive(*aElement));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SelectContentInternal() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SelectContentInternal(nsIContent& aContentToSelect) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Must be sure that element is contained in the editing host
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost) ||
+ NS_WARN_IF(!aContentToSelect.IsInclusiveDescendantOf(editingHost))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorRawDOMPoint newSelectionStart(&aContentToSelect);
+ if (NS_WARN_IF(!newSelectionStart.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ EditorRawDOMPoint newSelectionEnd(EditorRawDOMPoint::After(aContentToSelect));
+ MOZ_ASSERT(newSelectionEnd.IsSet());
+ ErrorResult error;
+ SelectionRef().SetStartAndEndInLimiter(newSelectionStart, newSelectionEnd,
+ error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "Selection::SetStartAndEndInLimiter() failed");
+ return error.StealNSResult();
+}
+
+nsresult HTMLEditor::AppendContentToSelectionAsRange(nsIContent& aContent) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ EditorRawDOMPoint atContent(&aContent);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(
+ atContent.ToRawRangeBoundary(),
+ atContent.NextPoint().ToRawRangeBoundary(), IgnoreErrors());
+ if (NS_WARN_IF(!range)) {
+ NS_WARNING("nsRange::Create() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult error;
+ SelectionRef().AddRangeAndSelectFramesAndNotifyListeners(*range, error);
+ if (NS_WARN_IF(Destroyed())) {
+ if (error.Failed()) {
+ error.SuppressException();
+ }
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(!error.Failed(), "Failed to add range to Selection");
+ return error.StealNSResult();
+}
+
+nsresult HTMLEditor::ClearSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ ErrorResult error;
+ SelectionRef().RemoveAllRanges(error);
+ if (NS_WARN_IF(Destroyed())) {
+ if (error.Failed()) {
+ error.SuppressException();
+ }
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(!error.Failed(), "Selection::RemoveAllRanges() failed");
+ return error.StealNSResult();
+}
+
+nsresult HTMLEditor::FormatBlockAsAction(const nsAString& aParagraphFormat,
+ nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eInsertBlockElement, aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (NS_WARN_IF(aParagraphFormat.IsEmpty())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<nsAtom> tagName = NS_Atomize(aParagraphFormat);
+ MOZ_ASSERT(tagName);
+ if (NS_WARN_IF(!tagName->IsStatic()) ||
+ NS_WARN_IF(!HTMLEditUtils::IsFormatTagForFormatBlockCommand(
+ *tagName->AsStatic()))) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (tagName == nsGkAtoms::dd || tagName == nsGkAtoms::dt) {
+ // MOZ_KnownLive(tagName->AsStatic()) because nsStaticAtom instances live
+ // while the process is running.
+ Result<EditActionResult, nsresult> result =
+ MakeOrChangeListAndListItemAsSubAction(
+ MOZ_KnownLive(*tagName->AsStatic()), u""_ns,
+ SelectAllOfCurrentList::No);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MakeOrChangeListAndListItemAsSubAction("
+ "SelectAllOfCurrentList::No) failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ return NS_OK;
+ }
+
+ rv = FormatBlockContainerAsSubAction(MOZ_KnownLive(*tagName->AsStatic()),
+ FormatBlockMode::HTMLFormatBlockCommand);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::FormatBlockContainerAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::SetParagraphStateAsAction(
+ const nsAString& aParagraphFormat, nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eInsertBlockElement, aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // TODO: Computing the editing host here makes the `execCommand` in
+ // docshell/base/crashtests/file_432114-2.xhtml cannot run
+ // `DOMNodeRemoved` event listener with deleting the bogus <br> element.
+ // So that it should be rewritten with different mutation event listener
+ // since we'd like to stop using it.
+
+ nsAutoString lowerCaseTagName(aParagraphFormat);
+ ToLowerCase(lowerCaseTagName);
+ RefPtr<nsAtom> tagName = NS_Atomize(lowerCaseTagName);
+ MOZ_ASSERT(tagName);
+ if (NS_WARN_IF(!tagName->IsStatic())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (tagName == nsGkAtoms::dd || tagName == nsGkAtoms::dt) {
+ // MOZ_KnownLive(tagName->AsStatic()) because nsStaticAtom instances live
+ // while the process is running.
+ Result<EditActionResult, nsresult> result =
+ MakeOrChangeListAndListItemAsSubAction(
+ MOZ_KnownLive(*tagName->AsStatic()), u""_ns,
+ SelectAllOfCurrentList::No);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MakeOrChangeListAndListItemAsSubAction("
+ "SelectAllOfCurrentList::No) failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ return NS_OK;
+ }
+
+ rv = FormatBlockContainerAsSubAction(
+ MOZ_KnownLive(*tagName->AsStatic()),
+ FormatBlockMode::XULParagraphStateCommand);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::FormatBlockContainerAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+// static
+bool HTMLEditor::IsFormatElement(FormatBlockMode aFormatBlockMode,
+ const nsIContent& aContent) {
+ // FYI: Optimize for HTML command because it may run too many times.
+ return MOZ_LIKELY(aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand)
+ ? HTMLEditUtils::IsFormatElementForFormatBlockCommand(aContent)
+ : (HTMLEditUtils::IsFormatElementForParagraphStateCommand(
+ aContent) &&
+ // XXX The XUL paragraph state command treats <dl>, <dd> and
+ // <dt> elements but all handlers do not treat them as a format
+ // node. Therefore, we keep the traditional behavior here.
+ !aContent.IsAnyOfHTMLElements(nsGkAtoms::dd, nsGkAtoms::dl,
+ nsGkAtoms::dt));
+}
+
+NS_IMETHODIMP HTMLEditor::GetParagraphState(bool* aMixed,
+ nsAString& aFirstParagraphState) {
+ if (NS_WARN_IF(!aMixed)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (!mInitSucceeded) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ ErrorResult error;
+ ParagraphStateAtSelection paragraphState(
+ *this, FormatBlockMode::XULParagraphStateCommand, error);
+ if (error.Failed()) {
+ NS_WARNING("ParagraphStateAtSelection failed");
+ return error.StealNSResult();
+ }
+
+ *aMixed = paragraphState.IsMixed();
+ if (NS_WARN_IF(!paragraphState.GetFirstParagraphStateAtSelection())) {
+ // XXX Odd result, but keep this behavior for now...
+ aFirstParagraphState.AssignASCII("x");
+ } else {
+ paragraphState.GetFirstParagraphStateAtSelection()->ToString(
+ aFirstParagraphState);
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::GetBackgroundColorState(bool* aMixed,
+ nsAString& aOutColor) {
+ if (NS_WARN_IF(!aMixed)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (IsCSSEnabled()) {
+ // if we are in CSS mode, we have to check if the containing block defines
+ // a background color
+ nsresult rv = GetCSSBackgroundColorState(aMixed, aOutColor, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetCSSBackgroundColorState() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ // in HTML mode, we look only at page's background
+ nsresult rv = GetHTMLBackgroundColorState(aMixed, aOutColor);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetCSSBackgroundColorState() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::GetHighlightColorState(bool* aMixed,
+ nsAString& aOutColor) {
+ if (NS_WARN_IF(!aMixed)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aMixed = false;
+ aOutColor.AssignLiteral("transparent");
+ if (!IsCSSEnabled()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // in CSS mode, text background can be added by the Text Highlight button
+ // we need to query the background of the selection without looking for
+ // the block container of the ranges in the selection
+ nsresult rv = GetCSSBackgroundColorState(aMixed, aOutColor, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetCSSBackgroundColorState() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::GetCSSBackgroundColorState(bool* aMixed,
+ nsAString& aOutColor,
+ bool aBlockLevel) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aMixed)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aMixed = false;
+ // the default background color is transparent
+ aOutColor.AssignLiteral("transparent");
+
+ RefPtr<const nsRange> firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsINode> startContainer = firstRange->GetStartContainer();
+ if (NS_WARN_IF(!startContainer) || NS_WARN_IF(!startContainer->IsContent())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // is the selection collapsed?
+ nsIContent* contentToExamine;
+ if (SelectionRef().IsCollapsed() || startContainer->IsText()) {
+ if (NS_WARN_IF(!startContainer->IsContent())) {
+ return NS_ERROR_FAILURE;
+ }
+ // we want to look at the startContainer and ancestors
+ contentToExamine = startContainer->AsContent();
+ } else {
+ // otherwise we want to look at the first editable node after
+ // {startContainer,offset} and its ancestors for divs with alignment on them
+ contentToExamine = firstRange->GetChildAtStartOffset();
+ // GetNextNode(startContainer, offset, true, address_of(contentToExamine));
+ }
+
+ if (NS_WARN_IF(!contentToExamine)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aBlockLevel) {
+ // we are querying the block background (and not the text background), let's
+ // climb to the block container. Note that background color of ancestor
+ // of editing host may be what the caller wants to know. Therefore, we
+ // should ignore the editing host boundaries.
+ Element* const closestBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *contentToExamine, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (NS_WARN_IF(!closestBlockElement)) {
+ return NS_OK;
+ }
+
+ for (RefPtr<Element> blockElement = closestBlockElement; blockElement;) {
+ RefPtr<Element> nextBlockElement = HTMLEditUtils::GetAncestorElement(
+ *blockElement, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetComputedProperty(
+ *blockElement, *nsGkAtoms::backgroundColor, aOutColor);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (MayHaveMutationEventListeners() &&
+ NS_WARN_IF(nextBlockElement !=
+ HTMLEditUtils::GetAncestorElement(
+ *blockElement, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle))) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::"
+ "backgroundColor) failed, but ignored");
+ // look at parent if the queried color is transparent and if the node to
+ // examine is not the root of the document
+ if (!aOutColor.EqualsLiteral("transparent") &&
+ !aOutColor.EqualsLiteral("rgba(0, 0, 0, 0)")) {
+ break;
+ }
+ blockElement = std::move(nextBlockElement);
+ }
+
+ if (aOutColor.EqualsLiteral("transparent") ||
+ aOutColor.EqualsLiteral("rgba(0, 0, 0, 0)")) {
+ // we have hit the root of the document and the color is still transparent
+ // ! Grumble... Let's look at the default background color because that's
+ // the color we are looking for
+ CSSEditUtils::GetDefaultBackgroundColor(aOutColor);
+ }
+ } else {
+ // no, we are querying the text background for the Text Highlight button
+ if (contentToExamine->IsText()) {
+ // if the node of interest is a text node, let's climb a level
+ contentToExamine = contentToExamine->GetParent();
+ }
+ // Return default value due to no parent node
+ if (!contentToExamine) {
+ return NS_OK;
+ }
+
+ for (RefPtr<Element> element =
+ contentToExamine->GetAsElementOrParentElement();
+ element; element = element->GetParentElement()) {
+ // is the node to examine a block ?
+ if (HTMLEditUtils::IsBlockElement(
+ *element, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ // yes it is a block; in that case, the text background color is
+ // transparent
+ aOutColor.AssignLiteral("transparent");
+ break;
+ }
+
+ // no, it's not; let's retrieve the computed style of background-color
+ // for the node to examine
+ nsCOMPtr<nsINode> parentNode = element->GetParentNode();
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetComputedProperty(
+ *element, *nsGkAtoms::backgroundColor, aOutColor);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_WARN_IF(parentNode != element->GetParentNode())) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::"
+ "backgroundColor) failed, but ignored");
+ if (!aOutColor.EqualsLiteral("transparent")) {
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::GetHTMLBackgroundColorState(bool* aMixed,
+ nsAString& aOutColor) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // TODO: We don't handle "mixed" correctly!
+ if (NS_WARN_IF(!aMixed)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aMixed = false;
+ aOutColor.Truncate();
+
+ Result<RefPtr<Element>, nsresult> cellOrRowOrTableElementOrError =
+ GetSelectedOrParentTableElement();
+ if (cellOrRowOrTableElementOrError.isErr()) {
+ NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() returned error");
+ return cellOrRowOrTableElementOrError.unwrapErr();
+ }
+
+ for (RefPtr<Element> element = cellOrRowOrTableElementOrError.unwrap();
+ element; element = element->GetParentElement()) {
+ // We are in a cell or selected table
+ element->GetAttr(nsGkAtoms::bgcolor, aOutColor);
+
+ // Done if we have a color explicitly set
+ if (!aOutColor.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // Once we hit the body, we're done
+ if (element->IsHTMLElement(nsGkAtoms::body)) {
+ return NS_OK;
+ }
+
+ // No color is set, but we need to report visible color inherited
+ // from nested cells/tables, so search up parent chain so that
+ // let's keep checking the ancestors.
+ }
+
+ // If no table or cell found, get page body
+ Element* rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rootElement->GetAttr(nsGkAtoms::bgcolor, aOutColor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetListState(bool* aMixed, bool* aOL, bool* aUL,
+ bool* aDL) {
+ if (NS_WARN_IF(!aMixed) || NS_WARN_IF(!aOL) || NS_WARN_IF(!aUL) ||
+ NS_WARN_IF(!aDL)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (!mInitSucceeded) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ ErrorResult error;
+ ListElementSelectionState state(*this, error);
+ if (error.Failed()) {
+ NS_WARNING("ListElementSelectionState failed");
+ return error.StealNSResult();
+ }
+
+ *aMixed = state.IsNotOneTypeListElementSelected();
+ *aOL = state.IsOLElementSelected();
+ *aUL = state.IsULElementSelected();
+ *aDL = state.IsDLElementSelected();
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetListItemState(bool* aMixed, bool* aLI, bool* aDT,
+ bool* aDD) {
+ if (NS_WARN_IF(!aMixed) || NS_WARN_IF(!aLI) || NS_WARN_IF(!aDT) ||
+ NS_WARN_IF(!aDD)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (!mInitSucceeded) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ ErrorResult error;
+ ListItemElementSelectionState state(*this, error);
+ if (error.Failed()) {
+ NS_WARNING("ListItemElementSelectionState failed");
+ return error.StealNSResult();
+ }
+
+ // XXX Why do we ignore `<li>` element selected state?
+ *aMixed = state.IsNotOneTypeDefinitionListItemElementSelected();
+ *aLI = state.IsLIElementSelected();
+ *aDT = state.IsDTElementSelected();
+ *aDD = state.IsDDElementSelected();
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetAlignment(bool* aMixed,
+ nsIHTMLEditor::EAlignment* aAlign) {
+ if (NS_WARN_IF(!aMixed) || NS_WARN_IF(!aAlign)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (!mInitSucceeded) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ ErrorResult error;
+ AlignStateAtSelection state(*this, error);
+ if (error.Failed()) {
+ NS_WARNING("AlignStateAtSelection failed");
+ return error.StealNSResult();
+ }
+
+ *aMixed = false;
+ *aAlign = state.AlignmentAtSelectionStart();
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::MakeOrChangeList(const nsAString& aListType,
+ bool aEntireList,
+ const nsAString& aBulletType) {
+ RefPtr<nsAtom> listTagName = NS_Atomize(aListType);
+ if (NS_WARN_IF(!listTagName) || NS_WARN_IF(!listTagName->IsStatic())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ // MOZ_KnownLive(listTagName->AsStatic()) because nsStaticAtom instances live
+ // while the process is running.
+ nsresult rv = MakeOrChangeListAsAction(
+ MOZ_KnownLive(*listTagName->AsStatic()), aBulletType,
+ aEntireList ? SelectAllOfCurrentList::Yes : SelectAllOfCurrentList::No);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::MakeOrChangeListAsAction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::MakeOrChangeListAsAction(
+ const nsStaticAtom& aListElementTagName, const nsAString& aBulletType,
+ SelectAllOfCurrentList aSelectAllOfCurrentList, nsIPrincipal* aPrincipal) {
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ AutoEditActionDataSetter editActionData(
+ *this, HTMLEditUtils::GetEditActionForInsert(aListElementTagName),
+ aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ Result<EditActionResult, nsresult> result =
+ MakeOrChangeListAndListItemAsSubAction(aListElementTagName, aBulletType,
+ aSelectAllOfCurrentList);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::MakeOrChangeListAndListItemAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::RemoveList(const nsAString& aListType) {
+ nsresult rv = RemoveListAsAction(aListType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveListAsAction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::RemoveListAsAction(const nsAString& aListType,
+ nsIPrincipal* aPrincipal) {
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Note that we ignore aListType when we actually remove parent list elements.
+ // However, we need to set InputEvent.inputType to "insertOrderedList" or
+ // "insertedUnorderedList" when this is called for
+ // execCommand("insertorderedlist") or execCommand("insertunorderedlist").
+ // Otherwise, comm-central UI may call this methods with "dl" or "".
+ // So, it's okay to use mismatched EditAction here if this is called in
+ // comm-central.
+
+ RefPtr<nsAtom> listAtom = NS_Atomize(aListType);
+ if (NS_WARN_IF(!listAtom)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ AutoEditActionDataSetter editActionData(
+ *this, HTMLEditUtils::GetEditActionForRemoveList(*listAtom), aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ rv = RemoveListAtSelectionAsSubAction(*editingHost);
+ NS_WARNING_ASSERTION(NS_FAILED(rv),
+ "HTMLEditor::RemoveListAtSelectionAsSubAction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::FormatBlockContainerAsSubAction(
+ const nsStaticAtom& aTagName, FormatBlockMode aFormatBlockMode) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ MOZ_ASSERT(&aTagName != nsGkAtoms::dd && &aTagName != nsGkAtoms::dt);
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eCreateOrRemoveBlock, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ if (IsSelectionRangeContainerNotContent()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ // FormatBlockContainerWithTransaction() creates AutoSelectionRestorer.
+ // Therefore, even if it returns NS_OK, editor might have been destroyed
+ // at restoring Selection.
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (MOZ_UNLIKELY(!editingHost)) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ AutoRangeArray selectionRanges(SelectionRef());
+ Result<RefPtr<Element>, nsresult> suggestBlockElementToPutCaretOrError =
+ FormatBlockContainerWithTransaction(selectionRanges, aTagName,
+ aFormatBlockMode, *editingHost);
+ if (suggestBlockElementToPutCaretOrError.isErr()) {
+ NS_WARNING("HTMLEditor::FormatBlockContainerWithTransaction() failed");
+ return suggestBlockElementToPutCaretOrError.unwrapErr();
+ }
+
+ if (selectionRanges.HasSavedRanges()) {
+ selectionRanges.RestoreFromSavedRanges();
+ }
+
+ rv = selectionRanges.ApplyTo(SelectionRef());
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::ApplyTo(SelectionRef()) failed, but ignored");
+ return rv;
+ }
+
+ rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() "
+ "failed");
+
+ if (!suggestBlockElementToPutCaretOrError.inspect() ||
+ !SelectionRef().IsCollapsed()) {
+ return rv;
+ }
+ const auto firstSelectionStartPoint =
+ GetFirstSelectionStartPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!firstSelectionStartPoint.IsSet())) {
+ return rv;
+ }
+ Result<EditorRawDOMPoint, nsresult> pointInBlockElementOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(*suggestBlockElementToPutCaretOrError.inspect(),
+ firstSelectionStartPoint);
+ if (MOZ_UNLIKELY(pointInBlockElementOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, but "
+ "ignored");
+ return rv;
+ }
+ // Note that if the point is unset, it means that firstSelectionStartPoint is
+ // in the block element.
+ if (pointInBlockElementOrError.inspect().IsSet()) {
+ nsresult rvOfCollapseSelection =
+ CollapseSelectionTo(pointInBlockElementOrError.inspect());
+ if (MOZ_UNLIKELY(rvOfCollapseSelection == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvOfCollapseSelection),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ return rv;
+}
+
+nsresult HTMLEditor::IndentAsAction(nsIPrincipal* aPrincipal) {
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eIndent,
+ aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ Result<EditActionResult, nsresult> result = IndentAsSubAction(*editingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::IndentAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::OutdentAsAction(nsIPrincipal* aPrincipal) {
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eOutdent,
+ aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ Result<EditActionResult, nsresult> result = OutdentAsSubAction(*editingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::OutdentAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ return NS_OK;
+}
+
+// TODO: IMPLEMENT ALIGNMENT!
+
+nsresult HTMLEditor::AlignAsAction(const nsAString& aAlignType,
+ nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(
+ *this, HTMLEditUtils::GetEditActionForAlignment(aAlignType), aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ Result<EditActionResult, nsresult> result =
+ AlignAsSubAction(aAlignType, *editingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::AlignAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(result.unwrapErr());
+ }
+ return NS_OK;
+}
+
+Element* HTMLEditor::GetInclusiveAncestorByTagName(const nsStaticAtom& aTagName,
+ nsIContent& aContent) const {
+ MOZ_ASSERT(&aTagName != nsGkAtoms::_empty);
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return nullptr;
+ }
+
+ return GetInclusiveAncestorByTagNameInternal(aTagName, aContent);
+}
+
+Element* HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(
+ const nsStaticAtom& aTagName) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(&aTagName != nsGkAtoms::_empty);
+
+ // If no node supplied, get it from anchor node of current selection
+ const EditorRawDOMPoint atAnchor(SelectionRef().AnchorRef());
+ if (NS_WARN_IF(!atAnchor.IsInContentNode())) {
+ return nullptr;
+ }
+
+ // Try to get the actual selected node
+ nsIContent* content = nullptr;
+ if (atAnchor.GetContainer()->HasChildNodes() &&
+ atAnchor.ContainerAs<nsIContent>()) {
+ content = atAnchor.GetChild();
+ }
+ // Anchor node is probably a text node - just use that
+ if (!content) {
+ content = atAnchor.ContainerAs<nsIContent>();
+ if (NS_WARN_IF(!content)) {
+ return nullptr;
+ }
+ }
+ return GetInclusiveAncestorByTagNameInternal(aTagName, *content);
+}
+
+Element* HTMLEditor::GetInclusiveAncestorByTagNameInternal(
+ const nsStaticAtom& aTagName, const nsIContent& aContent) const {
+ MOZ_ASSERT(&aTagName != nsGkAtoms::_empty);
+
+ Element* currentElement = aContent.GetAsElementOrParentElement();
+ if (NS_WARN_IF(!currentElement)) {
+ MOZ_ASSERT(!aContent.GetParentNode());
+ return nullptr;
+ }
+
+ bool lookForLink = IsLinkTag(aTagName);
+ bool lookForNamedAnchor = IsNamedAnchorTag(aTagName);
+ for (Element* element : currentElement->InclusiveAncestorsOfType<Element>()) {
+ // Stop searching if parent is a body element. Note: Originally used
+ // IsRoot() to/ stop at table cells, but that's too messy when you are
+ // trying to find the parent table.
+ if (element->IsHTMLElement(nsGkAtoms::body)) {
+ return nullptr;
+ }
+ if (lookForLink) {
+ // Test if we have a link (an anchor with href set)
+ if (HTMLEditUtils::IsLink(element)) {
+ return element;
+ }
+ } else if (lookForNamedAnchor) {
+ // Test if we have a named anchor (an anchor with name set)
+ if (HTMLEditUtils::IsNamedAnchor(element)) {
+ return element;
+ }
+ } else if (&aTagName == nsGkAtoms::list_) {
+ // Match "ol", "ul", or "dl" for lists
+ if (HTMLEditUtils::IsAnyListElement(element)) {
+ return element;
+ }
+ } else if (&aTagName == nsGkAtoms::td) {
+ // Table cells are another special case: match either "td" or "th"
+ if (HTMLEditUtils::IsTableCell(element)) {
+ return element;
+ }
+ } else if (&aTagName == element->NodeInfo()->NameAtom()) {
+ return element;
+ }
+ }
+ return nullptr;
+}
+
+NS_IMETHODIMP HTMLEditor::GetElementOrParentByTagName(const nsAString& aTagName,
+ nsINode* aNode,
+ Element** aReturn) {
+ if (NS_WARN_IF(aTagName.IsEmpty()) || NS_WARN_IF(!aReturn)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsStaticAtom* tagName = EditorUtils::GetTagNameAtom(aTagName);
+ if (NS_WARN_IF(!tagName)) {
+ // We don't need to support custom elements since this is an internal API.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+ if (NS_WARN_IF(tagName == nsGkAtoms::_empty)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aNode) {
+ AutoEditActionDataSetter dummyEditAction(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!dummyEditAction.CanHandle())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ RefPtr<Element> parentElement =
+ GetInclusiveAncestorByTagNameAtSelection(*tagName);
+ if (!parentElement) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+ parentElement.forget(aReturn);
+ return NS_OK;
+ }
+
+ if (!aNode->IsContent() || !aNode->GetAsElementOrParentElement()) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ RefPtr<Element> parentElement =
+ GetInclusiveAncestorByTagName(*tagName, *aNode->AsContent());
+ if (!parentElement) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+ parentElement.forget(aReturn);
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetSelectedElement(const nsAString& aTagName,
+ nsISupports** aReturn) {
+ if (NS_WARN_IF(!aReturn)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aReturn = nullptr;
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ ErrorResult error;
+ nsStaticAtom* tagName = EditorUtils::GetTagNameAtom(aTagName);
+ if (!aTagName.IsEmpty() && !tagName) {
+ // We don't need to support custom elements becaus of internal API.
+ return NS_OK;
+ }
+ RefPtr<nsINode> selectedNode = GetSelectedElement(tagName, error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "HTMLEditor::GetSelectedElement() failed");
+ selectedNode.forget(aReturn);
+ return error.StealNSResult();
+}
+
+already_AddRefed<Element> HTMLEditor::GetSelectedElement(const nsAtom* aTagName,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ MOZ_ASSERT(!aRv.Failed());
+
+ // If there is no Selection or two or more selection ranges, that means that
+ // not only one element is selected so that return nullptr.
+ if (SelectionRef().RangeCount() != 1) {
+ return nullptr;
+ }
+
+ bool isLinkTag = aTagName && IsLinkTag(*aTagName);
+ bool isNamedAnchorTag = aTagName && IsNamedAnchorTag(*aTagName);
+
+ RefPtr<nsRange> firstRange = SelectionRef().GetRangeAt(0);
+ MOZ_ASSERT(firstRange);
+
+ const RangeBoundary& startRef = firstRange->StartRef();
+ if (NS_WARN_IF(!startRef.IsSet())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ const RangeBoundary& endRef = firstRange->EndRef();
+ if (NS_WARN_IF(!endRef.IsSet())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Optimization for a single selected element
+ if (startRef.Container() == endRef.Container()) {
+ nsIContent* startContent = startRef.GetChildAtOffset();
+ nsIContent* endContent = endRef.GetChildAtOffset();
+ if (startContent && endContent &&
+ startContent->GetNextSibling() == endContent) {
+ if (!aTagName) {
+ if (!startContent->IsElement()) {
+ // This means only a text node or something is selected. We should
+ // return nullptr in this case since no other elements are selected.
+ return nullptr;
+ }
+ return do_AddRef(startContent->AsElement());
+ }
+ // Test for appropriate node type requested
+ if (aTagName == startContent->NodeInfo()->NameAtom() ||
+ (isLinkTag && HTMLEditUtils::IsLink(startContent)) ||
+ (isNamedAnchorTag && HTMLEditUtils::IsNamedAnchor(startContent))) {
+ MOZ_ASSERT(startContent->IsElement());
+ return do_AddRef(startContent->AsElement());
+ }
+ }
+ }
+
+ if (isLinkTag && startRef.Container()->IsContent() &&
+ endRef.Container()->IsContent()) {
+ // Link node must be the same for both ends of selection.
+ Element* parentLinkOfStart = GetInclusiveAncestorByTagNameInternal(
+ *nsGkAtoms::href, *startRef.Container()->AsContent());
+ if (parentLinkOfStart) {
+ if (SelectionRef().IsCollapsed()) {
+ // We have just a caret in the link.
+ return do_AddRef(parentLinkOfStart);
+ }
+ // Link node must be the same for both ends of selection.
+ Element* parentLinkOfEnd = GetInclusiveAncestorByTagNameInternal(
+ *nsGkAtoms::href, *endRef.Container()->AsContent());
+ if (parentLinkOfStart == parentLinkOfEnd) {
+ return do_AddRef(parentLinkOfStart);
+ }
+ }
+ }
+
+ if (SelectionRef().IsCollapsed()) {
+ return nullptr;
+ }
+
+ PostContentIterator postOrderIter;
+ postOrderIter.Init(firstRange);
+
+ RefPtr<Element> lastElementInRange;
+ for (nsINode* lastNodeInRange = nullptr; !postOrderIter.IsDone();
+ postOrderIter.Next()) {
+ if (lastElementInRange) {
+ // When any node follows an element node, not only one element is
+ // selected so that return nullptr.
+ return nullptr;
+ }
+
+ // This loop ignors any non-element nodes before first element node.
+ // Its purpose must be that this method treats this case as selecting
+ // the <b> element:
+ // - <p>abc <b>d[ef</b>}</p>
+ // because children of an element node is listed up before the element.
+ // However, this case must not be expected by the initial developer:
+ // - <p>a[bc <b>def</b>}</p>
+ // When we meet non-parent and non-next-sibling node of previous node,
+ // it means that the range across element boundary (open tag in HTML
+ // source). So, in this case, we should not say only the following
+ // element is selected.
+ nsINode* currentNode = postOrderIter.GetCurrentNode();
+ MOZ_ASSERT(currentNode);
+ if (lastNodeInRange && lastNodeInRange->GetParentNode() != currentNode &&
+ lastNodeInRange->GetNextSibling() != currentNode) {
+ return nullptr;
+ }
+
+ lastNodeInRange = currentNode;
+
+ lastElementInRange = Element::FromNodeOrNull(lastNodeInRange);
+ if (!lastElementInRange) {
+ continue;
+ }
+
+ // And also, if it's followed by a <br> element, we shouldn't treat the
+ // the element is selected like this case:
+ // - <p><b>[def</b>}<br></p>
+ // Note that we don't need special handling for <a href> because double
+ // clicking it selects the element and we use the first path to handle it.
+ // Additionally, we have this case too:
+ // - <p><b>[def</b><b>}<br></b></p>
+ // In these cases, the <br> element is not listed up by PostContentIterator.
+ // So, we should return nullptr if next sibling is a `<br>` element or
+ // next sibling starts with `<br>` element.
+ if (nsIContent* nextSibling = lastElementInRange->GetNextSibling()) {
+ if (nextSibling->IsHTMLElement(nsGkAtoms::br)) {
+ return nullptr;
+ }
+ nsIContent* firstEditableLeaf = HTMLEditUtils::GetFirstLeafContent(
+ *nextSibling, {LeafNodeType::OnlyLeafNode});
+ if (firstEditableLeaf &&
+ firstEditableLeaf->IsHTMLElement(nsGkAtoms::br)) {
+ return nullptr;
+ }
+ }
+
+ if (!aTagName) {
+ continue;
+ }
+
+ if (isLinkTag && HTMLEditUtils::IsLink(lastElementInRange)) {
+ continue;
+ }
+
+ if (isNamedAnchorTag && HTMLEditUtils::IsNamedAnchor(lastElementInRange)) {
+ continue;
+ }
+
+ if (aTagName == lastElementInRange->NodeInfo()->NameAtom()) {
+ continue;
+ }
+
+ // First element in the range does not match what the caller is looking
+ // for.
+ return nullptr;
+ }
+ return lastElementInRange.forget();
+}
+
+Result<CreateElementResult, nsresult> HTMLEditor::CreateAndInsertElement(
+ WithTransaction aWithTransaction, const nsAtom& aTagName,
+ const EditorDOMPoint& aPointToInsert,
+ const InitializeInsertingElement& aInitializer) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ // XXX We need offset at new node for RangeUpdaterRef(). Therefore, we need
+ // to compute the offset now but this is expensive. So, if it's possible,
+ // we need to redesign RangeUpdaterRef() as avoiding using indices.
+ Unused << aPointToInsert.Offset();
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eCreateNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // TODO: This method should have a callback function which is called
+ // immediately after creating an element but before it's inserted into
+ // the DOM tree. Then, caller can init the new element's attributes
+ // and children **without** transactions (it'll reduce the number of
+ // legacy mutation events). Finally, we can get rid of
+ // CreatElementTransaction since we can use InsertNodeTransaction
+ // instead.
+
+ auto createNewElementResult =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<CreateElementResult, nsresult> {
+ RefPtr<Element> newElement = CreateHTMLContent(&aTagName);
+ if (MOZ_UNLIKELY(!newElement)) {
+ NS_WARNING("EditorBase::CreateHTMLContent() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsresult rv = MarkElementDirty(*newElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::MarkElementDirty() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::MarkElementDirty() failed, but ignored");
+ rv = aInitializer(*this, *newElement, aPointToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "aInitializer failed");
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ RefPtr<InsertNodeTransaction> transaction =
+ InsertNodeTransaction::Create(*this, *newElement, aPointToInsert);
+ rv = aWithTransaction == WithTransaction::Yes
+ ? DoTransactionInternal(transaction)
+ : transaction->DoTransaction();
+ // FYI: Transaction::DoTransaction never returns NS_ERROR_EDITOR_*.
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING(
+ "InsertNodeTransaction::DoTransaction() caused destroying the "
+ "editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("InsertNodeTransaction::DoTransaction() failed");
+ return Err(rv);
+ }
+ // Override the success code if new element was moved by the web apps.
+ if (newElement &&
+ newElement->GetParentNode() != aPointToInsert.GetContainer()) {
+ NS_WARNING("The new element was not inserted into the expected node");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ return CreateElementResult(
+ std::move(newElement),
+ transaction->SuggestPointToPutCaret<EditorDOMPoint>());
+ }();
+
+ if (MOZ_UNLIKELY(createNewElementResult.isErr())) {
+ NS_WARNING("EditorBase::DoTransactionInternal() failed");
+ // XXX Why do we do this even when DoTransaction() returned error?
+ DebugOnly<nsresult> rvIgnored =
+ RangeUpdaterRef().SelAdjCreateNode(aPointToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "RangeUpdater::SelAdjCreateNode() failed");
+ return createNewElementResult;
+ }
+
+ // If we succeeded to create and insert new element, we need to adjust
+ // ranges in RangeUpdaterRef(). It currently requires offset of the new
+ // node. So, let's call it with original offset. Note that if
+ // aPointToInsert stores child node, it may not be at the offset since new
+ // element must be inserted before the old child. Although, mutation
+ // observer can do anything, but currently, we don't check it.
+ DebugOnly<nsresult> rvIgnored =
+ RangeUpdaterRef().SelAdjCreateNode(EditorRawDOMPoint(
+ aPointToInsert.GetContainer(), aPointToInsert.Offset()));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "RangeUpdater::SelAdjCreateNode() failed, but ignored");
+ if (MOZ_LIKELY(createNewElementResult.inspect().GetNewNode())) {
+ TopLevelEditSubActionDataRef().DidCreateElement(
+ *this, *createNewElementResult.inspect().GetNewNode());
+ }
+
+ return createNewElementResult;
+}
+
+nsresult HTMLEditor::CopyAttributes(WithTransaction aWithTransaction,
+ Element& aDestElement, Element& aSrcElement,
+ const AttributeFilter& aFilterFunc) {
+ RefPtr<nsDOMAttributeMap> srcAttributes = aSrcElement.Attributes();
+ if (!srcAttributes->Length()) {
+ return NS_OK;
+ }
+ AutoTArray<OwningNonNull<Attr>, 16> srcAttrs;
+ srcAttrs.SetCapacity(srcAttributes->Length());
+ for (uint32_t i = 0; i < srcAttributes->Length(); i++) {
+ RefPtr<Attr> attr = srcAttributes->Item(i);
+ if (!attr) {
+ break;
+ }
+ srcAttrs.AppendElement(std::move(attr));
+ }
+ if (aWithTransaction == WithTransaction::No) {
+ for (const OwningNonNull<Attr>& attr : srcAttrs) {
+ nsString value;
+ attr->GetValue(value);
+ if (!aFilterFunc(*this, aSrcElement, aDestElement, attr, value)) {
+ continue;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ aDestElement.SetAttr(attr->NodeInfo()->NamespaceID(),
+ attr->NodeInfo()->NameAtom(), value, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr() failed, but ignored");
+ }
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ return NS_OK;
+ }
+ MOZ_ASSERT_UNREACHABLE("Not implemented yet, but you try to use this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+already_AddRefed<Element> HTMLEditor::CreateElementWithDefaults(
+ const nsAtom& aTagName) {
+ // NOTE: Despite of public method, this can be called for internal use.
+
+ // Although this creates an element, but won't change the DOM tree nor
+ // transaction. So, EditAtion::eNotEditing is proper value here. If
+ // this is called for internal when there is already AutoEditActionDataSetter
+ // instance, this would be initialized with its EditAction value.
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return nullptr;
+ }
+
+ const nsAtom* realTagName = IsLinkTag(aTagName) || IsNamedAnchorTag(aTagName)
+ ? nsGkAtoms::a
+ : &aTagName;
+
+ // We don't use editor's CreateElement because we don't want to go through
+ // the transaction system
+
+ // New call to use instead to get proper HTML element, bug 39919
+ RefPtr<Element> newElement = CreateHTMLContent(realTagName);
+ if (!newElement) {
+ return nullptr;
+ }
+
+ // Mark the new element dirty, so it will be formatted
+ // XXX Don't we need to check the error result of setting _moz_dirty attr?
+ IgnoredErrorResult ignoredError;
+ newElement->SetAttribute(u"_moz_dirty"_ns, u""_ns, ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "Element::SetAttribute(_moz_dirty) failed, but ignored");
+ ignoredError.SuppressException();
+
+ // Set default values for new elements
+ if (realTagName == nsGkAtoms::table) {
+ newElement->SetAttr(nsGkAtoms::cellpadding, u"2"_ns, ignoredError);
+ if (ignoredError.Failed()) {
+ NS_WARNING("Element::SetAttr(nsGkAtoms::cellpadding, 2) failed");
+ return nullptr;
+ }
+ ignoredError.SuppressException();
+
+ newElement->SetAttr(nsGkAtoms::cellspacing, u"2"_ns, ignoredError);
+ if (ignoredError.Failed()) {
+ NS_WARNING("Element::SetAttr(nsGkAtoms::cellspacing, 2) failed");
+ return nullptr;
+ }
+ ignoredError.SuppressException();
+
+ newElement->SetAttr(nsGkAtoms::border, u"1"_ns, ignoredError);
+ if (ignoredError.Failed()) {
+ NS_WARNING("Element::SetAttr(nsGkAtoms::border, 1) failed");
+ return nullptr;
+ }
+ } else if (realTagName == nsGkAtoms::td) {
+ nsresult rv = SetAttributeOrEquivalent(newElement, nsGkAtoms::valign,
+ u"top"_ns, true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::SetAttributeOrEquivalent(nsGkAtoms::valign, top) "
+ "failed");
+ return nullptr;
+ }
+ }
+ // ADD OTHER TAGS HERE
+
+ return newElement.forget();
+}
+
+NS_IMETHODIMP HTMLEditor::CreateElementWithDefaults(const nsAString& aTagName,
+ Element** aReturn) {
+ if (NS_WARN_IF(aTagName.IsEmpty()) || NS_WARN_IF(!aReturn)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aReturn = nullptr;
+
+ nsStaticAtom* tagName = EditorUtils::GetTagNameAtom(aTagName);
+ if (NS_WARN_IF(!tagName)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ RefPtr<Element> newElement =
+ CreateElementWithDefaults(MOZ_KnownLive(*tagName));
+ if (!newElement) {
+ NS_WARNING("HTMLEditor::CreateElementWithDefaults() failed");
+ return NS_ERROR_FAILURE;
+ }
+ newElement.forget(aReturn);
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::InsertLinkAroundSelection(Element* aAnchorElement) {
+ nsresult rv = InsertLinkAroundSelectionAsAction(aAnchorElement);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertLinkAroundSelectionAsAction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::InsertLinkAroundSelectionAsAction(
+ Element* aAnchorElement, nsIPrincipal* aPrincipal) {
+ if (NS_WARN_IF(!aAnchorElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLinkElement,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (SelectionRef().IsCollapsed()) {
+ NS_WARNING("Selection was collapsed");
+ return NS_OK;
+ }
+
+ // Be sure we were given an anchor element
+ RefPtr<HTMLAnchorElement> anchor =
+ HTMLAnchorElement::FromNodeOrNull(aAnchorElement);
+ if (!anchor) {
+ return NS_OK;
+ }
+
+ nsAutoString rawHref;
+ anchor->GetAttr(nsGkAtoms::href, rawHref);
+ editActionData.SetData(rawHref);
+
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ nsAutoString href;
+ anchor->GetHref(href);
+ if (href.IsEmpty()) {
+ return NS_OK;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ // Set all attributes found on the supplied anchor element
+ RefPtr<nsDOMAttributeMap> attributeMap = anchor->Attributes();
+ if (NS_WARN_IF(!attributeMap)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // TODO: We should stop using this loop for adding attributes to newly created
+ // `<a href="...">` elements. Then, we can avoid to increate the ref-
+ // counter of attribute names since we can use nsStaticAtom if we don't
+ // need to support unknown attributes.
+ AutoTArray<EditorInlineStyleAndValue, 32> stylesToSet;
+ stylesToSet.SetCapacity(attributeMap->Length());
+ nsString value;
+ for (uint32_t i : IntegerRange(attributeMap->Length())) {
+ RefPtr<Attr> attribute = attributeMap->Item(i);
+ if (!attribute) {
+ continue;
+ }
+
+ RefPtr<nsAtom> attributeName = attribute->NodeInfo()->NameAtom();
+
+ MOZ_ASSERT(value.IsEmpty());
+ attribute->GetValue(value);
+
+ stylesToSet.AppendElement(EditorInlineStyleAndValue(
+ *nsGkAtoms::a, std::move(attributeName), std::move(value)));
+ }
+ rv = SetInlinePropertiesAsSubAction(stylesToSet);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetInlinePropertiesAsSubAction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SetHTMLBackgroundColorWithTransaction(
+ const nsAString& aColor) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Find a selected or enclosing table element to set background on
+ bool isCellSelected = false;
+ Result<RefPtr<Element>, nsresult> cellOrRowOrTableElementOrError =
+ GetSelectedOrParentTableElement(&isCellSelected);
+ if (cellOrRowOrTableElementOrError.isErr()) {
+ NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed");
+ return cellOrRowOrTableElementOrError.unwrapErr();
+ }
+
+ bool setColor = !aColor.IsEmpty();
+ RefPtr<Element> rootElementOfBackgroundColor =
+ cellOrRowOrTableElementOrError.unwrap();
+ if (rootElementOfBackgroundColor) {
+ // Needs to set or remove background color of each selected cell elements.
+ // Therefore, just the cell contains selection range, we don't need to
+ // do this. Note that users can select each cell, but with Selection API,
+ // web apps can select <tr> and <td> at same time. With <table>, looks
+ // odd, though.
+ if (isCellSelected || rootElementOfBackgroundColor->IsAnyOfHTMLElements(
+ nsGkAtoms::table, nsGkAtoms::tr)) {
+ SelectedTableCellScanner scanner(SelectionRef());
+ if (scanner.IsInTableCellSelectionMode()) {
+ if (setColor) {
+ for (const OwningNonNull<Element>& cellElement :
+ scanner.ElementsRef()) {
+ // `MOZ_KnownLive(cellElement)` is safe because of `scanner`
+ // is stack only class and keeps grabbing it until it's destroyed.
+ nsresult rv = SetAttributeWithTransaction(
+ MOZ_KnownLive(cellElement), *nsGkAtoms::bgcolor, aColor);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::::SetAttributeWithTransaction(nsGkAtoms::"
+ "bgcolor) failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+ }
+ for (const OwningNonNull<Element>& cellElement :
+ scanner.ElementsRef()) {
+ // `MOZ_KnownLive(cellElement)` is safe because of `scanner`
+ // is stack only class and keeps grabbing it until it's destroyed.
+ nsresult rv = RemoveAttributeWithTransaction(
+ MOZ_KnownLive(cellElement), *nsGkAtoms::bgcolor);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::bgcolor)"
+ " failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+ }
+ }
+ // If we failed to find a cell, fall through to use originally-found element
+ } else {
+ // No table element -- set the background color on the body tag
+ rootElementOfBackgroundColor = GetRoot();
+ if (NS_WARN_IF(!rootElementOfBackgroundColor)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ // Use the editor method that goes through the transaction system
+ if (setColor) {
+ nsresult rv = SetAttributeWithTransaction(*rootElementOfBackgroundColor,
+ *nsGkAtoms::bgcolor, aColor);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::bgcolor) failed");
+ return rv;
+ }
+ nsresult rv = RemoveAttributeWithTransaction(*rootElementOfBackgroundColor,
+ *nsGkAtoms::bgcolor);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::bgcolor) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::RemoveEmptyInclusiveAncestorInlineElements(
+ nsIContent& aContent) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aContent.Length());
+
+ Element* editingHost = aContent.GetEditingHost();
+ if (NS_WARN_IF(!editingHost)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (&aContent == editingHost ||
+ HTMLEditUtils::IsBlockElement(
+ aContent, BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ !EditorUtils::IsEditableContent(aContent, EditorType::HTML) ||
+ !aContent.GetParent()) {
+ return NS_OK;
+ }
+
+ // Don't strip wrappers if this is the only wrapper in the block. Then we'll
+ // add a <br> later, so it won't be an empty wrapper in the end.
+ // XXX This is different from Blink. We should delete empty inline element
+ // even if it's only child of the block element.
+ {
+ const Element* editableBlockElement = HTMLEditUtils::GetAncestorElement(
+ aContent, HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (!editableBlockElement ||
+ HTMLEditUtils::IsEmptyNode(
+ *editableBlockElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ return NS_OK;
+ }
+ }
+
+ OwningNonNull<nsIContent> content = aContent;
+ for (nsIContent* parentContent : aContent.AncestorsOfType<nsIContent>()) {
+ if (HTMLEditUtils::IsBlockElement(
+ *parentContent, BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ parentContent->Length() != 1 ||
+ !EditorUtils::IsEditableContent(*parentContent, EditorType::HTML) ||
+ parentContent == editingHost) {
+ break;
+ }
+ content = *parentContent;
+ }
+
+ nsresult rv = DeleteNodeWithTransaction(content);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::DeleteAllChildrenWithTransaction(Element& aElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Prevent rules testing until we're done
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ while (nsCOMPtr<nsIContent> child = aElement.GetLastChild()) {
+ nsresult rv = DeleteNodeWithTransaction(*child);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::DeleteNode(nsINode* aNode, bool aPreserveSelection,
+ uint8_t aOptionalArgCount) {
+ if (NS_WARN_IF(!aNode) || NS_WARN_IF(!aNode->IsContent())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveNode);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Make dispatch `input` event after stopping preserving selection.
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this,
+ ScrollSelectionIntoView::No, // not a user interaction
+ __FUNCTION__);
+
+ Maybe<AutoTransactionsConserveSelection> preserveSelection;
+ if (aOptionalArgCount && aPreserveSelection) {
+ preserveSelection.emplace(*this);
+ }
+
+ rv = DeleteNodeWithTransaction(MOZ_KnownLive(*aNode->AsContent()));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+}
+
+Result<CaretPoint, nsresult> HTMLEditor::DeleteTextWithTransaction(
+ Text& aTextNode, uint32_t aOffset, uint32_t aLength) {
+ if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aTextNode))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ EditorBase::DeleteTextWithTransaction(aTextNode, aOffset, aLength);
+ NS_WARNING_ASSERTION(caretPointOrError.isOk(),
+ "EditorBase::DeleteTextWithTransaction() failed");
+ return caretPointOrError;
+}
+
+Result<InsertTextResult, nsresult> HTMLEditor::ReplaceTextWithTransaction(
+ Text& aTextNode, uint32_t aOffset, uint32_t aLength,
+ const nsAString& aStringToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aLength > 0 || !aStringToInsert.IsEmpty());
+
+ if (aStringToInsert.IsEmpty()) {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ DeleteTextWithTransaction(aTextNode, aOffset, aLength);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ return InsertTextResult(EditorDOMPointInText(&aTextNode, aOffset),
+ caretPointOrError.unwrap());
+ }
+
+ if (!aLength) {
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, aStringToInsert,
+ EditorDOMPoint(&aTextNode, aOffset));
+ NS_WARNING_ASSERTION(insertTextResult.isOk(),
+ "HTMLEditor::InsertTextWithTransaction() failed");
+ return insertTextResult;
+ }
+
+ if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aTextNode))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // This should emulates inserting text for better undo/redo behavior.
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // FYI: Create the insertion point before changing the DOM tree because
+ // the point may become invalid offset after that.
+ EditorDOMPointInText pointToInsert(&aTextNode, aOffset);
+
+ RefPtr<ReplaceTextTransaction> transaction = ReplaceTextTransaction::Create(
+ *this, aStringToInsert, aTextNode, aOffset, aLength);
+ MOZ_ASSERT(transaction);
+
+ if (aLength && !mActionListeners.IsEmpty()) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored =
+ listener->WillDeleteText(&aTextNode, aOffset, aLength);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::WillDeleteText() failed, but ignored");
+ }
+ }
+
+ nsresult rv = DoTransactionInternal(transaction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DoTransactionInternal() failed");
+
+ // Don't check whether we've been destroyed here because we need to notify
+ // listeners and observers below even if we've already destroyed.
+
+ EditorDOMPointInText endOfInsertedText(&aTextNode,
+ aOffset + aStringToInsert.Length());
+
+ if (pointToInsert.IsSet()) {
+ auto [begin, end] = ComputeInsertedRange(pointToInsert, aStringToInsert);
+ if (begin.IsSet() && end.IsSet()) {
+ TopLevelEditSubActionDataRef().DidDeleteText(
+ *this, begin.To<EditorRawDOMPoint>());
+ TopLevelEditSubActionDataRef().DidInsertText(
+ *this, begin.To<EditorRawDOMPoint>(), end.To<EditorRawDOMPoint>());
+ }
+
+ // XXX Should we update endOfInsertedText here?
+ }
+
+ if (!mActionListeners.IsEmpty()) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored =
+ listener->DidInsertText(&aTextNode, aOffset, aStringToInsert, rv);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::DidInsertText() failed, but ignored");
+ }
+ }
+
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ return InsertTextResult(
+ std::move(endOfInsertedText),
+ transaction->SuggestPointToPutCaret<EditorDOMPoint>());
+}
+
+Result<InsertTextResult, nsresult> HTMLEditor::InsertTextWithTransaction(
+ Document& aDocument, const nsAString& aStringToInsert,
+ const EditorDOMPoint& aPointToInsert) {
+ if (NS_WARN_IF(!aPointToInsert.IsSet())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ // Do nothing if the node is read-only
+ if (MOZ_UNLIKELY(NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(
+ *aPointToInsert.GetContainer())))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ return EditorBase::InsertTextWithTransaction(aDocument, aStringToInsert,
+ aPointToInsert);
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::PrepareToInsertBRElement(
+ const EditorDOMPoint& aPointToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aPointToInsert.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (!aPointToInsert.IsInTextNode()) {
+ return aPointToInsert;
+ }
+
+ if (aPointToInsert.IsStartOfContainer()) {
+ // Insert before the text node.
+ EditorDOMPoint pointInContainer(aPointToInsert.GetContainer());
+ if (!pointInContainer.IsSet()) {
+ NS_WARNING("Failed to climb up the DOM tree from text node");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return pointInContainer;
+ }
+
+ if (aPointToInsert.IsEndOfContainer()) {
+ // Insert after the text node.
+ EditorDOMPoint pointInContainer(aPointToInsert.GetContainer());
+ if (NS_WARN_IF(!pointInContainer.IsSet())) {
+ NS_WARNING("Failed to climb up the DOM tree from text node");
+ return Err(NS_ERROR_FAILURE);
+ }
+ MOZ_ALWAYS_TRUE(pointInContainer.AdvanceOffset());
+ return pointInContainer;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aPointToInsert.IsSetAndValid());
+
+ // Unfortunately, we need to split the text node at the offset.
+ Result<SplitNodeResult, nsresult> splitTextNodeResult =
+ SplitNodeWithTransaction(aPointToInsert);
+ if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitTextNodeResult.propagateErr();
+ }
+ nsresult rv = splitTextNodeResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+
+ // Insert new <br> before the right node.
+ auto atNextContent =
+ splitTextNodeResult.inspect().AtNextContent<EditorDOMPoint>();
+ if (MOZ_UNLIKELY(!atNextContent.IsSet())) {
+ NS_WARNING("The next node seems not in the DOM tree");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return atNextContent;
+}
+
+Result<CreateElementResult, nsresult> HTMLEditor::InsertBRElement(
+ WithTransaction aWithTransaction, const EditorDOMPoint& aPointToInsert,
+ EDirection aSelect /* = eNone */) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Result<EditorDOMPoint, nsresult> maybePointToInsert =
+ PrepareToInsertBRElement(aPointToInsert);
+ if (maybePointToInsert.isErr()) {
+ NS_WARNING(
+ nsPrintfCString("HTMLEditor::PrepareToInsertBRElement(%s) failed",
+ ToString(aWithTransaction).c_str())
+ .get());
+ return maybePointToInsert.propagateErr();
+ }
+ MOZ_ASSERT(maybePointToInsert.inspect().IsSetAndValid());
+
+ Result<CreateElementResult, nsresult> createNewBRElementResult =
+ CreateAndInsertElement(aWithTransaction, *nsGkAtoms::br,
+ maybePointToInsert.inspect());
+ if (MOZ_UNLIKELY(createNewBRElementResult.isErr())) {
+ NS_WARNING(nsPrintfCString("HTMLEditor::CreateAndInsertElement(%s) failed",
+ ToString(aWithTransaction).c_str())
+ .get());
+ return createNewBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewBRElementResult =
+ createNewBRElementResult.unwrap();
+ RefPtr<Element> newBRElement =
+ unwrappedCreateNewBRElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newBRElement);
+
+ unwrappedCreateNewBRElementResult.IgnoreCaretPointSuggestion();
+ switch (aSelect) {
+ case eNext: {
+ const auto pointToPutCaret = EditorDOMPoint::After(
+ *newBRElement, Selection::InterlinePosition::StartOfNextLine);
+ return CreateElementResult(std::move(newBRElement), pointToPutCaret);
+ }
+ case ePrevious: {
+ const auto pointToPutCaret = EditorDOMPoint(
+ newBRElement, Selection::InterlinePosition::StartOfNextLine);
+ return CreateElementResult(std::move(newBRElement), pointToPutCaret);
+ }
+ default:
+ NS_WARNING(
+ "aSelect has invalid value, the caller need to set selection "
+ "by itself");
+ [[fallthrough]];
+ case eNone:
+ return CreateElementResult(
+ std::move(newBRElement),
+ unwrappedCreateNewBRElementResult.UnwrapCaretPoint());
+ }
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::InsertContainerWithTransaction(
+ nsIContent& aContentToBeWrapped, const nsAtom& aWrapperTagName,
+ const InitializeInsertingElement& aInitializer) {
+ EditorDOMPoint pointToInsertNewContainer(&aContentToBeWrapped);
+ if (NS_WARN_IF(!pointToInsertNewContainer.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // aContentToBeWrapped will be moved to the new container before inserting the
+ // new container. So, when we insert the container, the insertion point is
+ // before the next sibling of aContentToBeWrapped.
+ // XXX If pointerToInsertNewContainer stores offset here, the offset and
+ // referring child node become mismatched. Although, currently this
+ // is not a problem since InsertNodeTransaction refers only child node.
+ MOZ_ALWAYS_TRUE(pointToInsertNewContainer.AdvanceOffset());
+
+ // Create new container.
+ RefPtr<Element> newContainer = CreateHTMLContent(&aWrapperTagName);
+ if (NS_WARN_IF(!newContainer)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (&aInitializer != &HTMLEditor::DoNothingForNewElement) {
+ nsresult rv = aInitializer(*this, *newContainer,
+ EditorDOMPoint(&aContentToBeWrapped));
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("aInitializer() failed");
+ return Err(rv);
+ }
+ }
+
+ // Notify our internal selection state listener
+ AutoInsertContainerSelNotify selNotify(RangeUpdaterRef());
+
+ // Put aNode in the new container, first.
+ // XXX Perhaps, we should not remove the container if it's not editable.
+ nsresult rv = DeleteNodeWithTransaction(aContentToBeWrapped);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+
+ {
+ // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary
+ // in normal cases. However, it may be required for nested edit
+ // actions which may be caused by legacy mutation event listeners or
+ // chrome script.
+ AutoTransactionsConserveSelection conserveSelection(*this);
+ Result<CreateContentResult, nsresult> insertContentNodeResult =
+ InsertNodeWithTransaction(aContentToBeWrapped,
+ EditorDOMPoint(newContainer, 0u));
+ if (MOZ_UNLIKELY(insertContentNodeResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertContentNodeResult.propagateErr();
+ }
+ insertContentNodeResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ // Put the new container where aNode was.
+ Result<CreateElementResult, nsresult> insertNewContainerElementResult =
+ InsertNodeWithTransaction<Element>(*newContainer,
+ pointToInsertNewContainer);
+ NS_WARNING_ASSERTION(insertNewContainerElementResult.isOk(),
+ "EditorBase::InsertNodeWithTransaction() failed");
+ return insertNewContainerElementResult;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::ReplaceContainerWithTransactionInternal(
+ Element& aOldContainer, const nsAtom& aTagName, const nsAtom& aAttribute,
+ const nsAString& aAttributeValue, bool aCloneAllAttributes) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(aOldContainer)) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aOldContainer))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // If we're replacing <dd> or <dt> with different type of element, we need to
+ // split the parent <dl>.
+ OwningNonNull<Element> containerElementToDelete = aOldContainer;
+ if (aOldContainer.IsAnyOfHTMLElements(nsGkAtoms::dd, nsGkAtoms::dt) &&
+ &aTagName != nsGkAtoms::dt && &aTagName != nsGkAtoms::dd &&
+ // aOldContainer always has a parent node because of removable.
+ aOldContainer.GetParentNode()->IsHTMLElement(nsGkAtoms::dl)) {
+ OwningNonNull<Element> const dlElement = *aOldContainer.GetParentElement();
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(dlElement)) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(dlElement))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<SplitRangeOffFromNodeResult, nsresult> splitDLElementResult =
+ SplitRangeOffFromElement(dlElement, aOldContainer, aOldContainer);
+ if (MOZ_UNLIKELY(splitDLElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitRangeOffFromElement() failed");
+ return splitDLElementResult.propagateErr();
+ }
+ splitDLElementResult.inspect().IgnoreCaretPointSuggestion();
+ RefPtr<Element> middleDLElement = aOldContainer.GetParentElement();
+ if (NS_WARN_IF(!middleDLElement) ||
+ NS_WARN_IF(!middleDLElement->IsHTMLElement(nsGkAtoms::dl)) ||
+ NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*middleDLElement))) {
+ NS_WARNING("The parent <dl> was lost at splitting it");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ containerElementToDelete = std::move(middleDLElement);
+ }
+
+ const RefPtr<Element> newContainer = CreateHTMLContent(&aTagName);
+ if (NS_WARN_IF(!newContainer)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Set or clone attribute if needed.
+ // FIXME: What should we do attributes of <dl> elements if we removed it
+ // above?
+ if (aCloneAllAttributes) {
+ MOZ_ASSERT(&aAttribute == nsGkAtoms::_empty);
+ CloneAttributesWithTransaction(*newContainer, aOldContainer);
+ } else if (&aAttribute != nsGkAtoms::_empty) {
+ nsresult rv = newContainer->SetAttr(kNameSpaceID_None,
+ const_cast<nsAtom*>(&aAttribute),
+ aAttributeValue, true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Element::SetAttr() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ const OwningNonNull<nsINode> parentNode =
+ *containerElementToDelete->GetParentNode();
+ const nsCOMPtr<nsINode> referenceNode =
+ containerElementToDelete->GetNextSibling();
+ AutoReplaceContainerSelNotify selStateNotify(RangeUpdaterRef(), aOldContainer,
+ *newContainer);
+ {
+ AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfChildren;
+ HTMLEditUtils::CollectChildren(
+ aOldContainer, arrayOfChildren, 0u,
+ // Move non-editable children too because its container, aElement, is
+ // editable so that all children must be removable node.
+ {});
+ // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary
+ // in normal cases. However, it may be required for nested edit
+ // actions which may be caused by legacy mutation event listeners or
+ // chrome script.
+ AutoTransactionsConserveSelection conserveSelection(*this);
+ // Move all children from the old container to the new container.
+ // For making all MoveNodeTransactions have a reference node in the current
+ // parent, move nodes from last one to preceding ones.
+ for (const OwningNonNull<nsIContent>& child : Reversed(arrayOfChildren)) {
+ Result<MoveNodeResult, nsresult> moveChildResult =
+ MoveNodeWithTransaction(MOZ_KnownLive(child), // due to bug 1622253.
+ EditorDOMPoint(newContainer, 0u));
+ if (MOZ_UNLIKELY(moveChildResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveChildResult.propagateErr();
+ }
+ // We'll suggest new caret point which is suggested by new container
+ // element insertion result. Therefore, we need to do nothing here.
+ moveChildResult.inspect().IgnoreCaretPointSuggestion();
+ }
+ }
+
+ // Delete containerElementToDelete from the DOM tree to make it not referred
+ // by InsertNodeTransaction.
+ nsresult rv = DeleteNodeWithTransaction(containerElementToDelete);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+
+ if (referenceNode && (!referenceNode->GetParentNode() ||
+ parentNode != referenceNode->GetParentNode())) {
+ NS_WARNING(
+ "The reference node for insertion has been moved to different parent, "
+ "so we got lost the insertion point");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // Finally, insert the new node to where probably aOldContainer was.
+ Result<CreateElementResult, nsresult> insertNewContainerElementResult =
+ InsertNodeWithTransaction<Element>(
+ *newContainer, referenceNode ? EditorDOMPoint(referenceNode)
+ : EditorDOMPoint::AtEndOf(*parentNode));
+ NS_WARNING_ASSERTION(insertNewContainerElementResult.isOk(),
+ "EditorBase::InsertNodeWithTransaction() failed");
+ MOZ_ASSERT_IF(
+ insertNewContainerElementResult.isOk(),
+ insertNewContainerElementResult.inspect().GetNewNode() == newContainer);
+ return insertNewContainerElementResult;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::RemoveContainerWithTransaction(
+ Element& aElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(aElement)) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aElement))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Notify our internal selection state listener.
+ AutoRemoveContainerSelNotify selNotify(RangeUpdaterRef(),
+ EditorRawDOMPoint(&aElement));
+
+ AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfChildren;
+ HTMLEditUtils::CollectChildren(
+ aElement, arrayOfChildren, 0u,
+ // Move non-editable children too because its container, aElement, is
+ // editable so that all children must be removable node.
+ {});
+ const OwningNonNull<nsINode> parentNode = *aElement.GetParentNode();
+ nsCOMPtr<nsIContent> previousChild = aElement.GetPreviousSibling();
+ // For making all MoveNodeTransactions have a referenc node in the current
+ // parent, move nodes from last one to preceding ones.
+ for (const OwningNonNull<nsIContent>& child : Reversed(arrayOfChildren)) {
+ if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(child))) {
+ continue;
+ }
+ Result<MoveNodeResult, nsresult> moveChildResult = MoveNodeWithTransaction(
+ MOZ_KnownLive(child), // due to bug 1622253.
+ previousChild ? EditorDOMPoint::After(previousChild)
+ : EditorDOMPoint(parentNode, 0u));
+ if (MOZ_UNLIKELY(moveChildResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveChildResult.propagateErr();
+ }
+ // If the reference node was moved to different container, try to recover
+ // the original position.
+ if (previousChild &&
+ MOZ_UNLIKELY(previousChild->GetParentNode() != parentNode)) {
+ if (MOZ_UNLIKELY(child->GetParentNode() != parentNode)) {
+ NS_WARNING(
+ "Neither the reference (previous) sibling nor the moved child was "
+ "in the expected parent node");
+ moveChildResult.inspect().IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ previousChild = child->GetPreviousSibling();
+ }
+ // We'll need to put caret at next sibling of aElement if nobody moves
+ // content nodes under the parent node except us.
+ moveChildResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ if (aElement.GetParentNode() && aElement.GetParentNode() != parentNode) {
+ NS_WARNING(
+ "The removing element has already been moved to another element");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ NS_WARNING_ASSERTION(!aElement.GetFirstChild(),
+ "The removing container still has some children, but "
+ "they are removed by removing the container");
+
+ auto GetNextSiblingOf =
+ [](const nsTArray<OwningNonNull<nsIContent>>& aArrayOfMovedContent,
+ const nsINode& aExpectedParentNode) -> nsIContent* {
+ for (const OwningNonNull<nsIContent>& movedChild :
+ Reversed(aArrayOfMovedContent)) {
+ if (movedChild != &aExpectedParentNode) {
+ continue; // Ignore moved node which was moved to different place
+ }
+ return movedChild->GetNextSibling();
+ }
+ // XXX If all nodes were moved by web apps, we cannot suggest "collect"
+ // position without computing the index of aElement. However, I
+ // don't think that it's necessary for the web apps in the wild.
+ return nullptr;
+ };
+
+ nsCOMPtr<nsIContent> nextSibling =
+ aElement.GetParentNode() ? aElement.GetNextSibling()
+ : GetNextSiblingOf(arrayOfChildren, *parentNode);
+
+ nsresult rv = DeleteNodeWithTransaction(aElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteNodeTransaction() failed");
+ return Err(rv);
+ }
+
+ if (nextSibling && nextSibling->GetParentNode() != parentNode) {
+ nextSibling = GetNextSiblingOf(arrayOfChildren, *parentNode);
+ }
+ return nextSibling ? EditorDOMPoint(nextSibling)
+ : EditorDOMPoint::AtEndOf(*parentNode);
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentAppended(
+ nsIContent* aFirstNewContent) {
+ DoContentInserted(aFirstNewContent, ContentNodeIs::Appended);
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentInserted(
+ nsIContent* aChild) {
+ DoContentInserted(aChild, ContentNodeIs::Inserted);
+}
+
+bool HTMLEditor::IsInObservedSubtree(nsIContent* aChild) {
+ if (!aChild) {
+ return false;
+ }
+
+ // FIXME(emilio, bug 1596856): This should probably work if the root is in the
+ // same shadow tree as the child, probably? I don't know what the
+ // contenteditable-in-shadow-dom situation is.
+ if (Element* root = GetRoot()) {
+ // To be super safe here, check both ChromeOnlyAccess and NAC / Shadow DOM.
+ // That catches (also unbound) native anonymous content and ShadowDOM.
+ if (root->ChromeOnlyAccess() != aChild->ChromeOnlyAccess() ||
+ root->IsInNativeAnonymousSubtree() !=
+ aChild->IsInNativeAnonymousSubtree() ||
+ root->IsInShadowTree() != aChild->IsInShadowTree()) {
+ return false;
+ }
+ }
+
+ return !aChild->ChromeOnlyAccess() && !aChild->IsInShadowTree() &&
+ !aChild->IsInNativeAnonymousSubtree();
+}
+
+void HTMLEditor::DoContentInserted(nsIContent* aChild,
+ ContentNodeIs aContentNodeIs) {
+ MOZ_ASSERT(aChild);
+ nsINode* container = aChild->GetParentNode();
+ MOZ_ASSERT(container);
+
+ if (!IsInObservedSubtree(aChild)) {
+ return;
+ }
+
+ // XXX Why do we need this? This method is a helper of mutation observer.
+ // So, the callers of mutation observer should guarantee that this won't
+ // be deleted at least during the call.
+ RefPtr<HTMLEditor> kungFuDeathGrip(this);
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ if (ShouldReplaceRootElement()) {
+ UpdateRootElement();
+ if (mPendingRootElementUpdatedRunner) {
+ return;
+ }
+ mPendingRootElementUpdatedRunner = NewRunnableMethod(
+ "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged);
+ nsContentUtils::AddScriptRunner(
+ do_AddRef(mPendingRootElementUpdatedRunner));
+ return;
+ }
+
+ // We don't need to handle our own modifications
+ if (!GetTopLevelEditSubAction() && container->IsEditable()) {
+ if (EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) {
+ // Ignore insertion of the padding <br> element.
+ return;
+ }
+ nsresult rv = OnDocumentModified();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::OnDocumentModified() failed, but ignored");
+
+ // Update spellcheck for only the newly-inserted node (bug 743819)
+ if (mInlineSpellChecker) {
+ nsIContent* endContent = aChild;
+ if (aContentNodeIs == ContentNodeIs::Appended) {
+ nsIContent* child = nullptr;
+ for (child = aChild; child; child = child->GetNextSibling()) {
+ if (child->InclusiveDescendantMayNeedSpellchecking(this)) {
+ break;
+ }
+ }
+ if (!child) {
+ // No child needed spellchecking, return.
+ return;
+ }
+
+ // Maybe more than 1 child was appended.
+ endContent = container->GetLastChild();
+ } else if (!aChild->InclusiveDescendantMayNeedSpellchecking(this)) {
+ return;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(aChild);
+ range->SelectNodesInContainer(container, aChild, endContent);
+ DebugOnly<nsresult> rvIgnored =
+ mInlineSpellChecker->SpellCheckRange(range);
+ NS_WARNING_ASSERTION(
+ rvIgnored == NS_ERROR_NOT_INITIALIZED || NS_SUCCEEDED(rvIgnored),
+ "mozInlineSpellChecker::SpellCheckRange() failed, but ignored");
+ }
+ }
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentRemoved(
+ nsIContent* aChild, nsIContent* aPreviousSibling) {
+ if (!IsInObservedSubtree(aChild)) {
+ return;
+ }
+
+ // XXX Why do we need to do this? This method is a mutation observer's
+ // method. Therefore, the caller should guarantee that this won't be
+ // deleted during the call.
+ RefPtr<HTMLEditor> kungFuDeathGrip(this);
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ if (SameCOMIdentity(aChild, mRootElement)) {
+ mRootElement = nullptr;
+ if (mPendingRootElementUpdatedRunner) {
+ return;
+ }
+ mPendingRootElementUpdatedRunner = NewRunnableMethod(
+ "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged);
+ nsContentUtils::AddScriptRunner(
+ do_AddRef(mPendingRootElementUpdatedRunner));
+ return;
+ }
+
+ // We don't need to handle our own modifications
+ if (!GetTopLevelEditSubAction() && aChild->GetParentNode()->IsEditable()) {
+ if (aChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) {
+ // Ignore removal of the padding <br> element for empty editor.
+ return;
+ }
+
+ nsresult rv = OnDocumentModified();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::OnDocumentModified() failed, but ignored");
+ }
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::CharacterDataChanged(
+ nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {
+ if (!mInlineSpellChecker || !aContent->IsEditable() ||
+ !IsInObservedSubtree(aContent) ||
+ GetTopLevelEditSubAction() != EditSubAction::eNone) {
+ return;
+ }
+
+ nsIContent* parent = aContent->GetParent();
+ if (!parent || !parent->InclusiveDescendantMayNeedSpellchecking(this)) {
+ return;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(aContent);
+ range->SelectNodesInContainer(parent, aContent, aContent);
+ DebugOnly<nsresult> rvIgnored = mInlineSpellChecker->SpellCheckRange(range);
+}
+
+nsresult HTMLEditor::SelectEntireDocument() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!mInitSucceeded) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // XXX It's odd to select all of the document body if an contenteditable
+ // element has focus.
+ RefPtr<Element> bodyOrDocumentElement = GetRoot();
+ if (NS_WARN_IF(!bodyOrDocumentElement)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // If we're empty, don't select all children because that would select the
+ // padding <br> element for empty editor.
+ if (IsEmpty()) {
+ nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ return rv;
+ }
+
+ // Otherwise, select all children.
+ ErrorResult error;
+ SelectionRef().SelectAllChildren(*bodyOrDocumentElement, error);
+ if (NS_WARN_IF(Destroyed())) {
+ error.SuppressException();
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "Selection::SelectAllChildren() failed");
+ return error.StealNSResult();
+}
+
+nsresult HTMLEditor::SelectAllInternal() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ CommitComposition();
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+
+ auto GetBodyElementIfElementIsParentOfHTMLBody =
+ [](const Element& aElement) -> Element* {
+ if (!aElement.OwnerDoc()->IsHTMLDocument()) {
+ return const_cast<Element*>(&aElement);
+ }
+ HTMLBodyElement* bodyElement = aElement.OwnerDoc()->GetBodyElement();
+ return bodyElement && nsContentUtils::ContentIsFlattenedTreeDescendantOf(
+ bodyElement, &aElement)
+ ? bodyElement
+ : const_cast<Element*>(&aElement);
+ };
+
+ nsCOMPtr<nsIContent> selectionRootContent =
+ [&]() MOZ_CAN_RUN_SCRIPT -> nsIContent* {
+ RefPtr<Element> elementToBeSelected = [&]() -> Element* {
+ // If there is at least one selection range, we should compute the
+ // selection root from the anchor node.
+ if (SelectionRef().RangeCount()) {
+ if (nsIContent* content =
+ nsIContent::FromNodeOrNull(SelectionRef().GetAnchorNode())) {
+ if (content->IsElement()) {
+ return content->AsElement();
+ }
+ if (Element* parentElement =
+ content->GetParentElementCrossingShadowRoot()) {
+ return parentElement;
+ }
+ }
+ }
+ // If no element contains a selection range, we should select all children
+ // of the focused element at least.
+ if (Element* focusedElement = GetFocusedElement()) {
+ return focusedElement;
+ }
+ // of the body or document element.
+ Element* bodyOrDocumentElement = GetRoot();
+ NS_WARNING_ASSERTION(bodyOrDocumentElement,
+ "There was no element in the document");
+ return bodyOrDocumentElement;
+ }();
+
+ // If the element to be selected is <input type="text"> or <textarea>,
+ // GetSelectionRootContent() returns its anonymous <div> element, but we
+ // want to select all of the document or selection limiter. Therefore,
+ // we should use its parent to compute the selection root.
+ if (elementToBeSelected->HasIndependentSelection()) {
+ Element* parentElement = elementToBeSelected->GetParentElement();
+ if (MOZ_LIKELY(parentElement)) {
+ elementToBeSelected = parentElement;
+ }
+ }
+
+ // Then, compute the selection root content to select all including
+ // elementToBeSelected.
+ RefPtr<PresShell> presShell = GetPresShell();
+ nsIContent* computedSelectionRootContent =
+ elementToBeSelected->GetSelectionRootContent(presShell);
+ if (NS_WARN_IF(!computedSelectionRootContent)) {
+ return nullptr;
+ }
+ if (MOZ_UNLIKELY(!computedSelectionRootContent->IsElement())) {
+ return computedSelectionRootContent;
+ }
+ return GetBodyElementIfElementIsParentOfHTMLBody(
+ *computedSelectionRootContent->AsElement());
+ }();
+ if (NS_WARN_IF(!selectionRootContent)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ Maybe<Selection::AutoUserInitiated> userSelection;
+ // XXX Do we need to mark it as "user initiated" for
+ // `Document.execCommand("selectAll")`?
+ if (!selectionRootContent->IsEditable()) {
+ userSelection.emplace(SelectionRef());
+ }
+ ErrorResult error;
+ SelectionRef().SelectAllChildren(*selectionRootContent, error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "Selection::SelectAllChildren() failed");
+ return error.StealNSResult();
+}
+
+bool HTMLEditor::SetCaretInTableCell(Element* aElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!aElement || !aElement->IsHTMLElement() ||
+ !HTMLEditUtils::IsAnyTableElement(aElement)) {
+ return false;
+ }
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost || !aElement->IsInclusiveDescendantOf(editingHost)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIContent> deepestFirstChild = aElement;
+ while (deepestFirstChild->HasChildren()) {
+ deepestFirstChild = deepestFirstChild->GetFirstChild();
+ }
+
+ // Set selection at beginning of the found node
+ nsresult rv = CollapseSelectionToStartOf(*deepestFirstChild);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ return NS_SUCCEEDED(rv);
+}
+
+/**
+ * This method scans the selection for adjacent text nodes
+ * and collapses them into a single text node.
+ * "adjacent" means literally adjacent siblings of the same parent.
+ * Uses HTMLEditor::JoinNodesWithTransaction() so action is undoable.
+ * Should be called within the context of a batch transaction.
+ */
+nsresult HTMLEditor::CollapseAdjacentTextNodes(nsRange& aInRange) {
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ // we can't actually do anything during iteration, so store the text nodes in
+ // an array first.
+ DOMSubtreeIterator subtreeIter;
+ if (NS_FAILED(subtreeIter.Init(aInRange))) {
+ NS_WARNING("DOMSubtreeIterator::Init() failed");
+ return NS_ERROR_FAILURE;
+ }
+ AutoTArray<OwningNonNull<Text>, 8> textNodes;
+ subtreeIter.AppendNodesToArray(
+ +[](nsINode& aNode, void*) -> bool {
+ return EditorUtils::IsEditableContent(*aNode.AsText(),
+ EditorType::HTML);
+ },
+ textNodes);
+
+ // now that I have a list of text nodes, collapse adjacent text nodes
+ while (textNodes.Length() > 1u) {
+ OwningNonNull<Text>& leftTextNode = textNodes[0u];
+ OwningNonNull<Text>& rightTextNode = textNodes[1u];
+
+ // If the text nodes are not direct siblings, we shouldn't join them, and
+ // we don't need to handle the left one anymore.
+ if (rightTextNode->GetPreviousSibling() != leftTextNode) {
+ textNodes.RemoveElementAt(0u);
+ continue;
+ }
+
+ Result<JoinNodesResult, nsresult> joinNodesResult =
+ JoinNodesWithTransaction(MOZ_KnownLive(*leftTextNode),
+ MOZ_KnownLive(*rightTextNode));
+ if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
+ NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
+ return joinNodesResult.unwrapErr();
+ }
+ if (MOZ_LIKELY(joinNodesResult.inspect().RemovedContent() ==
+ leftTextNode)) {
+ textNodes.RemoveElementAt(0u);
+ } else if (MOZ_LIKELY(joinNodesResult.inspect().RemovedContent() ==
+ rightTextNode)) {
+ textNodes.RemoveElementAt(1u);
+ } else {
+ MOZ_ASSERT_UNREACHABLE(
+ "HTMLEditor::JoinNodesWithTransaction() removed unexpected node");
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::SetSelectionAtDocumentStart() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Element> rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = CollapseSelectionToStartOf(*rootElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ return rv;
+}
+
+/**
+ * Remove aNode, reparenting any children into the parent of aNode. In
+ * addition, insert any br's needed to preserve identity of removed block.
+ */
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::RemoveBlockContainerWithTransaction(Element& aElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Two possibilities: the container could be empty of editable content. If
+ // that is the case, we need to compare what is before and after aNode to
+ // determine if we need a br.
+ //
+ // Or it could be not empty, in which case we have to compare previous
+ // sibling and first child to determine if we need a leading br, and compare
+ // following sibling and last child to determine if we need a trailing br.
+
+ EditorDOMPoint pointToPutCaret;
+ if (nsCOMPtr<nsIContent> child = HTMLEditUtils::GetFirstChild(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode})) {
+ // The case of aNode not being empty. We need a br at start unless:
+ // 1) previous sibling of aNode is a block, OR
+ // 2) previous sibling of aNode is a br, OR
+ // 3) first child of aNode is a block OR
+ // 4) either is null
+
+ if (nsIContent* previousSibling = HTMLEditUtils::GetPreviousSibling(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (!HTMLEditUtils::IsBlockElement(
+ *previousSibling,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle) &&
+ !previousSibling->IsHTMLElement(nsGkAtoms::br) &&
+ !HTMLEditUtils::IsBlockElement(
+ *child, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ // Insert br node
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes,
+ EditorDOMPoint(&aElement, 0u));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ unwrappedInsertBRElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ }
+ }
+
+ // We need a br at end unless:
+ // 1) following sibling of aNode is a block, OR
+ // 2) last child of aNode is a block, OR
+ // 3) last child of aNode is a br OR
+ // 4) either is null
+
+ if (nsIContent* nextSibling = HTMLEditUtils::GetNextSibling(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (nextSibling &&
+ !HTMLEditUtils::IsBlockElement(
+ *nextSibling, BlockInlineCheck::UseComputedDisplayStyle)) {
+ if (nsIContent* lastChild = HTMLEditUtils::GetLastChild(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused)) {
+ if (!HTMLEditUtils::IsBlockElement(
+ *lastChild, BlockInlineCheck::UseComputedDisplayStyle) &&
+ !lastChild->IsHTMLElement(nsGkAtoms::br)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes,
+ EditorDOMPoint::AtEndOf(aElement));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ unwrappedInsertBRElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ }
+ }
+ }
+ }
+ } else if (nsIContent* previousSibling = HTMLEditUtils::GetPreviousSibling(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode})) {
+ // The case of aNode being empty. We need a br at start unless:
+ // 1) previous sibling of aNode is a block, OR
+ // 2) previous sibling of aNode is a br, OR
+ // 3) following sibling of aNode is a block, OR
+ // 4) following sibling of aNode is a br OR
+ // 5) either is null
+ if (!HTMLEditUtils::IsBlockElement(
+ *previousSibling, BlockInlineCheck::UseComputedDisplayStyle) &&
+ !previousSibling->IsHTMLElement(nsGkAtoms::br)) {
+ if (nsIContent* nextSibling = HTMLEditUtils::GetNextSibling(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (!HTMLEditUtils::IsBlockElement(
+ *nextSibling, BlockInlineCheck::UseComputedDisplayStyle) &&
+ !nextSibling->IsHTMLElement(nsGkAtoms::br)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes,
+ EditorDOMPoint(&aElement, 0u));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ unwrappedInsertBRElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ }
+ }
+ }
+ }
+
+ // Now remove container
+ Result<EditorDOMPoint, nsresult> unwrapBlockElementResult =
+ RemoveContainerWithTransaction(aElement);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapBlockElementResult;
+ }
+ if (AllowsTransactionsToChangeSelection() &&
+ unwrapBlockElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapBlockElementResult.unwrap();
+ }
+ return pointToPutCaret; // May be unset
+}
+
+Result<SplitNodeResult, nsresult> HTMLEditor::SplitNodeWithTransaction(
+ const EditorDOMPoint& aStartOfRightNode) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aStartOfRightNode.IsInContentNode())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+ MOZ_ASSERT(aStartOfRightNode.IsSetAndValid());
+
+ if (NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(
+ *aStartOfRightNode.ContainerAs<nsIContent>()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSplitNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ RefPtr<SplitNodeTransaction> transaction =
+ SplitNodeTransaction::Create(*this, aStartOfRightNode);
+ nsresult rv = DoTransactionInternal(transaction);
+ if (NS_WARN_IF(Destroyed())) {
+ NS_WARNING(
+ "EditorBase::DoTransactionInternal() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DoTransactionInternal() failed");
+ return Err(rv);
+ }
+
+ nsIContent* newContent = transaction->GetNewContent();
+ nsIContent* splitContent = transaction->GetSplitContent();
+ if (NS_WARN_IF(!newContent) || NS_WARN_IF(!splitContent)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ TopLevelEditSubActionDataRef().DidSplitContent(*this, *splitContent,
+ *newContent);
+ if (NS_WARN_IF(!newContent->IsInComposedDoc()) ||
+ NS_WARN_IF(!splitContent->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ return SplitNodeResult(*newContent, *splitContent);
+}
+
+Result<SplitNodeResult, nsresult> HTMLEditor::SplitNodeDeepWithTransaction(
+ nsIContent& aMostAncestorToSplit,
+ const EditorDOMPoint& aDeepestStartOfRightNode,
+ SplitAtEdges aSplitAtEdges) {
+ MOZ_ASSERT(aDeepestStartOfRightNode.IsSetAndValidInComposedDoc());
+ MOZ_ASSERT(
+ aDeepestStartOfRightNode.GetContainer() == &aMostAncestorToSplit ||
+ EditorUtils::IsDescendantOf(*aDeepestStartOfRightNode.GetContainer(),
+ aMostAncestorToSplit));
+
+ if (NS_WARN_IF(!aDeepestStartOfRightNode.IsInComposedDoc())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ nsCOMPtr<nsIContent> newLeftNodeOfMostAncestor;
+ EditorDOMPoint atStartOfRightNode(aDeepestStartOfRightNode);
+ // lastResult is as explained by its name, the last result which may not be
+ // split a node actually.
+ SplitNodeResult lastResult = SplitNodeResult::NotHandled(atStartOfRightNode);
+ MOZ_ASSERT(lastResult.AtSplitPoint<EditorRawDOMPoint>()
+ .IsSetAndValidInComposedDoc());
+
+ while (true) {
+ // Need to insert rules code call here to do things like not split a list
+ // if you are after the last <li> or before the first, etc. For now we
+ // just have some smarts about unnecessarily splitting text nodes, which
+ // should be universal enough to put straight in this EditorBase routine.
+ auto* splittingContent = atStartOfRightNode.GetContainerAs<nsIContent>();
+ if (NS_WARN_IF(!splittingContent)) {
+ lastResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_FAILURE);
+ }
+ // If we meet an orphan node before meeting aMostAncestorToSplit, we need
+ // to stop splitting. This is a bug of the caller.
+ if (NS_WARN_IF(splittingContent != &aMostAncestorToSplit &&
+ !atStartOfRightNode.GetContainerParentAs<nsIContent>())) {
+ lastResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_FAILURE);
+ }
+ // If the container is not splitable node such as comment node, atomic
+ // element, etc, we should keep it as-is, and try to split its parents.
+ if (!HTMLEditUtils::IsSplittableNode(*splittingContent)) {
+ if (splittingContent == &aMostAncestorToSplit) {
+ return lastResult;
+ }
+ atStartOfRightNode.Set(splittingContent);
+ continue;
+ }
+
+ // If the split point is middle of the node or the node is not a text node
+ // and we're allowed to create empty element node, split it.
+ if ((aSplitAtEdges == SplitAtEdges::eAllowToCreateEmptyContainer &&
+ !atStartOfRightNode.IsInTextNode()) ||
+ (!atStartOfRightNode.IsStartOfContainer() &&
+ !atStartOfRightNode.IsEndOfContainer())) {
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitNodeWithTransaction(atStartOfRightNode);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ lastResult.IgnoreCaretPointSuggestion();
+ return splitNodeResult;
+ }
+ lastResult = SplitNodeResult::MergeWithDeeperSplitNodeResult(
+ splitNodeResult.unwrap(), lastResult);
+ if (NS_WARN_IF(!lastResult.AtSplitPoint<EditorRawDOMPoint>()
+ .IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(lastResult.HasCaretPointSuggestion());
+ MOZ_ASSERT(lastResult.GetOriginalContent() == splittingContent);
+ if (splittingContent == &aMostAncestorToSplit) {
+ // Actually, we split aMostAncestorToSplit.
+ return lastResult;
+ }
+
+ // Then, try to split its parent before current node.
+ atStartOfRightNode = lastResult.AtNextContent<EditorDOMPoint>();
+ }
+ // If the split point is end of the node and it is a text node or we're not
+ // allowed to create empty container node, try to split its parent after it.
+ else if (!atStartOfRightNode.IsStartOfContainer()) {
+ lastResult = SplitNodeResult::HandledButDidNotSplitDueToEndOfContainer(
+ *splittingContent, &lastResult);
+ MOZ_ASSERT(lastResult.AtSplitPoint<EditorRawDOMPoint>()
+ .IsSetAndValidInComposedDoc());
+ if (splittingContent == &aMostAncestorToSplit) {
+ return lastResult;
+ }
+
+ // Try to split its parent after current node.
+ atStartOfRightNode.SetAfter(splittingContent);
+ }
+ // If the split point is start of the node and it is a text node or we're
+ // not allowed to create empty container node, try to split its parent.
+ else {
+ if (splittingContent == &aMostAncestorToSplit) {
+ return SplitNodeResult::HandledButDidNotSplitDueToStartOfContainer(
+ *splittingContent, &lastResult);
+ }
+
+ // Try to split its parent before current node.
+ // XXX This is logically wrong. If we've already split something but
+ // this is the last splitable content node in the limiter, this
+ // method will return "not handled".
+ lastResult = SplitNodeResult::NotHandled(atStartOfRightNode, &lastResult);
+ MOZ_ASSERT(lastResult.AtSplitPoint<EditorRawDOMPoint>()
+ .IsSetAndValidInComposedDoc());
+ atStartOfRightNode.Set(splittingContent);
+ MOZ_ASSERT(atStartOfRightNode.IsSetAndValidInComposedDoc());
+ }
+ }
+
+ // Not reached because while (true) loop never breaks.
+}
+
+Result<SplitNodeResult, nsresult> HTMLEditor::DoSplitNode(
+ const EditorDOMPoint& aStartOfRightNode, nsIContent& aNewNode) {
+ // Ensure computing the offset if it's initialized with a child content node.
+ Unused << aStartOfRightNode.Offset();
+
+ // XXX Perhaps, aStartOfRightNode may be invalid if this is a redo
+ // operation after modifying DOM node with JS.
+ if (NS_WARN_IF(!aStartOfRightNode.IsInContentNode())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+ MOZ_ASSERT(aStartOfRightNode.IsSetAndValid());
+
+ // Remember all selection points.
+ AutoTArray<SavedRange, 10> savedRanges;
+ for (SelectionType selectionType : kPresentSelectionTypes) {
+ SavedRange savingRange;
+ savingRange.mSelection = GetSelection(selectionType);
+ if (NS_WARN_IF(!savingRange.mSelection &&
+ selectionType == SelectionType::eNormal)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (!savingRange.mSelection) {
+ // For non-normal selections, skip over the non-existing ones.
+ continue;
+ }
+
+ for (uint32_t j : IntegerRange(savingRange.mSelection->RangeCount())) {
+ const nsRange* r = savingRange.mSelection->GetRangeAt(j);
+ MOZ_ASSERT(r);
+ MOZ_ASSERT(r->IsPositioned());
+ // XXX Looks like that SavedRange should have mStart and mEnd which
+ // are RangeBoundary. Then, we can avoid to compute offset here.
+ savingRange.mStartContainer = r->GetStartContainer();
+ savingRange.mStartOffset = r->StartOffset();
+ savingRange.mEndContainer = r->GetEndContainer();
+ savingRange.mEndOffset = r->EndOffset();
+
+ savedRanges.AppendElement(savingRange);
+ }
+ }
+
+ nsCOMPtr<nsINode> parent = aStartOfRightNode.GetContainerParent();
+ if (NS_WARN_IF(!parent)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Fix the child before mutation observer may touch the DOM tree.
+ nsIContent* firstChildOfRightNode = aStartOfRightNode.GetChild();
+ IgnoredErrorResult error;
+ parent->InsertBefore(
+ aNewNode, aStartOfRightNode.GetContainer()->GetNextSibling(), error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("nsINode::InsertBefore() failed");
+ return Err(error.StealNSResult());
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT_IF(aStartOfRightNode.IsInTextNode(), aNewNode.IsText());
+ MOZ_DIAGNOSTIC_ASSERT_IF(!aStartOfRightNode.IsInTextNode(),
+ !aNewNode.IsText());
+
+ // If we are splitting a text node, we need to move its some data to the
+ // new text node.
+ if (aStartOfRightNode.IsInTextNode()) {
+ if (!aStartOfRightNode.IsEndOfContainer()) {
+ Text* originalTextNode = aStartOfRightNode.ContainerAs<Text>();
+ Text* newTextNode = aNewNode.AsText();
+ nsAutoString movingText;
+ const uint32_t cutStartOffset = aStartOfRightNode.Offset();
+ const uint32_t cutLength =
+ originalTextNode->Length() - aStartOfRightNode.Offset();
+ IgnoredErrorResult error;
+ originalTextNode->SubstringData(cutStartOffset, cutLength, movingText,
+ error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "Text::SubstringData() failed, but ignored");
+ error.SuppressException();
+
+ // XXX This call may destroy us.
+ DoDeleteText(MOZ_KnownLive(*originalTextNode), cutStartOffset, cutLength,
+ error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "EditorBase::DoDeleteText() failed, but ignored");
+ error.SuppressException();
+
+ // XXX This call may destroy us.
+ DoSetText(MOZ_KnownLive(*newTextNode), movingText, error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "EditorBase::DoSetText() failed, but ignored");
+ }
+ }
+ // If the node has been moved to different parent, we should do nothing
+ // since web apps should handle everything in such case.
+ else if (firstChildOfRightNode &&
+ aStartOfRightNode.GetContainer() !=
+ firstChildOfRightNode->GetParentNode()) {
+ NS_WARNING(
+ "The web app interrupted us and touched the DOM tree, we stopped "
+ "splitting anything");
+ } else {
+ // If the right node is new one and there is no children or splitting at
+ // end of the node, we need to do nothing.
+ if (!firstChildOfRightNode) {
+ // Do nothing.
+ }
+ // If the right node is new one and splitting at start of the container,
+ // we need to move all children to the new right node.
+ else if (!firstChildOfRightNode->GetPreviousSibling()) {
+ // XXX Why do we ignore an error while moving nodes from the right
+ // node to the left node?
+ nsresult rv = MoveAllChildren(*aStartOfRightNode.GetContainer(),
+ EditorRawDOMPoint(&aNewNode, 0u));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::MoveAllChildren() failed, but ignored");
+ }
+ // If the right node is new one and splitting at middle of the node, we need
+ // to move inclusive next siblings of the split point to the new right node.
+ else {
+ // XXX Why do we ignore an error while moving nodes from the right node
+ // to the left node?
+ nsresult rv = MoveInclusiveNextSiblings(*firstChildOfRightNode,
+ EditorRawDOMPoint(&aNewNode, 0u));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::MoveInclusiveNextSiblings() failed, but ignored");
+ }
+ }
+
+ // Handle selection
+ // TODO: Stop doing this, this shouldn't be necessary to update selection.
+ if (RefPtr<PresShell> presShell = GetPresShell()) {
+ presShell->FlushPendingNotifications(FlushType::Frames);
+ }
+ NS_WARNING_ASSERTION(!Destroyed(),
+ "The editor is destroyed during splitting a node");
+
+ const bool allowedTransactionsToChangeSelection =
+ AllowsTransactionsToChangeSelection();
+
+ RefPtr<Selection> previousSelection;
+ for (SavedRange& savedRange : savedRanges) {
+ // If we have not seen the selection yet, clear all of its ranges.
+ if (savedRange.mSelection != previousSelection) {
+ MOZ_KnownLive(savedRange.mSelection)->RemoveAllRanges(error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("Selection::RemoveAllRanges() failed");
+ return Err(error.StealNSResult());
+ }
+ previousSelection = savedRange.mSelection;
+ }
+
+ // XXX Looks like that we don't need to modify normal selection here
+ // because selection will be modified by the caller if
+ // AllowsTransactionsToChangeSelection() will return true.
+ if (allowedTransactionsToChangeSelection &&
+ savedRange.mSelection->Type() == SelectionType::eNormal) {
+ // If the editor should adjust the selection, don't bother restoring
+ // the ranges for the normal selection here.
+ continue;
+ }
+
+ auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer,
+ uint32_t& aOffset) {
+ if (aContainer != aStartOfRightNode.GetContainer()) {
+ return;
+ }
+
+ // If the container is the left node and offset is after the split
+ // point, the content was moved from the right node to aNewNode.
+ // So, we need to change the container to aNewNode and decrease the
+ // offset.
+ if (aOffset >= aStartOfRightNode.Offset()) {
+ aContainer = &aNewNode;
+ aOffset -= aStartOfRightNode.Offset();
+ }
+ };
+ AdjustDOMPoint(savedRange.mStartContainer, savedRange.mStartOffset);
+ AdjustDOMPoint(savedRange.mEndContainer, savedRange.mEndOffset);
+
+ RefPtr<nsRange> newRange =
+ nsRange::Create(savedRange.mStartContainer, savedRange.mStartOffset,
+ savedRange.mEndContainer, savedRange.mEndOffset, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("nsRange::Create() failed");
+ return Err(error.StealNSResult());
+ }
+ // The `MOZ_KnownLive` annotation is only necessary because of a bug
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1622253) in the
+ // static analyzer.
+ MOZ_KnownLive(savedRange.mSelection)
+ ->AddRangeAndSelectFramesAndNotifyListeners(*newRange, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING(
+ "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed");
+ return Err(error.StealNSResult());
+ }
+ }
+
+ // We don't need to set selection here because the caller should do that
+ // in any case.
+
+ // If splitting the node causes running mutation event listener and we've
+ // got unexpected result, we should return error because callers will
+ // continue to do their work without complicated DOM tree result.
+ // NOTE: Perhaps, we shouldn't do this immediately after each DOM tree change
+ // because stopping handling it causes some data loss. E.g., user
+ // may loose the text which is moved to the new text node.
+ // XXX We cannot check all descendants in the right node and the new left
+ // node for performance reason. I think that if caller needs to access
+ // some of the descendants, they should check by themselves.
+ if (NS_WARN_IF(parent != aStartOfRightNode.GetContainer()->GetParentNode()) ||
+ NS_WARN_IF(parent != aNewNode.GetParentNode()) ||
+ NS_WARN_IF(aNewNode.GetPreviousSibling() !=
+ aStartOfRightNode.GetContainer())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ DebugOnly<nsresult> rvIgnored = RangeUpdaterRef().SelAdjSplitNode(
+ *aStartOfRightNode.ContainerAs<nsIContent>(), aStartOfRightNode.Offset(),
+ aNewNode);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "RangeUpdater::SelAdjSplitNode() failed, but ignored");
+
+ return SplitNodeResult(aNewNode,
+ *aStartOfRightNode.ContainerAs<nsIContent>());
+}
+
+Result<JoinNodesResult, nsresult> HTMLEditor::JoinNodesWithTransaction(
+ nsIContent& aLeftContent, nsIContent& aRightContent) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(&aLeftContent != &aRightContent);
+ MOZ_ASSERT(aLeftContent.GetParentNode());
+ MOZ_ASSERT(aRightContent.GetParentNode());
+ MOZ_ASSERT(aLeftContent.GetParentNode() == aRightContent.GetParentNode());
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eJoinNodes, nsIEditor::ePrevious, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ if (NS_WARN_IF(!aRightContent.GetParentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ RefPtr<JoinNodesTransaction> transaction =
+ JoinNodesTransaction::MaybeCreate(*this, aLeftContent, aRightContent);
+ if (MOZ_UNLIKELY(!transaction)) {
+ NS_WARNING("JoinNodesTransaction::MaybeCreate() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ const nsresult rv = DoTransactionInternal(transaction);
+ // FYI: Now, DidJoinNodesTransaction() must have been run if succeeded.
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ // This shouldn't occur unless the cycle collector runs by chrome script
+ // forcibly.
+ if (NS_WARN_IF(!transaction->GetRemovedContent()) ||
+ NS_WARN_IF(!transaction->GetExistingContent())) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ // If joined node is moved to different place, offset may not have any
+ // meaning. In this case, the web app modified the DOM tree takes on the
+ // responsibility for the remaning things.
+ if (NS_WARN_IF(transaction->GetExistingContent()->GetParent() !=
+ transaction->GetParentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DoTransactionInternal() failed");
+ return Err(rv);
+ }
+
+ return JoinNodesResult(transaction->CreateJoinedPoint<EditorDOMPoint>(),
+ *transaction->GetRemovedContent());
+}
+
+void HTMLEditor::DidJoinNodesTransaction(
+ const JoinNodesTransaction& aTransaction, nsresult aDoJoinNodesResult) {
+ // This shouldn't occur unless the cycle collector runs by chrome script
+ // forcibly.
+ if (MOZ_UNLIKELY(NS_WARN_IF(!aTransaction.GetRemovedContent()) ||
+ NS_WARN_IF(!aTransaction.GetExistingContent()))) {
+ return;
+ }
+
+ // If joined node is moved to different place, offset may not have any
+ // meaning. In this case, the web app modified the DOM tree takes on the
+ // responsibility for the remaning things.
+ if (MOZ_UNLIKELY(aTransaction.GetExistingContent()->GetParentNode() !=
+ aTransaction.GetParentNode())) {
+ return;
+ }
+
+ // Be aware, the joined point should be created for each call because
+ // they may refer the child node, but some of them may change the DOM tree
+ // after that, thus we need to avoid invalid point (Although it shouldn't
+ // occur).
+ TopLevelEditSubActionDataRef().DidJoinContents(
+ *this, aTransaction.CreateJoinedPoint<EditorRawDOMPoint>());
+
+ if (NS_SUCCEEDED(aDoJoinNodesResult)) {
+ if (RefPtr<TextServicesDocument> textServicesDocument =
+ mTextServicesDocument) {
+ textServicesDocument->DidJoinContents(
+ aTransaction.CreateJoinedPoint<EditorRawDOMPoint>(),
+ *aTransaction.GetRemovedContent());
+ }
+ }
+
+ if (!mActionListeners.IsEmpty()) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored = listener->DidJoinContents(
+ aTransaction.CreateJoinedPoint<EditorRawDOMPoint>(),
+ aTransaction.GetRemovedContent());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::DidJoinContents() failed, but ignored");
+ }
+ }
+}
+
+nsresult HTMLEditor::DoJoinNodes(nsIContent& aContentToKeep,
+ nsIContent& aContentToRemove) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ const uint32_t keepingContentLength = aContentToKeep.Length();
+ const EditorDOMPoint oldPointAtRightContent(&aContentToRemove);
+ if (MOZ_LIKELY(oldPointAtRightContent.IsSet())) {
+ Unused << oldPointAtRightContent.Offset(); // Fix the offset
+ }
+
+ // Remember all selection points.
+ // XXX Do we need to restore all types of selections by ourselves? Normal
+ // selection should be modified later as result of handling edit action.
+ // IME selections shouldn't be there when nodes are joined. Spellcheck
+ // selections should be recreated with newer text. URL selections
+ // shouldn't be there because of used only by the URL bar.
+ AutoTArray<SavedRange, 10> savedRanges;
+ {
+ EditorRawDOMPoint atRemovingNode(&aContentToRemove);
+ EditorRawDOMPoint atNodeToKeep(&aContentToKeep);
+ for (SelectionType selectionType : kPresentSelectionTypes) {
+ SavedRange savingRange;
+ savingRange.mSelection = GetSelection(selectionType);
+ if (selectionType == SelectionType::eNormal) {
+ if (NS_WARN_IF(!savingRange.mSelection)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (!savingRange.mSelection) {
+ // For non-normal selections, skip over the non-existing ones.
+ continue;
+ }
+
+ const uint32_t rangeCount = savingRange.mSelection->RangeCount();
+ for (const uint32_t j : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(savingRange.mSelection->RangeCount() == rangeCount);
+ const RefPtr<nsRange> r = savingRange.mSelection->GetRangeAt(j);
+ MOZ_ASSERT(r);
+ MOZ_ASSERT(r->IsPositioned());
+ savingRange.mStartContainer = r->GetStartContainer();
+ savingRange.mStartOffset = r->StartOffset();
+ savingRange.mEndContainer = r->GetEndContainer();
+ savingRange.mEndOffset = r->EndOffset();
+
+ // If selection endpoint is between the nodes, remember it as being
+ // in the one that is going away instead. This simplifies later
+ // selection adjustment logic at end of this method.
+ if (savingRange.mStartContainer) {
+ MOZ_ASSERT(savingRange.mEndContainer);
+ auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer,
+ uint32_t& aOffset) {
+ // If range boundary points aContentToRemove and aContentToKeep is
+ // its left node, remember it as being at end of aContentToKeep.
+ // Then, it will point start of the first content of moved content
+ // from aContentToRemove.
+ if (aContainer == atRemovingNode.GetContainer() &&
+ atNodeToKeep.Offset() < aOffset &&
+ aOffset <= atRemovingNode.Offset()) {
+ aContainer = &aContentToKeep;
+ aOffset = keepingContentLength;
+ }
+ };
+ AdjustDOMPoint(savingRange.mStartContainer, savingRange.mStartOffset);
+ AdjustDOMPoint(savingRange.mEndContainer, savingRange.mEndOffset);
+ }
+
+ savedRanges.AppendElement(savingRange);
+ }
+ }
+ }
+
+ // OK, ready to do join now.
+ nsresult rv = [&]() MOZ_CAN_RUN_SCRIPT {
+ // If it's a text node, just shuffle around some text.
+ if (aContentToKeep.IsText() && aContentToRemove.IsText()) {
+ nsAutoString rightText;
+ nsAutoString leftText;
+ aContentToRemove.AsText()->GetData(rightText);
+ aContentToKeep.AsText()->GetData(leftText);
+ leftText += rightText;
+ IgnoredErrorResult ignoredError;
+ DoSetText(MOZ_KnownLive(*aContentToKeep.AsText()), leftText,
+ ignoredError);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "EditorBase::DoSetText() failed, but ignored");
+ return NS_OK;
+ }
+ // Otherwise it's an interior node, so shuffle around the children.
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfChildContents;
+ HTMLEditUtils::CollectAllChildren(aContentToRemove, arrayOfChildContents);
+
+ for (const OwningNonNull<nsIContent>& child : arrayOfChildContents) {
+ IgnoredErrorResult error;
+ aContentToKeep.AppendChild(child, error);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (error.Failed()) {
+ NS_WARNING("nsINode::AppendChild() failed");
+ return error.StealNSResult();
+ }
+ }
+ return NS_OK;
+ }();
+
+ // Delete the extra node.
+ if (NS_SUCCEEDED(rv)) {
+ aContentToRemove.Remove();
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ }
+
+ if (MOZ_LIKELY(oldPointAtRightContent.IsSet())) {
+ DebugOnly<nsresult> rvIgnored = RangeUpdaterRef().SelAdjJoinNodes(
+ EditorRawDOMPoint(&aContentToKeep, std::min(keepingContentLength,
+ aContentToKeep.Length())),
+ aContentToRemove, oldPointAtRightContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "RangeUpdater::SelAdjJoinNodes() failed, but ignored");
+ }
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ const bool allowedTransactionsToChangeSelection =
+ AllowsTransactionsToChangeSelection();
+
+ // And adjust the selection if needed.
+ RefPtr<Selection> previousSelection;
+ for (SavedRange& savedRange : savedRanges) {
+ // If we have not seen the selection yet, clear all of its ranges.
+ if (savedRange.mSelection != previousSelection) {
+ IgnoredErrorResult error;
+ MOZ_KnownLive(savedRange.mSelection)->RemoveAllRanges(error);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (error.Failed()) {
+ NS_WARNING("Selection::RemoveAllRanges() failed");
+ return error.StealNSResult();
+ }
+ previousSelection = savedRange.mSelection;
+ }
+
+ if (allowedTransactionsToChangeSelection &&
+ savedRange.mSelection->Type() == SelectionType::eNormal) {
+ // If the editor should adjust the selection, don't bother restoring
+ // the ranges for the normal selection here.
+ continue;
+ }
+
+ auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer,
+ uint32_t& aOffset) {
+ // Now, all content of aContentToRemove are moved to end of
+ // aContentToKeep. Therefore, if a range boundary was in
+ // aContentToRemove, we need to change the container to aContentToKeep and
+ // adjust the offset to after the original content of aContentToKeep.
+ if (aContainer == &aContentToRemove) {
+ aContainer = &aContentToKeep;
+ aOffset += keepingContentLength;
+ }
+ };
+ AdjustDOMPoint(savedRange.mStartContainer, savedRange.mStartOffset);
+ AdjustDOMPoint(savedRange.mEndContainer, savedRange.mEndOffset);
+
+ const RefPtr<nsRange> newRange = nsRange::Create(
+ savedRange.mStartContainer, savedRange.mStartOffset,
+ savedRange.mEndContainer, savedRange.mEndOffset, IgnoreErrors());
+ if (!newRange) {
+ NS_WARNING("nsRange::Create() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ IgnoredErrorResult error;
+ // The `MOZ_KnownLive` annotation is only necessary because of a bug
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1622253) in the
+ // static analyzer.
+ MOZ_KnownLive(savedRange.mSelection)
+ ->AddRangeAndSelectFramesAndNotifyListeners(*newRange, error);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+ }
+
+ if (allowedTransactionsToChangeSelection) {
+ // Editor wants us to set selection at join point.
+ DebugOnly<nsresult> rvIgnored = CollapseSelectionToStartOf(aContentToKeep);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBases::CollapseSelectionTos() failed, but ignored");
+ }
+
+ return NS_OK;
+}
+
+Result<MoveNodeResult, nsresult> HTMLEditor::MoveNodeWithTransaction(
+ nsIContent& aContentToMove, const EditorDOMPoint& aPointToInsert) {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ EditorDOMPoint oldPoint(&aContentToMove);
+ if (NS_WARN_IF(!oldPoint.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Don't do anything if it's already in right place.
+ if (aPointToInsert == oldPoint) {
+ return MoveNodeResult::IgnoredResult(aPointToInsert.NextPoint());
+ }
+
+ RefPtr<MoveNodeTransaction> moveNodeTransaction =
+ MoveNodeTransaction::MaybeCreate(*this, aContentToMove, aPointToInsert);
+ if (MOZ_UNLIKELY(!moveNodeTransaction)) {
+ NS_WARNING("MoveNodeTransaction::MaybeCreate() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eMoveNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ TopLevelEditSubActionDataRef().WillDeleteContent(*this, aContentToMove);
+
+ nsresult rv = DoTransactionInternal(moveNodeTransaction);
+ if (NS_SUCCEEDED(rv)) {
+ if (mTextServicesDocument) {
+ const OwningNonNull<TextServicesDocument> textServicesDocument =
+ *mTextServicesDocument;
+ textServicesDocument->DidDeleteContent(aContentToMove);
+ }
+ }
+
+ if (!mActionListeners.IsEmpty()) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored =
+ listener->DidDeleteNode(&aContentToMove, rv);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::DidDeleteNode() failed, but ignored");
+ }
+ }
+
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING(
+ "MoveNodeTransaction::DoTransaction() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeTransaction::DoTransaction() failed");
+ return Err(rv);
+ }
+
+ TopLevelEditSubActionDataRef().DidInsertContent(*this, aContentToMove);
+
+ return MoveNodeResult::HandledResult(
+ moveNodeTransaction->SuggestNextInsertionPoint<EditorDOMPoint>(),
+ moveNodeTransaction->SuggestPointToPutCaret<EditorDOMPoint>());
+}
+
+Result<RefPtr<Element>, nsresult> HTMLEditor::DeleteSelectionAndCreateElement(
+ nsAtom& aTag, const InitializeInsertingElement& aInitializer) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsresult rv = DeleteSelectionAndPrepareToCreateNode();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteSelectionAndPrepareToCreateNode() failed");
+ return Err(rv);
+ }
+
+ EditorDOMPoint pointToInsert(SelectionRef().AnchorRef());
+ if (!pointToInsert.IsSet()) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<CreateElementResult, nsresult> createNewElementResult =
+ CreateAndInsertElement(WithTransaction::Yes, aTag, pointToInsert,
+ aInitializer);
+ if (MOZ_UNLIKELY(createNewElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewElementResult.propagateErr();
+ }
+ MOZ_ASSERT(createNewElementResult.inspect().GetNewNode());
+
+ // We want the selection to be just after the new node
+ createNewElementResult.inspect().IgnoreCaretPointSuggestion();
+ rv = CollapseSelectionTo(
+ EditorRawDOMPoint::After(*createNewElementResult.inspect().GetNewNode()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ return createNewElementResult.unwrap().UnwrapNewNode();
+}
+
+nsresult HTMLEditor::DeleteSelectionAndPrepareToCreateNode() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!SelectionRef().GetAnchorFocusRange())) {
+ return NS_OK;
+ }
+
+ if (!SelectionRef().GetAnchorFocusRange()->Collapsed()) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteSelectionAsSubAction() failed");
+ return rv;
+ }
+ MOZ_ASSERT(SelectionRef().GetAnchorFocusRange() &&
+ SelectionRef().GetAnchorFocusRange()->Collapsed(),
+ "Selection not collapsed after delete");
+ }
+
+ // If the selection is a chardata node, split it if necessary and compute
+ // where to put the new node
+ EditorDOMPoint atAnchor(SelectionRef().AnchorRef());
+ if (NS_WARN_IF(!atAnchor.IsSet()) || !atAnchor.IsInDataNode()) {
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!atAnchor.GetContainerParent())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (atAnchor.IsStartOfContainer()) {
+ const EditorRawDOMPoint atAnchorContainer(atAnchor.GetContainer());
+ if (NS_WARN_IF(!atAnchorContainer.IsSetAndValid())) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = CollapseSelectionTo(atAnchorContainer);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+
+ if (atAnchor.IsEndOfContainer()) {
+ EditorRawDOMPoint afterAnchorContainer(atAnchor.GetContainer());
+ if (NS_WARN_IF(!afterAnchorContainer.AdvanceOffset())) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = CollapseSelectionTo(afterAnchorContainer);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+
+ Result<SplitNodeResult, nsresult> splitAtAnchorResult =
+ SplitNodeWithTransaction(atAnchor);
+ if (MOZ_UNLIKELY(splitAtAnchorResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitAtAnchorResult.unwrapErr();
+ }
+
+ splitAtAnchorResult.inspect().IgnoreCaretPointSuggestion();
+ const auto atRightContent =
+ splitAtAnchorResult.inspect().AtNextContent<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!atRightContent.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atRightContent.IsSetAndValid());
+ nsresult rv = CollapseSelectionTo(atRightContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+bool HTMLEditor::IsEmpty() const {
+ if (mPaddingBRElementForEmptyEditor) {
+ return true;
+ }
+
+ // XXX Oddly, we check body or document element's state instead of
+ // active editing host. Must be a bug.
+ Element* bodyOrDocumentElement = GetRoot();
+ if (!bodyOrDocumentElement) {
+ return true;
+ }
+
+ for (nsIContent* childContent = bodyOrDocumentElement->GetFirstChild();
+ childContent; childContent = childContent->GetNextSibling()) {
+ if (!childContent->IsText() || childContent->Length()) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// add to aElement the CSS inline styles corresponding to the HTML attribute
+// aAttribute with its value aValue
+nsresult HTMLEditor::SetAttributeOrEquivalent(Element* aElement,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ bool aSuppressTransaction) {
+ MOZ_ASSERT(aElement);
+ MOZ_ASSERT(aAttribute);
+
+ nsAutoScriptBlocker scriptBlocker;
+ nsStyledElement* styledElement = nsStyledElement::FromNodeOrNull(aElement);
+ if (!IsCSSEnabled()) {
+ // we are not in an HTML+CSS editor; let's set the attribute the HTML way
+ if (EditorElementStyle::IsHTMLStyle(aAttribute)) {
+ const EditorElementStyle elementStyle =
+ EditorElementStyle::Create(*aAttribute);
+ if (styledElement && elementStyle.IsCSSRemovable(*styledElement)) {
+ // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must
+ // be guaranteed by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ nsresult rv = CSSEditUtils::RemoveCSSEquivalentToStyle(
+ aSuppressTransaction ? WithTransaction::No : WithTransaction::Yes,
+ *this, MOZ_KnownLive(*styledElement), elementStyle, nullptr);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSEquivalentToStyle() failed, but ignored");
+ }
+ }
+ if (aSuppressTransaction) {
+ nsresult rv =
+ aElement->SetAttr(kNameSpaceID_None, aAttribute, aValue, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr() failed");
+ return rv;
+ }
+ nsresult rv = SetAttributeWithTransaction(*aElement, *aAttribute, aValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction() failed");
+ return rv;
+ }
+
+ if (EditorElementStyle::IsHTMLStyle(aAttribute)) {
+ const EditorElementStyle elementStyle =
+ EditorElementStyle::Create(*aAttribute);
+ if (styledElement && elementStyle.IsCSSSettable(*styledElement)) {
+ // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must
+ // be guaranteed by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ Result<size_t, nsresult> count = CSSEditUtils::SetCSSEquivalentToStyle(
+ aSuppressTransaction ? WithTransaction::No : WithTransaction::Yes,
+ *this, MOZ_KnownLive(*styledElement), elementStyle, &aValue);
+ if (MOZ_UNLIKELY(count.isErr())) {
+ if (NS_WARN_IF(count.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle() failed, but ignored");
+ }
+ if (count.inspect()) {
+ // we found an equivalence ; let's remove the HTML attribute itself if
+ // it is set
+ nsAutoString existingValue;
+ if (!aElement->GetAttr(aAttribute, existingValue)) {
+ return NS_OK;
+ }
+
+ if (aSuppressTransaction) {
+ nsresult rv =
+ aElement->UnsetAttr(kNameSpaceID_None, aAttribute, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::UnsetAttr() failed");
+ return rv;
+ }
+ nsresult rv = RemoveAttributeWithTransaction(*aElement, *aAttribute);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction() failed");
+ return rv;
+ }
+ }
+ }
+
+ // count is an integer that represents the number of CSS declarations
+ // applied to the element. If it is zero, we found no equivalence in this
+ // implementation for the attribute
+ if (aAttribute == nsGkAtoms::style) {
+ // if it is the style attribute, just add the new value to the existing
+ // style attribute's value
+ nsString existingValue; // Use nsString to avoid copying the string
+ // buffer at setting the attribute below.
+ aElement->GetAttr(nsGkAtoms::style, existingValue);
+ if (!existingValue.IsEmpty()) {
+ existingValue.Append(HTMLEditUtils::kSpace);
+ }
+ existingValue.Append(aValue);
+ if (aSuppressTransaction) {
+ nsresult rv = aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+ existingValue, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Element::SetAttr(nsGkAtoms::style) failed");
+ return rv;
+ }
+ nsresult rv = SetAttributeWithTransaction(*aElement, *nsGkAtoms::style,
+ existingValue);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::style) failed");
+ return rv;
+ }
+
+ // we have no CSS equivalence for this attribute and it is not the style
+ // attribute; let's set it the good'n'old HTML way
+ if (aSuppressTransaction) {
+ nsresult rv =
+ aElement->SetAttr(kNameSpaceID_None, aAttribute, aValue, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr() failed");
+ return rv;
+ }
+ nsresult rv = SetAttributeWithTransaction(*aElement, *aAttribute, aValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::RemoveAttributeOrEquivalent(Element* aElement,
+ nsAtom* aAttribute,
+ bool aSuppressTransaction) {
+ MOZ_ASSERT(aElement);
+ MOZ_ASSERT(aAttribute);
+
+ if (IsCSSEnabled() && EditorElementStyle::IsHTMLStyle(aAttribute)) {
+ const EditorElementStyle elementStyle =
+ EditorElementStyle::Create(*aAttribute);
+ if (elementStyle.IsCSSRemovable(*aElement)) {
+ // XXX It might be keep handling attribute even if aElement is not
+ // an nsStyledElement instance.
+ nsStyledElement* styledElement =
+ nsStyledElement::FromNodeOrNull(aElement);
+ if (NS_WARN_IF(!styledElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must
+ // be guaranteed by the caller because of MOZ_CAN_RUN_SCRIPT method.
+ nsresult rv = CSSEditUtils::RemoveCSSEquivalentToStyle(
+ aSuppressTransaction ? WithTransaction::No : WithTransaction::Yes,
+ *this, MOZ_KnownLive(*styledElement), elementStyle, nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::RemoveCSSEquivalentToStyle() failed");
+ return rv;
+ }
+ }
+ }
+
+ if (!aElement->HasAttr(aAttribute)) {
+ return NS_OK;
+ }
+
+ if (aSuppressTransaction) {
+ nsresult rv = aElement->UnsetAttr(kNameSpaceID_None, aAttribute,
+ /* aNotify = */ true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::UnsetAttr() failed");
+ return rv;
+ }
+ nsresult rv = RemoveAttributeWithTransaction(*aElement, *aAttribute);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::SetIsCSSEnabled(bool aIsCSSPrefChecked) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eEnableOrDisableCSS);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mIsCSSPrefChecked = aIsCSSPrefChecked;
+ return NS_OK;
+}
+
+// Set the block background color
+nsresult HTMLEditor::SetBlockBackgroundColorWithCSSAsSubAction(
+ const nsAString& aColor) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // background-color change and committing composition should be undone
+ // together
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ CommitComposition();
+
+ // XXX Shouldn't we do this before calling `CommitComposition()`?
+ if (IsPlaintextMailComposer()) {
+ return NS_OK;
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertElement, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() "
+ "failed, but ignored");
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ AutoRangeArray selectionRanges(SelectionRef());
+ MOZ_ALWAYS_TRUE(selectionRanges.SaveAndTrackRanges(*this));
+ for (const OwningNonNull<nsRange>& domRange : selectionRanges.Ranges()) {
+ EditorDOMRange range(domRange);
+ if (NS_WARN_IF(!range.IsPositioned())) {
+ continue;
+ }
+
+ if (range.InSameContainer()) {
+ // If the range is in a text node, set background color of its parent
+ // block.
+ if (range.StartRef().IsInTextNode()) {
+ const RefPtr<nsStyledElement> editableBlockStyledElement =
+ nsStyledElement::FromNodeOrNull(HTMLEditUtils::GetAncestorElement(
+ *range.StartRef().ContainerAs<Text>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle));
+ if (!editableBlockStyledElement ||
+ !EditorElementStyle::BGColor().IsCSSSettable(
+ *editableBlockStyledElement)) {
+ continue;
+ }
+ Result<size_t, nsresult> result = CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::Yes, *this, *editableBlockStyledElement,
+ EditorElementStyle::BGColor(), &aColor);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::"
+ "BGColor()) failed, but ignored");
+ }
+ continue;
+ }
+
+ // If `Selection` is collapsed in a `<body>` element, set background
+ // color of the `<body>` element.
+ if (range.Collapsed() &&
+ range.StartRef().IsContainerHTMLElement(nsGkAtoms::body)) {
+ const RefPtr<nsStyledElement> styledElement =
+ range.StartRef().GetContainerAs<nsStyledElement>();
+ if (!styledElement ||
+ !EditorElementStyle::BGColor().IsCSSSettable(*styledElement)) {
+ continue;
+ }
+ Result<size_t, nsresult> result = CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::Yes, *this, *styledElement,
+ EditorElementStyle::BGColor(), &aColor);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::"
+ "BGColor()) failed, but ignored");
+ }
+ continue;
+ }
+
+ // If one node is selected, set background color of it if it's a
+ // block, or of its parent block otherwise.
+ if ((range.StartRef().IsStartOfContainer() &&
+ range.EndRef().IsStartOfContainer()) ||
+ range.StartRef().Offset() + 1 == range.EndRef().Offset()) {
+ if (NS_WARN_IF(range.StartRef().IsInDataNode())) {
+ continue;
+ }
+ const RefPtr<nsStyledElement> editableBlockStyledElement =
+ nsStyledElement::FromNodeOrNull(
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *range.StartRef().GetChild(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle));
+ if (!editableBlockStyledElement ||
+ !EditorElementStyle::BGColor().IsCSSSettable(
+ *editableBlockStyledElement)) {
+ continue;
+ }
+ Result<size_t, nsresult> result = CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::Yes, *this, *editableBlockStyledElement,
+ EditorElementStyle::BGColor(), &aColor);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::"
+ "BGColor()) failed, but ignored");
+ }
+ continue;
+ }
+ } // if (range.InSameContainer())
+
+ // Collect editable nodes which are entirely contained in the range.
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ ContentSubtreeIterator subtreeIter;
+ // If there is no node which is entirely in the range,
+ // `ContentSubtreeIterator::Init()` fails, but this is possible case,
+ // don't warn it.
+ nsresult rv = subtreeIter.Init(range.StartRef().ToRawRangeBoundary(),
+ range.EndRef().ToRawRangeBoundary());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "ContentSubtreeIterator::Init() failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
+ nsINode* node = subtreeIter.GetCurrentNode();
+ if (NS_WARN_IF(!node)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (node->IsContent() && EditorUtils::IsEditableContent(
+ *node->AsContent(), EditorType::HTML)) {
+ arrayOfContents.AppendElement(*node->AsContent());
+ }
+ }
+ }
+ }
+
+ // This caches block parent if we set its background color.
+ RefPtr<Element> handledBlockParent;
+
+ // If start node is a text node, set background color of its parent
+ // block.
+ if (range.StartRef().IsInTextNode() &&
+ EditorUtils::IsEditableContent(*range.StartRef().ContainerAs<Text>(),
+ EditorType::HTML)) {
+ Element* const editableBlockElement = HTMLEditUtils::GetAncestorElement(
+ *range.StartRef().ContainerAs<Text>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (editableBlockElement && handledBlockParent != editableBlockElement) {
+ handledBlockParent = editableBlockElement;
+ nsStyledElement* const blockStyledElement =
+ nsStyledElement::FromNode(handledBlockParent);
+ if (blockStyledElement &&
+ EditorElementStyle::BGColor().IsCSSSettable(*blockStyledElement)) {
+ // MOZ_KnownLive(*blockStyledElement): It's handledBlockParent
+ // whose type is RefPtr.
+ Result<size_t, nsresult> result =
+ CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::Yes, *this,
+ MOZ_KnownLive(*blockStyledElement),
+ EditorElementStyle::BGColor(), &aColor);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::"
+ "BGColor()) failed, but ignored");
+ }
+ }
+ }
+ }
+
+ // Then, set background color of each block or block parent of all nodes
+ // in the range entirely.
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ Element* const editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ content, HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (editableBlockElement && handledBlockParent != editableBlockElement) {
+ handledBlockParent = editableBlockElement;
+ nsStyledElement* const blockStyledElement =
+ nsStyledElement::FromNode(handledBlockParent);
+ if (blockStyledElement &&
+ EditorElementStyle::BGColor().IsCSSSettable(*blockStyledElement)) {
+ // MOZ_KnownLive(*blockStyledElement): It's handledBlockParent whose
+ // type is RefPtr.
+ Result<size_t, nsresult> result =
+ CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::Yes, *this,
+ MOZ_KnownLive(*blockStyledElement),
+ EditorElementStyle::BGColor(), &aColor);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::"
+ "BGColor()) failed, but ignored");
+ }
+ }
+ }
+ }
+
+ // Finally, if end node is a text node, set background color of its
+ // parent block.
+ if (range.EndRef().IsInTextNode() &&
+ EditorUtils::IsEditableContent(*range.EndRef().ContainerAs<Text>(),
+ EditorType::HTML)) {
+ Element* const editableBlockElement = HTMLEditUtils::GetAncestorElement(
+ *range.EndRef().ContainerAs<Text>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (editableBlockElement && handledBlockParent != editableBlockElement) {
+ const RefPtr<nsStyledElement> blockStyledElement =
+ nsStyledElement::FromNode(editableBlockElement);
+ if (blockStyledElement &&
+ EditorElementStyle::BGColor().IsCSSSettable(*blockStyledElement)) {
+ Result<size_t, nsresult> result =
+ CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::Yes, *this, *blockStyledElement,
+ EditorElementStyle::BGColor(), &aColor);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::"
+ "BGColor()) failed, but ignored");
+ }
+ }
+ }
+ }
+ } // for-loop of selectionRanges
+
+ MOZ_ASSERT(selectionRanges.HasSavedRanges());
+ selectionRanges.RestoreFromSavedRanges();
+ nsresult rv = selectionRanges.ApplyTo(SelectionRef());
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::SetBackgroundColor(const nsAString& aColor) {
+ nsresult rv = SetBackgroundColorAsAction(aColor);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetBackgroundColorAsAction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SetBackgroundColorAsAction(const nsAString& aColor,
+ nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eSetBackgroundColor, aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (IsCSSEnabled()) {
+ // if we are in CSS mode, we have to apply the background color to the
+ // containing block (or the body if we have no block-level element in
+ // the document)
+ nsresult rv = SetBlockBackgroundColorWithCSSAsSubAction(aColor);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::SetBlockBackgroundColorWithCSSAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // but in HTML mode, we can only set the document's background color
+ rv = SetHTMLBackgroundColorWithTransaction(aColor);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::SetHTMLBackgroundColorWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::CopyLastEditableChildStylesWithTransaction(
+ Element& aPreviousBlock, Element& aNewBlock, const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // First, clear out aNewBlock. Contract is that we want only the styles
+ // from aPreviousBlock.
+ AutoTArray<OwningNonNull<nsIContent>, 32> newBlockChildren;
+ HTMLEditUtils::CollectAllChildren(aNewBlock, newBlockChildren);
+ for (const OwningNonNull<nsIContent>& child : newBlockChildren) {
+ // MOZ_KNownLive(child) because of bug 1622253
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(child));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ if (MOZ_UNLIKELY(aNewBlock.GetFirstChild())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // XXX aNewBlock may be moved or removed. Even in such case, we should
+ // keep cloning the styles?
+
+ // Look for the deepest last editable leaf node in aPreviousBlock.
+ // Then, if found one is a <br> element, look for non-<br> element.
+ nsIContent* deepestEditableContent = nullptr;
+ for (nsCOMPtr<nsIContent> child = &aPreviousBlock; child;
+ child = HTMLEditUtils::GetLastChild(
+ *child, {WalkTreeOption::IgnoreNonEditableNode})) {
+ deepestEditableContent = child;
+ }
+ while (deepestEditableContent &&
+ deepestEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ deepestEditableContent = HTMLEditUtils::GetPreviousContent(
+ *deepestEditableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost);
+ }
+ if (!deepestEditableContent) {
+ return EditorDOMPoint(&aNewBlock, 0u);
+ }
+
+ Element* deepestVisibleEditableElement =
+ deepestEditableContent->GetAsElementOrParentElement();
+ if (!deepestVisibleEditableElement) {
+ return EditorDOMPoint(&aNewBlock, 0u);
+ }
+
+ // Clone inline elements to keep current style in the new block.
+ // XXX Looks like that this is really slow if lastEditableDescendant is
+ // far from aPreviousBlock. Probably, we should clone inline containers
+ // from ancestor to descendants without transactions, then, insert it
+ // after that with transaction.
+ RefPtr<Element> lastClonedElement, firstClonedElement;
+ for (RefPtr<Element> elementInPreviousBlock = deepestVisibleEditableElement;
+ elementInPreviousBlock && elementInPreviousBlock != &aPreviousBlock;
+ elementInPreviousBlock = elementInPreviousBlock->GetParentElement()) {
+ if (!HTMLEditUtils::IsInlineStyle(elementInPreviousBlock) &&
+ !elementInPreviousBlock->IsHTMLElement(nsGkAtoms::span)) {
+ continue;
+ }
+ OwningNonNull<nsAtom> tagName =
+ *elementInPreviousBlock->NodeInfo()->NameAtom();
+ // At first time, just create the most descendant inline container
+ // element.
+ if (!firstClonedElement) {
+ Result<CreateElementResult, nsresult> createNewElementResult =
+ CreateAndInsertElement(
+ WithTransaction::Yes, tagName, EditorDOMPoint(&aNewBlock, 0u),
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [&elementInPreviousBlock](
+ HTMLEditor& aHTMLEditor, Element& aNewElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ // Clone all attributes. Note that despite the method name,
+ // CloneAttributesWithTransaction does not create
+ // transactions in this case because aNewElement has not
+ // been connected yet.
+ // XXX Looks like that this clones id attribute too.
+ aHTMLEditor.CloneAttributesWithTransaction(
+ aNewElement, *elementInPreviousBlock);
+ return NS_OK;
+ });
+ if (MOZ_UNLIKELY(createNewElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewElementResult =
+ createNewElementResult.unwrap();
+ // We'll return with a point suggesting new caret position and the
+ // following path does not require an update of selection here.
+ // Therefore, we don't need to update selection here.
+ unwrappedCreateNewElementResult.IgnoreCaretPointSuggestion();
+ firstClonedElement = lastClonedElement =
+ unwrappedCreateNewElementResult.UnwrapNewNode();
+ continue;
+ }
+ // Otherwise, inserts new parent inline container to the previous inserted
+ // inline container.
+ Result<CreateElementResult, nsresult> wrapClonedElementResult =
+ InsertContainerWithTransaction(*lastClonedElement, tagName);
+ if (MOZ_UNLIKELY(wrapClonedElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
+ return wrapClonedElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedWrapClonedElementResult =
+ wrapClonedElementResult.unwrap();
+ // We'll return with a point suggesting new caret so that we don't need to
+ // update selection here.
+ unwrappedWrapClonedElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedWrapClonedElementResult.GetNewNode());
+ lastClonedElement = unwrappedWrapClonedElementResult.UnwrapNewNode();
+ CloneAttributesWithTransaction(*lastClonedElement, *elementInPreviousBlock);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ }
+
+ if (!firstClonedElement) {
+ // XXX Even if no inline elements are cloned, shouldn't we create new
+ // <br> element for aNewBlock?
+ return EditorDOMPoint(&aNewBlock, 0u);
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult = InsertBRElement(
+ WithTransaction::Yes, EditorDOMPoint(firstClonedElement, 0u));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ return EditorDOMPoint(insertBRElementResult.inspect().GetNewNode());
+}
+
+nsresult HTMLEditor::GetElementOrigin(Element& aElement, int32_t& aX,
+ int32_t& aY) {
+ aX = 0;
+ aY = 0;
+
+ if (NS_WARN_IF(!IsInitialized())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ PresShell* presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsIFrame* frame = aElement.GetPrimaryFrame();
+ if (NS_WARN_IF(!frame)) {
+ return NS_OK;
+ }
+
+ nsIFrame* absoluteContainerBlockFrame =
+ presShell->GetAbsoluteContainingBlock(frame);
+ if (NS_WARN_IF(!absoluteContainerBlockFrame)) {
+ return NS_OK;
+ }
+ nsPoint off = frame->GetOffsetTo(absoluteContainerBlockFrame);
+ aX = nsPresContext::AppUnitsToIntCSSPixels(off.x);
+ aY = nsPresContext::AppUnitsToIntCSSPixels(off.y);
+
+ return NS_OK;
+}
+
+Element* HTMLEditor::GetSelectionContainerElement() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsINode* focusNode = nullptr;
+ if (SelectionRef().IsCollapsed()) {
+ focusNode = SelectionRef().GetFocusNode();
+ if (NS_WARN_IF(!focusNode)) {
+ return nullptr;
+ }
+ } else {
+ const uint32_t rangeCount = SelectionRef().RangeCount();
+ MOZ_ASSERT(rangeCount, "If 0, Selection::IsCollapsed() should return true");
+
+ if (rangeCount == 1) {
+ const nsRange* range = SelectionRef().GetRangeAt(0);
+
+ const RangeBoundary& startRef = range->StartRef();
+ const RangeBoundary& endRef = range->EndRef();
+
+ // This method called GetSelectedElement() to retrieve proper container
+ // when only one node is selected. However, it simply returns start
+ // node of Selection with additional cost. So, we do not need to call
+ // it anymore.
+ if (startRef.Container()->IsElement() &&
+ startRef.Container() == endRef.Container() &&
+ startRef.GetChildAtOffset() &&
+ startRef.GetChildAtOffset()->GetNextSibling() ==
+ endRef.GetChildAtOffset()) {
+ focusNode = startRef.GetChildAtOffset();
+ MOZ_ASSERT(focusNode, "Start container must not be nullptr");
+ } else {
+ focusNode = range->GetClosestCommonInclusiveAncestor();
+ if (!focusNode) {
+ NS_WARNING(
+ "AbstractRange::GetClosestCommonInclusiveAncestor() returned "
+ "nullptr");
+ return nullptr;
+ }
+ }
+ } else {
+ for (const uint32_t i : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(SelectionRef().RangeCount() == rangeCount);
+ const nsRange* range = SelectionRef().GetRangeAt(i);
+ MOZ_ASSERT(range);
+ nsINode* startContainer = range->GetStartContainer();
+ if (!focusNode) {
+ focusNode = startContainer;
+ } else if (focusNode != startContainer) {
+ // XXX Looks odd to use parent of startContainer because previous
+ // range may not be in the parent node of current
+ // startContainer.
+ focusNode = startContainer->GetParentNode();
+ // XXX Looks odd to break the for-loop here because we refer only
+ // first range and another range which starts from different
+ // container, and the latter range is preferred. Why?
+ break;
+ }
+ }
+ if (!focusNode) {
+ NS_WARNING("Focused node of selection was not found");
+ return nullptr;
+ }
+ }
+ }
+
+ if (focusNode->IsText()) {
+ focusNode = focusNode->GetParentNode();
+ if (NS_WARN_IF(!focusNode)) {
+ return nullptr;
+ }
+ }
+
+ if (NS_WARN_IF(!focusNode->IsElement())) {
+ return nullptr;
+ }
+ return focusNode->AsElement();
+}
+
+NS_IMETHODIMP HTMLEditor::IsAnonymousElement(Element* aElement, bool* aReturn) {
+ if (NS_WARN_IF(!aElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aReturn = aElement->IsRootOfNativeAnonymousSubtree();
+ return NS_OK;
+}
+
+nsresult HTMLEditor::SetReturnInParagraphCreatesNewParagraph(
+ bool aCreatesNewParagraph) {
+ mCRInParagraphCreatesParagraph = aCreatesNewParagraph;
+ return NS_OK;
+}
+
+bool HTMLEditor::GetReturnInParagraphCreatesNewParagraph() const {
+ return mCRInParagraphCreatesParagraph;
+}
+
+nsresult HTMLEditor::GetReturnInParagraphCreatesNewParagraph(
+ bool* aCreatesNewParagraph) {
+ *aCreatesNewParagraph = mCRInParagraphCreatesParagraph;
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetWrapWidth(int32_t* aWrapColumn) {
+ if (NS_WARN_IF(!aWrapColumn)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aWrapColumn = WrapWidth();
+ return NS_OK;
+}
+
+//
+// See if the style value includes this attribute, and if it does,
+// cut out everything from the attribute to the next semicolon.
+//
+static void CutStyle(const char* stylename, nsString& styleValue) {
+ // Find the current wrapping type:
+ int32_t styleStart = styleValue.LowerCaseFindASCII(stylename);
+ if (styleStart >= 0) {
+ int32_t styleEnd = styleValue.Find(u";", styleStart);
+ if (styleEnd > styleStart) {
+ styleValue.Cut(styleStart, styleEnd - styleStart + 1);
+ } else {
+ styleValue.Cut(styleStart, styleValue.Length() - styleStart);
+ }
+ }
+}
+
+NS_IMETHODIMP HTMLEditor::SetWrapWidth(int32_t aWrapColumn) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetWrapWidth);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mWrapColumn = aWrapColumn;
+
+ // Make sure we're a plaintext editor, otherwise we shouldn't
+ // do the rest of this.
+ if (!IsPlaintextMailComposer()) {
+ return NS_OK;
+ }
+
+ // Ought to set a style sheet here...
+ RefPtr<Element> rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Get the current style for this root element:
+ nsAutoString styleValue;
+ rootElement->GetAttr(nsGkAtoms::style, styleValue);
+
+ // We'll replace styles for these values:
+ CutStyle("white-space", styleValue);
+ CutStyle("width", styleValue);
+ CutStyle("font-family", styleValue);
+
+ // If we have other style left, trim off any existing semicolons
+ // or white-space, then add a known semicolon-space:
+ if (!styleValue.IsEmpty()) {
+ styleValue.Trim("; \t", false, true);
+ styleValue.AppendLiteral("; ");
+ }
+
+ // Make sure we have fixed-width font. This should be done for us,
+ // but it isn't, see bug 22502, so we have to add "font: -moz-fixed;".
+ // Only do this if we're wrapping.
+ if (IsWrapHackEnabled() && aWrapColumn >= 0) {
+ styleValue.AppendLiteral("font-family: -moz-fixed; ");
+ }
+
+ // and now we're ready to set the new white-space/wrapping style.
+ if (aWrapColumn > 0) {
+ // Wrap to a fixed column.
+ styleValue.AppendLiteral("white-space: pre-wrap; width: ");
+ styleValue.AppendInt(aWrapColumn);
+ styleValue.AppendLiteral("ch;");
+ } else if (!aWrapColumn) {
+ styleValue.AppendLiteral("white-space: pre-wrap;");
+ } else {
+ styleValue.AppendLiteral("white-space: pre;");
+ }
+
+ nsresult rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+ styleValue, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Element::SetAttr(nsGkAtoms::style) failed");
+ return rv;
+}
+
+Element* HTMLEditor::GetFocusedElement() const {
+ nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return nullptr;
+ }
+
+ Element* const focusedElement = focusManager->GetFocusedElement();
+
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return nullptr;
+ }
+ const bool inDesignMode = IsInDesignMode();
+ if (!focusedElement) {
+ // in designMode, nobody gets focus in most cases.
+ if (inDesignMode && OurWindowHasFocus()) {
+ return document->GetRootElement();
+ }
+ return nullptr;
+ }
+
+ if (inDesignMode) {
+ return OurWindowHasFocus() &&
+ focusedElement->IsInclusiveDescendantOf(document)
+ ? focusedElement
+ : nullptr;
+ }
+
+ // We're HTML editor for contenteditable
+
+ // If the focused content isn't editable, or it has independent selection,
+ // we don't have focus.
+ if (!focusedElement->HasFlag(NODE_IS_EDITABLE) ||
+ focusedElement->HasIndependentSelection()) {
+ return nullptr;
+ }
+ // If our window is focused, we're focused.
+ return OurWindowHasFocus() ? focusedElement : nullptr;
+}
+
+bool HTMLEditor::IsActiveInDOMWindow() const {
+ nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return false;
+ }
+
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return false;
+ }
+ const bool inDesignMode = IsInDesignMode();
+
+ // If we're in designMode, we're always active in the DOM window.
+ if (inDesignMode) {
+ return true;
+ }
+
+ nsPIDOMWindowOuter* ourWindow = document->GetWindow();
+ nsCOMPtr<nsPIDOMWindowOuter> win;
+ nsIContent* content = nsFocusManager::GetFocusedDescendant(
+ ourWindow, nsFocusManager::eOnlyCurrentWindow, getter_AddRefs(win));
+ if (!content) {
+ return false;
+ }
+
+ // We're HTML editor for contenteditable
+
+ // If the active content isn't editable, or it has independent selection,
+ // we're not active).
+ if (!content->HasFlag(NODE_IS_EDITABLE) ||
+ content->HasIndependentSelection()) {
+ return false;
+ }
+ return true;
+}
+
+Element* HTMLEditor::ComputeEditingHostInternal(
+ const nsIContent* aContent, LimitInBodyElement aLimitInBodyElement) const {
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return nullptr;
+ }
+
+ auto MaybeLimitInBodyElement =
+ [&](const Element* aCandidiateEditingHost) -> Element* {
+ if (!aCandidiateEditingHost) {
+ return nullptr;
+ }
+ if (aLimitInBodyElement != LimitInBodyElement::Yes) {
+ return const_cast<Element*>(aCandidiateEditingHost);
+ }
+ // By default, we should limit editing host to the <body> element for
+ // avoiding deleting or creating unexpected elements outside the <body>.
+ // However, this is incompatible with Chrome so that we should stop
+ // doing this with adding safety checks more.
+ if (document->GetBodyElement() &&
+ nsContentUtils::ContentIsFlattenedTreeDescendantOf(
+ aCandidiateEditingHost, document->GetBodyElement())) {
+ return const_cast<Element*>(aCandidiateEditingHost);
+ }
+ // XXX If aContent is an editing host and has no parent node, we reach here,
+ // but returing the <body> which is not connected to aContent is odd.
+ return document->GetBodyElement();
+ };
+
+ if (IsInDesignMode()) {
+ // TODO: In this case, we need to compute editing host from aContent or the
+ // focus node of selection, and it may be in an editing host in a
+ // shadow DOM tree etc. We need to do more complicated things.
+ // See also InDesignMode().
+ return document->GetBodyElement();
+ }
+
+ // We're HTML editor for contenteditable
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return nullptr;
+ }
+
+ const nsIContent* const content =
+ aContent ? aContent
+ : nsIContent::FromNodeOrNull(SelectionRef().GetFocusNode());
+ if (NS_WARN_IF(!content)) {
+ return nullptr;
+ }
+
+ // If the active content isn't editable, we're not active.
+ if (!content->HasFlag(NODE_IS_EDITABLE)) {
+ return nullptr;
+ }
+
+ // Although the content shouldn't be in a native anonymous subtree, but
+ // perhaps due to a bug of Selection or Range API, it may occur. HTMLEditor
+ // shouldn't touch native anonymous subtree so that return nullptr in such
+ // case.
+ if (MOZ_UNLIKELY(content->IsInNativeAnonymousSubtree())) {
+ return nullptr;
+ }
+
+ // Note that `Selection` can be in <input> or <textarea>. In the case, we
+ // need to look for an ancestor which does not have editable parent.
+ return MaybeLimitInBodyElement(
+ const_cast<nsIContent*>(content)->GetEditingHost());
+}
+
+void HTMLEditor::NotifyEditingHostMaybeChanged() {
+ // Note that even if the document is in design mode, a contenteditable element
+ // in a shadow tree is focusable. Therefore, we may need to update editing
+ // host even when the document is in design mode.
+ if (MOZ_UNLIKELY(NS_WARN_IF(!GetDocument()))) {
+ return;
+ }
+
+ // We're HTML editor for contenteditable
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ // Get selection ancestor limit which may be old editing host.
+ nsIContent* ancestorLimiter = SelectionRef().GetAncestorLimiter();
+ if (!ancestorLimiter) {
+ // If we've not initialized selection ancestor limit, we should wait focus
+ // event to set proper limiter.
+ return;
+ }
+
+ // Compute current editing host.
+ nsIContent* editingHost = ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost)) {
+ return;
+ }
+
+ // Update selection ancestor limit if current editing host includes the
+ // previous editing host.
+ // Additionally, the editing host may be an element in shadow DOM and the
+ // shadow host is in designMode. In this case, we need to set the editing
+ // host as the new selection limiter.
+ if (ancestorLimiter->IsInclusiveDescendantOf(editingHost) ||
+ (ancestorLimiter->IsInDesignMode() != editingHost->IsInDesignMode())) {
+ // Note that don't call HTMLEditor::InitializeSelectionAncestorLimit()
+ // here because it may collapse selection to the first editable node.
+ EditorBase::InitializeSelectionAncestorLimit(*editingHost);
+ }
+}
+
+EventTarget* HTMLEditor::GetDOMEventTarget() const {
+ // Don't use getDocument here, because we have no way of knowing
+ // whether Init() was ever called. So we need to get the document
+ // ourselves, if it exists.
+ Document* doc = GetDocument();
+ MOZ_ASSERT(doc, "The HTMLEditor has not been initialized yet");
+ if (!doc) {
+ return nullptr;
+ }
+
+ // Register the EditorEventListener to the parent of window.
+ //
+ // The advantage of this approach is HTMLEditor can still
+ // receive events when shadow dom is involved.
+ if (nsPIDOMWindowOuter* win = doc->GetWindow()) {
+ return win->GetParentTarget();
+ }
+ return nullptr;
+}
+
+bool HTMLEditor::ShouldReplaceRootElement() const {
+ if (!mRootElement) {
+ // If we don't know what is our root element, we should find our root.
+ return true;
+ }
+
+ // If we temporary set document root element to mRootElement, but there is
+ // body element now, we should replace the root element by the body element.
+ return mRootElement != GetBodyElement();
+}
+
+void HTMLEditor::NotifyRootChanged() {
+ MOZ_ASSERT(mPendingRootElementUpdatedRunner,
+ "HTMLEditor::NotifyRootChanged() should be called via a runner");
+ mPendingRootElementUpdatedRunner = nullptr;
+
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ RemoveEventListeners();
+ nsresult rv = InstallEventListeners();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::InstallEventListeners() failed, but ignored");
+ return;
+ }
+
+ UpdateRootElement();
+ if (!mRootElement) {
+ return;
+ }
+
+ rv = MaybeCollapseSelectionAtFirstEditableNode(false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) "
+ "failed, "
+ "but ignored");
+ return;
+ }
+
+ // When this editor has focus, we need to reset the selection limiter to
+ // new root. Otherwise, that is going to be done when this gets focus.
+ nsCOMPtr<nsINode> node = GetFocusedNode();
+ if (node) {
+ DebugOnly<nsresult> rvIgnored = InitializeSelection(*node);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::InitializeSelection() failed, but ignored");
+ }
+
+ SyncRealTimeSpell();
+}
+
+Element* HTMLEditor::GetBodyElement() const {
+ Document* document = GetDocument();
+ MOZ_ASSERT(document, "The HTMLEditor hasn't been initialized yet");
+ if (NS_WARN_IF(!document)) {
+ return nullptr;
+ }
+ return document->GetBody();
+}
+
+nsINode* HTMLEditor::GetFocusedNode() const {
+ Element* focusedElement = GetFocusedElement();
+ if (!focusedElement) {
+ return nullptr;
+ }
+
+ // focusedElement might be non-null even focusManager->GetFocusedElement()
+ // is null. That's the designMode case, and in that case our
+ // FocusedContent() returns the root element, but we want to return
+ // the document.
+
+ nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
+ NS_ASSERTION(focusManager, "Focus manager is null");
+ if ((focusedElement = focusManager->GetFocusedElement())) {
+ return focusedElement;
+ }
+
+ return GetDocument();
+}
+
+bool HTMLEditor::OurWindowHasFocus() const {
+ nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return false;
+ }
+ nsPIDOMWindowOuter* focusedWindow = focusManager->GetFocusedWindow();
+ if (!focusedWindow) {
+ return false;
+ }
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return false;
+ }
+ nsPIDOMWindowOuter* ourWindow = document->GetWindow();
+ return ourWindow == focusedWindow;
+}
+
+bool HTMLEditor::IsAcceptableInputEvent(WidgetGUIEvent* aGUIEvent) const {
+ if (!EditorBase::IsAcceptableInputEvent(aGUIEvent)) {
+ return false;
+ }
+
+ // While there is composition, all composition events in its top level
+ // window are always fired on the composing editor. Therefore, if this
+ // editor has composition, the composition events should be handled in this
+ // editor.
+ if (mComposition && aGUIEvent->AsCompositionEvent()) {
+ return true;
+ }
+
+ nsCOMPtr<nsINode> eventTargetNode =
+ nsINode::FromEventTargetOrNull(aGUIEvent->GetOriginalDOMEventTarget());
+ if (NS_WARN_IF(!eventTargetNode)) {
+ return false;
+ }
+
+ if (eventTargetNode->IsContent()) {
+ eventTargetNode =
+ eventTargetNode->AsContent()->FindFirstNonChromeOnlyAccessContent();
+ if (NS_WARN_IF(!eventTargetNode)) {
+ return false;
+ }
+ }
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return false;
+ }
+
+ if (IsInDesignMode()) {
+ // If this editor is in designMode and the event target is the document,
+ // the event is for this editor.
+ if (eventTargetNode->IsDocument()) {
+ return eventTargetNode == document;
+ }
+ // Otherwise, check whether the event target is in this document or not.
+ if (NS_WARN_IF(!eventTargetNode->IsContent())) {
+ return false;
+ }
+ if (document == eventTargetNode->GetUncomposedDoc()) {
+ return true;
+ }
+ // If the event target is in a shadow tree, the content is not editable
+ // by default, but if the focused content is an editing host, we need to
+ // handle it as contenteditable mode.
+ if (!eventTargetNode->IsInShadowTree()) {
+ return false;
+ }
+ }
+
+ // Space event for <button> and <summary> with contenteditable
+ // should be handle by the themselves.
+ if (aGUIEvent->mMessage == eKeyPress &&
+ aGUIEvent->AsKeyboardEvent()->ShouldWorkAsSpaceKey()) {
+ nsGenericHTMLElement* element =
+ HTMLButtonElement::FromNode(eventTargetNode);
+ if (!element) {
+ element = HTMLSummaryElement::FromNode(eventTargetNode);
+ }
+
+ if (element && element->IsContentEditable()) {
+ return false;
+ }
+ }
+ // This HTML editor is for contenteditable. We need to check the validity
+ // of the target.
+ if (NS_WARN_IF(!eventTargetNode->IsContent())) {
+ return false;
+ }
+
+ // If the event is a mouse event, we need to check if the target content is
+ // the focused editing host or its descendant.
+ if (aGUIEvent->AsMouseEventBase()) {
+ nsIContent* editingHost = ComputeEditingHost();
+ // If there is no active editing host, we cannot handle the mouse event
+ // correctly.
+ if (!editingHost) {
+ return false;
+ }
+ // If clicked on non-editable root element but the body element is the
+ // active editing host, we should assume that the click event is
+ // targetted.
+ if (eventTargetNode == document->GetRootElement() &&
+ !eventTargetNode->HasFlag(NODE_IS_EDITABLE) &&
+ editingHost == document->GetBodyElement()) {
+ eventTargetNode = editingHost;
+ }
+ // If the target element is neither the active editing host nor a
+ // descendant of it, we may not be able to handle the event.
+ if (!eventTargetNode->IsInclusiveDescendantOf(editingHost)) {
+ return false;
+ }
+ // If the clicked element has an independent selection, we shouldn't
+ // handle this click event.
+ if (eventTargetNode->AsContent()->HasIndependentSelection()) {
+ return false;
+ }
+ // If the target content is editable, we should handle this event.
+ return eventTargetNode->HasFlag(NODE_IS_EDITABLE);
+ }
+
+ // If the target of the other events which target focused element isn't
+ // editable or has an independent selection, this editor shouldn't handle
+ // the event.
+ if (!eventTargetNode->HasFlag(NODE_IS_EDITABLE) ||
+ eventTargetNode->AsContent()->HasIndependentSelection()) {
+ return false;
+ }
+
+ // Finally, check whether we're actually focused or not. When we're not
+ // focused, we should ignore the dispatched event by script (or something)
+ // because content editable element needs selection in itself for editing.
+ // However, when we're not focused, it's not guaranteed.
+ return IsActiveInDOMWindow();
+}
+
+nsresult HTMLEditor::GetPreferredIMEState(IMEState* aState) {
+ // HTML editor don't prefer the CSS ime-mode because IE didn't do so too.
+ aState->mOpen = IMEState::DONT_CHANGE_OPEN_STATE;
+ if (IsReadonly()) {
+ aState->mEnabled = IMEEnabled::Disabled;
+ } else {
+ aState->mEnabled = IMEEnabled::Enabled;
+ }
+ return NS_OK;
+}
+
+already_AddRefed<Element> HTMLEditor::GetInputEventTargetElement() const {
+ RefPtr<Element> target = ComputeEditingHost(LimitInBodyElement::No);
+ if (target) {
+ return target.forget();
+ }
+
+ // When there is no active editing host due to focus node is a
+ // non-editable node, we should look for its editable parent to
+ // dispatch `beforeinput` event.
+ nsIContent* focusContent =
+ nsIContent::FromNodeOrNull(SelectionRef().GetFocusNode());
+ if (!focusContent || focusContent->IsEditable()) {
+ return nullptr;
+ }
+ for (Element* element : focusContent->AncestorsOfType<Element>()) {
+ if (element->IsEditable()) {
+ target = element->GetEditingHost();
+ return target.forget();
+ }
+ }
+ return nullptr;
+}
+
+nsresult HTMLEditor::OnModifyDocument() {
+ MOZ_ASSERT(mPendingDocumentModifiedRunner,
+ "HTMLEditor::OnModifyDocument() should be called via a runner");
+ mPendingDocumentModifiedRunner = nullptr;
+
+ if (IsEditActionDataAvailable()) {
+ return OnModifyDocumentInternal();
+ }
+
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eCreatePaddingBRElementForEmptyEditor);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv = OnModifyDocumentInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::OnModifyDocumentInternal() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::OnModifyDocumentInternal() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!mPendingDocumentModifiedRunner);
+
+ // EnsureNoPaddingBRElementForEmptyEditor() below may cause a flush, which
+ // could destroy the editor
+ nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;
+
+ // Delete our padding <br> element for empty editor, if we have one, since
+ // the document might not be empty any more.
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return rv;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ // Try to recreate the padding <br> element for empty editor if needed.
+ rv = MaybeCreatePaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed");
+
+ return rv;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditor.h b/editor/libeditor/HTMLEditor.h
new file mode 100644
index 0000000000..2a31ead29d
--- /dev/null
+++ b/editor/libeditor/HTMLEditor.h
@@ -0,0 +1,4726 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_HTMLEditor_h
+#define mozilla_HTMLEditor_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ComposerCommandsUpdater.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/ManualNAC.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/File.h"
+
+#include "nsAttrName.h"
+#include "nsCOMPtr.h"
+#include "nsIDocumentObserver.h"
+#include "nsIDOMEventListener.h"
+#include "nsIEditorMailSupport.h"
+#include "nsIHTMLAbsPosEditor.h"
+#include "nsIHTMLEditor.h"
+#include "nsIHTMLInlineTableEditor.h"
+#include "nsIHTMLObjectResizer.h"
+#include "nsIPrincipal.h"
+#include "nsITableEditor.h"
+#include "nsPoint.h"
+#include "nsStubMutationObserver.h"
+
+#include <functional>
+
+class nsDocumentFragment;
+class nsFrameSelection;
+class nsHTMLDocument;
+class nsITransferable;
+class nsIClipboard;
+class nsRange;
+class nsStaticAtom;
+class nsStyledElement;
+class nsTableCellFrame;
+class nsTableWrapperFrame;
+template <class E>
+class nsTArray;
+
+namespace mozilla {
+class AlignStateAtSelection;
+class AutoSelectionSetterAfterTableEdit;
+class AutoSetTemporaryAncestorLimiter;
+class EmptyEditableFunctor;
+class ListElementSelectionState;
+class ListItemElementSelectionState;
+class ParagraphStateAtSelection;
+class ResizerSelectionListener;
+class Runnable;
+template <class T>
+class OwningNonNull;
+namespace dom {
+class AbstractRange;
+class Blob;
+class DocumentFragment;
+class Event;
+class HTMLBRElement;
+class MouseEvent;
+class StaticRange;
+} // namespace dom
+namespace widget {
+struct IMEState;
+} // namespace widget
+
+enum class ParagraphSeparator { div, p, br };
+
+/**
+ * The HTML editor implementation.<br>
+ * Use to edit HTML document represented as a DOM tree.
+ */
+class HTMLEditor final : public EditorBase,
+ public nsIHTMLEditor,
+ public nsIHTMLObjectResizer,
+ public nsIHTMLAbsPosEditor,
+ public nsITableEditor,
+ public nsIHTMLInlineTableEditor,
+ public nsStubMutationObserver,
+ public nsIEditorMailSupport {
+ public:
+ /****************************************************************************
+ * NOTE: DO NOT MAKE YOUR NEW METHODS PUBLIC IF they are called by other
+ * classes under libeditor except EditorEventListener and
+ * HTMLEditorEventListener because each public method which may fire
+ * eEditorInput event will need to instantiate new stack class for
+ * managing input type value of eEditorInput and cache some objects
+ * for smarter handling. In other words, when you add new root
+ * method to edit the DOM tree, you can make your new method public.
+ ****************************************************************************/
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLEditor, EditorBase)
+
+ // nsStubMutationObserver overrides
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+ NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
+
+ // nsIHTMLEditor methods
+ NS_DECL_NSIHTMLEDITOR
+
+ // nsIHTMLObjectResizer methods (implemented in HTMLObjectResizer.cpp)
+ NS_DECL_NSIHTMLOBJECTRESIZER
+
+ // nsIHTMLAbsPosEditor methods (implemented in HTMLAbsPositionEditor.cpp)
+ NS_DECL_NSIHTMLABSPOSEDITOR
+
+ // nsIHTMLInlineTableEditor methods (implemented in HTMLInlineTableEditor.cpp)
+ NS_DECL_NSIHTMLINLINETABLEEDITOR
+
+ // nsIEditorMailSupport methods
+ NS_DECL_NSIEDITORMAILSUPPORT
+
+ // nsITableEditor methods
+ NS_DECL_NSITABLEEDITOR
+
+ // nsISelectionListener overrides
+ NS_DECL_NSISELECTIONLISTENER
+
+ /**
+ * @param aDocument The document whose content will be editable.
+ */
+ explicit HTMLEditor(const Document& aDocument);
+
+ /**
+ * @param aDocument The document whose content will be editable.
+ * @param aComposerCommandsUpdater The composer command updater.
+ * @param aFlags Some of nsIEditor::eEditor*Mask flags.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ Init(Document& aDocument, ComposerCommandsUpdater& aComposerCommandsUpdater,
+ uint32_t aFlags);
+
+ /**
+ * PostCreate() should be called after Init, and is the time that the editor
+ * tells its documentStateObservers that the document has been created.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult PostCreate();
+
+ /**
+ * PreDestroy() is called before the editor goes away, and gives the editor a
+ * chance to tell its documentStateObservers that the document is going away.
+ */
+ MOZ_CAN_RUN_SCRIPT void PreDestroy();
+
+ static HTMLEditor* GetFrom(nsIEditor* aEditor) {
+ return aEditor ? aEditor->GetAsHTMLEditor() : nullptr;
+ }
+ static const HTMLEditor* GetFrom(const nsIEditor* aEditor) {
+ return aEditor ? aEditor->GetAsHTMLEditor() : nullptr;
+ }
+
+ [[nodiscard]] bool GetReturnInParagraphCreatesNewParagraph() const;
+
+ // EditorBase overrides
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD BeginningOfDocument() final;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD EndOfDocument() final;
+
+ NS_IMETHOD GetDocumentCharacterSet(nsACString& aCharacterSet) final;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD
+ SetDocumentCharacterSet(const nsACString& aCharacterSet) final;
+
+ bool IsEmpty() const final;
+
+ bool CanPaste(int32_t aClipboardType) const final;
+ using EditorBase::CanPaste;
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD DeleteNode(nsINode* aNode,
+ bool aPreseveSelection,
+ uint8_t aOptionalArgCount) final;
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD InsertLineBreak() final;
+
+ /**
+ * PreHandleMouseDown() and PreHandleMouseUp() are called before
+ * HTMLEditorEventListener handles them. The coming event may be
+ * non-acceptable event.
+ */
+ void PreHandleMouseDown(const dom::MouseEvent& aMouseDownEvent);
+ void PreHandleMouseUp(const dom::MouseEvent& aMouseUpEvent);
+
+ /**
+ * PreHandleSelectionChangeCommand() and PostHandleSelectionChangeCommand()
+ * are called before or after handling a command which may change selection
+ * and/or scroll position.
+ */
+ void PreHandleSelectionChangeCommand(Command aCommand);
+ void PostHandleSelectionChangeCommand(Command aCommand);
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) final;
+ Element* GetFocusedElement() const final;
+ bool IsActiveInDOMWindow() const final;
+ dom::EventTarget* GetDOMEventTarget() const final;
+ [[nodiscard]] Element* FindSelectionRoot(const nsINode& aNode) const final;
+ bool IsAcceptableInputEvent(WidgetGUIEvent* aGUIEvent) const final;
+ nsresult GetPreferredIMEState(widget::IMEState* aState) final;
+ MOZ_CAN_RUN_SCRIPT nsresult
+ OnFocus(const nsINode& aOriginalEventTargetNode) final;
+ nsresult OnBlur(const dom::EventTarget* aEventTarget) final;
+
+ /**
+ * Called when aDocument or aElement becomes editable without focus change.
+ * E.g., when the design mode is enabled or the contenteditable attribute
+ * is set to the focused element.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult FocusedElementOrDocumentBecomesEditable(
+ Document& aDocument, Element* aElement);
+
+ /**
+ * Called when aDocument or aElement becomes not editable without focus
+ * change. E.g., when the design mode ends or the contenteditable attribute is
+ * removed or set to "false".
+ */
+ MOZ_CAN_RUN_SCRIPT static nsresult FocusedElementOrDocumentBecomesNotEditable(
+ HTMLEditor* aHTMLEditor, Document& aDocument, Element* aElement);
+
+ /**
+ * GetBackgroundColorState() returns what the background color of the
+ * selection.
+ *
+ * @param aMixed true if there is more than one font color
+ * @param aOutColor Color string. "" is returned for none.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult GetBackgroundColorState(bool* aMixed,
+ nsAString& aOutColor);
+
+ /**
+ * PasteNoFormattingAsAction() pastes content in clipboard without any style
+ * information.
+ *
+ * @param aClipboardType nsIClipboard::kGlobalClipboard or
+ * nsIClipboard::kSelectionClipboard.
+ * @param aDispatchPasteEvent Yes if this should dispatch ePaste event
+ * before pasting. Otherwise, No.
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult PasteNoFormattingAsAction(
+ int32_t aClipboardType, DispatchPasteEvent aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ bool CanPasteTransferable(nsITransferable* aTransferable) final;
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ InsertLineBreakAsAction(nsIPrincipal* aPrincipal = nullptr) final;
+
+ /**
+ * InsertParagraphSeparatorAsAction() is called when user tries to separate
+ * current paragraph with Enter key press in HTMLEditor or something.
+ *
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ InsertParagraphSeparatorAsAction(nsIPrincipal* aPrincipal = nullptr);
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ InsertElementAtSelectionAsAction(Element* aElement, bool aDeleteSelection,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ MOZ_CAN_RUN_SCRIPT nsresult InsertLinkAroundSelectionAsAction(
+ Element* aAnchorElement, nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * CreateElementWithDefaults() creates new element whose name is
+ * aTagName with some default attributes are set. Note that this is a
+ * public utility method. I.e., just creates element, not insert it
+ * into the DOM tree.
+ * NOTE: This is available for internal use too since this does not change
+ * the DOM tree nor undo transactions, and does not refer Selection,
+ * etc.
+ *
+ * @param aTagName The new element's tag name. If the name is
+ * one of "href", "anchor" or "namedanchor",
+ * this creates an <a> element.
+ * @return Newly created element.
+ */
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Element> CreateElementWithDefaults(
+ const nsAtom& aTagName);
+
+ /**
+ * Indent or outdent content around Selection.
+ *
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ IndentAsAction(nsIPrincipal* aPrincipal = nullptr);
+ MOZ_CAN_RUN_SCRIPT nsresult
+ OutdentAsAction(nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * The Document.execCommand("formatBlock") handler.
+ *
+ * @param aParagraphFormat Must not be an empty string, and the value must
+ * be one of address, article, aside, blockquote,
+ * div, footer, h1, h2, h3, h4, h5, h6, header,
+ * hgroup, main, nav, p, pre, selection, dt or dd.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult FormatBlockAsAction(
+ const nsAString& aParagraphFormat, nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * The cmd_paragraphState command handler.
+ *
+ * @param aParagraphFormat Can be empty string. If this is empty string,
+ * this removes ancestor format elements.
+ * Otherwise, the value must be one of p, pre,
+ * h1, h2, h3, h4, h5, h6, address, dt or dl.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetParagraphStateAsAction(
+ const nsAString& aParagraphFormat, nsIPrincipal* aPrincipal = nullptr);
+
+ MOZ_CAN_RUN_SCRIPT nsresult AlignAsAction(const nsAString& aAlignType,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ MOZ_CAN_RUN_SCRIPT nsresult RemoveListAsAction(
+ const nsAString& aListType, nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * MakeOrChangeListAsAction() makes selected hard lines list element(s).
+ *
+ * @param aListElementTagName The new list element tag name. Must be
+ * nsGkAtoms::ul, nsGkAtoms::ol or
+ * nsGkAtoms::dl.
+ * @param aBulletType If this is not empty string, it's set
+ * to `type` attribute of new list item
+ * elements. Otherwise, existing `type`
+ * attributes will be removed.
+ * @param aSelectAllOfCurrentList Yes if this should treat all of
+ * ancestor list element at selection.
+ */
+ enum class SelectAllOfCurrentList { Yes, No };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MakeOrChangeListAsAction(
+ const nsStaticAtom& aListElementTagName, const nsAString& aBulletType,
+ SelectAllOfCurrentList aSelectAllOfCurrentList,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * If aTargetElement is a resizer, start to drag the resizer. Otherwise, if
+ * aTargetElement is the grabber, start to handle drag gester on it.
+ *
+ * @param aMouseDownEvent A `mousedown` event fired on aTargetElement.
+ * @param aEventTargetElement The target element being pressed. This must
+ * be same as explicit original event target of
+ * aMouseDownEvent.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult StartToDragResizerOrHandleDragGestureOnGrabber(
+ dom::MouseEvent& aMouseDownEvent, Element& aEventTargetElement);
+
+ /**
+ * If the editor is handling dragging a resizer, handling drag gesture on
+ * the grabber or dragging the grabber, this finalize it. Otherwise,
+ * does nothing.
+ *
+ * @param aClientPoint The final point of the drag.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ StopDraggingResizerOrGrabberAt(const CSSIntPoint& aClientPoint);
+
+ /**
+ * If the editor is handling dragging a resizer, handling drag gesture to
+ * start dragging the grabber or dragging the grabber, this method updates
+ * it's position.
+ *
+ * @param aClientPoint The new point of the drag.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ UpdateResizerOrGrabberPositionTo(const CSSIntPoint& aClientPoint);
+
+ /**
+ * IsCSSEnabled() returns true if this editor treats styles with style
+ * attribute of HTML elements. Otherwise, if this editor treats all styles
+ * with "font style elements" like <b>, <i>, etc, and <blockquote> to indent,
+ * align attribute to align contents, returns false.
+ */
+ bool IsCSSEnabled() const { return mIsCSSPrefChecked; }
+
+ /**
+ * Enable/disable object resizers for <img> elements, <table> elements,
+ * absolute positioned elements (required absolute position editor enabled).
+ */
+ MOZ_CAN_RUN_SCRIPT void EnableObjectResizer(bool aEnable) {
+ if (mIsObjectResizingEnabled == aEnable) {
+ return;
+ }
+
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eEnableOrDisableResizer);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ mIsObjectResizingEnabled = aEnable;
+ RefreshEditingUI();
+ }
+ bool IsObjectResizerEnabled() const { return mIsObjectResizingEnabled; }
+
+ Element* GetResizerTarget() const { return mResizedObject; }
+
+ /**
+ * Enable/disable inline table editor, e.g., adding new row or column,
+ * removing existing row or column.
+ */
+ MOZ_CAN_RUN_SCRIPT void EnableInlineTableEditor(bool aEnable) {
+ if (mIsInlineTableEditingEnabled == aEnable) {
+ return;
+ }
+
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eEnableOrDisableInlineTableEditingUI);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ mIsInlineTableEditingEnabled = aEnable;
+ RefreshEditingUI();
+ }
+ bool IsInlineTableEditorEnabled() const {
+ return mIsInlineTableEditingEnabled;
+ }
+
+ /**
+ * Enable/disable absolute position editor, resizing absolute positioned
+ * elements (required object resizers enabled) or positioning them with
+ * dragging grabber.
+ */
+ MOZ_CAN_RUN_SCRIPT void EnableAbsolutePositionEditor(bool aEnable) {
+ if (mIsAbsolutelyPositioningEnabled == aEnable) {
+ return;
+ }
+
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eEnableOrDisableAbsolutePositionEditor);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ mIsAbsolutelyPositioningEnabled = aEnable;
+ RefreshEditingUI();
+ }
+ bool IsAbsolutePositionEditorEnabled() const {
+ return mIsAbsolutelyPositioningEnabled;
+ }
+
+ /**
+ * returns the deepest absolutely positioned container of the selection
+ * if it exists or null.
+ */
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Element>
+ GetAbsolutelyPositionedSelectionContainer() const;
+
+ Element* GetPositionedElement() const { return mAbsolutelyPositionedObject; }
+
+ /**
+ * extracts the selection from the normal flow of the document and
+ * positions it.
+ *
+ * @param aEnabled [IN] true to absolutely position the selection,
+ * false to put it back in the normal flow
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetSelectionToAbsoluteOrStaticAsAction(
+ bool aEnabled, nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * returns the absolute z-index of a positioned element. Never returns 'auto'
+ * @return the z-index of the element
+ * @param aElement [IN] the element.
+ */
+ MOZ_CAN_RUN_SCRIPT int32_t GetZIndex(Element& aElement);
+
+ /**
+ * adds aChange to the z-index of the currently positioned element.
+ *
+ * @param aChange [IN] relative change to apply to current z-index
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ AddZIndexAsAction(int32_t aChange, nsIPrincipal* aPrincipal = nullptr);
+
+ MOZ_CAN_RUN_SCRIPT nsresult SetBackgroundColorAsAction(
+ const nsAString& aColor, nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * SetInlinePropertyAsAction() sets a property which changes inline style of
+ * text. E.g., bold, italic, super and sub.
+ * This automatically removes exclusive style, however, treats all changes
+ * as a transaction.
+ *
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetInlinePropertyAsAction(
+ nsStaticAtom& aProperty, nsStaticAtom* aAttribute,
+ const nsAString& aValue, nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * GetInlineProperty() gets aggregate properties of the current selection.
+ * All object in the current selection are scanned and their attributes are
+ * represented in a list of Property object.
+ * TODO: Make this return Result<Something> instead of bool out arguments.
+ *
+ * @param aHTMLProperty the property to get on the selection
+ * @param aAttribute the attribute of the property, if applicable.
+ * May be null.
+ * Example: aHTMLProperty=nsGkAtoms::font,
+ * aAttribute=nsGkAtoms::color
+ * @param aValue if aAttribute is not null, the value of the
+ * attribute. May be null.
+ * Example: aHTMLProperty=nsGkAtoms::font,
+ * aAttribute=nsGkAtoms::color,
+ * aValue="0x00FFFF"
+ * @param aFirst [OUT] true if the first text node in the
+ * selection has the property
+ * @param aAny [OUT] true if any of the text nodes in the
+ * selection have the property
+ * @param aAll [OUT] true if all of the text nodes in the
+ * selection have the property
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult GetInlineProperty(
+ nsStaticAtom& aHTMLProperty, nsAtom* aAttribute, const nsAString& aValue,
+ bool* aFirst, bool* aAny, bool* aAll) const;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult GetInlinePropertyWithAttrValue(
+ nsStaticAtom& aHTMLProperty, nsAtom* aAttribute, const nsAString& aValue,
+ bool* aFirst, bool* aAny, bool* aAll, nsAString& outValue);
+
+ /**
+ * RemoveInlinePropertyAsAction() removes a property which changes inline
+ * style of text. E.g., bold, italic, super and sub.
+ *
+ * @param aHTMLProperty Tag name whcih represents the inline style you want
+ * to remove. E.g., nsGkAtoms::strong, nsGkAtoms::b,
+ * etc. If nsGkAtoms::href, <a> element which has
+ * href attribute will be removed.
+ * If nsGkAtoms::name, <a> element which has non-empty
+ * name attribute will be removed.
+ * @param aAttribute If aHTMLProperty is nsGkAtoms::font, aAttribute should
+ * be nsGkAtoms::fase, nsGkAtoms::size, nsGkAtoms::color
+ * or nsGkAtoms::bgcolor. Otherwise, set nullptr.
+ * Must not use nsGkAtoms::_empty here.
+ * @param aPrincipal Set subject principal if it may be called by JS. If
+ * set to nullptr, will be treated as called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult RemoveInlinePropertyAsAction(
+ nsStaticAtom& aHTMLProperty, nsStaticAtom* aAttribute,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ RemoveAllInlinePropertiesAsAction(nsIPrincipal* aPrincipal = nullptr);
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ IncreaseFontSizeAsAction(nsIPrincipal* aPrincipal = nullptr);
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DecreaseFontSizeAsAction(nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * GetFontColorState() returns foreground color information in first
+ * range of Selection.
+ * If first range of Selection is collapsed and there is a cache of style for
+ * new text, aIsMixed is set to false and aColor is set to the cached color.
+ * If first range of Selection is collapsed and there is no cached color,
+ * this returns the color of the node, aIsMixed is set to false and aColor is
+ * set to the color.
+ * If first range of Selection is not collapsed, this collects colors of
+ * each node in the range. If there are two or more colors, aIsMixed is set
+ * to true and aColor is truncated. If only one color is set to all of the
+ * range, aIsMixed is set to false and aColor is set to the color.
+ * If there is no Selection ranges, aIsMixed is set to false and aColor is
+ * truncated.
+ *
+ * @param aIsMixed Must not be nullptr. This is set to true
+ * if there is two or more colors in first
+ * range of Selection.
+ * @param aColor Returns the color if only one color is set to
+ * all of first range in Selection. Otherwise,
+ * returns empty string.
+ * @return Returns error only when illegal cases, e.g.,
+ * Selection instance has gone, first range
+ * Selection is broken.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ GetFontColorState(bool* aIsMixed, nsAString& aColor);
+
+ /**
+ * Detach aComposerCommandsUpdater from this.
+ */
+ void Detach(const ComposerCommandsUpdater& aComposerCommandsUpdater);
+
+ nsStaticAtom& DefaultParagraphSeparatorTagName() const {
+ return HTMLEditor::ToParagraphSeparatorTagName(mDefaultParagraphSeparator);
+ }
+ ParagraphSeparator GetDefaultParagraphSeparator() const {
+ return mDefaultParagraphSeparator;
+ }
+ void SetDefaultParagraphSeparator(ParagraphSeparator aSep) {
+ mDefaultParagraphSeparator = aSep;
+ }
+ static nsStaticAtom& ToParagraphSeparatorTagName(
+ ParagraphSeparator aSeparator) {
+ switch (aSeparator) {
+ case ParagraphSeparator::div:
+ return *nsGkAtoms::div;
+ case ParagraphSeparator::p:
+ return *nsGkAtoms::p;
+ case ParagraphSeparator::br:
+ return *nsGkAtoms::br;
+ default:
+ MOZ_ASSERT_UNREACHABLE("New paragraph separator isn't handled here");
+ return *nsGkAtoms::div;
+ }
+ }
+
+ /**
+ * Modifies the table containing the selection according to the
+ * activation of an inline table editing UI element
+ * @param aUIAnonymousElement [IN] the inline table editing UI element
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DoInlineTableEditingAction(const Element& aUIAnonymousElement);
+
+ /**
+ * GetInclusiveAncestorByTagName() looks for an element node whose name
+ * matches aTagName from aNode or anchor node of Selection to <body> element.
+ *
+ * @param aTagName The tag name which you want to look for.
+ * Must not be nsGkAtoms::_empty.
+ * If nsGkAtoms::list, the result may be <ul>, <ol> or
+ * <dl> element.
+ * If nsGkAtoms::td, the result may be <td> or <th>.
+ * If nsGkAtoms::href, the result may be <a> element
+ * which has "href" attribute with non-empty value.
+ * If nsGkAtoms::anchor, the result may be <a> which
+ * has "name" attribute with non-empty value.
+ * @param aContent Start node to look for the result.
+ * @return If an element which matches aTagName, returns
+ * an Element. Otherwise, nullptr.
+ */
+ Element* GetInclusiveAncestorByTagName(const nsStaticAtom& aTagName,
+ nsIContent& aContent) const;
+
+ /**
+ * Compute editing host for aContent. If this editor isn't active in the DOM
+ * window, this returns nullptr.
+ */
+ enum class LimitInBodyElement { No, Yes };
+ [[nodiscard]] Element* ComputeEditingHost(
+ const nsIContent& aContent,
+ LimitInBodyElement aLimitInBodyElement = LimitInBodyElement::Yes) const {
+ return ComputeEditingHostInternal(&aContent, aLimitInBodyElement);
+ }
+
+ /**
+ * Compute editing host for the focus node of the Selection. If this editor
+ * isn't active in the DOM window, this returns nullptr.
+ */
+ [[nodiscard]] Element* ComputeEditingHost(
+ LimitInBodyElement aLimitInBodyElement = LimitInBodyElement::Yes) const {
+ return ComputeEditingHostInternal(nullptr, aLimitInBodyElement);
+ }
+
+ /**
+ * Return true if we're in designMode.
+ */
+ bool IsInDesignMode() const;
+
+ /**
+ * Return true if entire the document is editable (although the document
+ * may have non-editable nodes, e.g.,
+ * <body contenteditable><div contenteditable="false"></div></body>
+ */
+ bool EntireDocumentIsEditable() const;
+
+ /**
+ * Basically, this always returns true if we're for `contenteditable` or
+ * `designMode` editor in web apps. However, e.g., Composer of SeaMonkey
+ * can make the editor not tabbable.
+ */
+ bool IsTabbable() const { return IsInteractionAllowed(); }
+
+ /**
+ * NotifyEditingHostMaybeChanged() is called when new element becomes
+ * contenteditable when the document already had contenteditable elements.
+ */
+ void NotifyEditingHostMaybeChanged();
+
+ /** Insert a string as quoted text
+ * (whose representation is dependant on the editor type),
+ * replacing the selected text (if any).
+ *
+ * @param aQuotedText The actual text to be quoted
+ * @parem aNodeInserted Return the node which was inserted.
+ */
+ MOZ_CAN_RUN_SCRIPT // USED_BY_COMM_CENTRAL
+ nsresult
+ InsertAsQuotation(const nsAString& aQuotedText, nsINode** aNodeInserted);
+
+ MOZ_CAN_RUN_SCRIPT nsresult InsertHTMLAsAction(
+ const nsAString& aInString, nsIPrincipal* aPrincipal = nullptr);
+
+ /**
+ * Refresh positions of resizers. If you change size of target of resizers,
+ * you need to refresh position of resizers with calling this.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult RefreshResizers();
+
+ bool IsWrapHackEnabled() const {
+ return (mFlags & nsIEditor::eEditorEnableWrapHackMask) != 0;
+ }
+
+ /**
+ * Return true if this is in the plaintext mail composer mode of
+ * Thunderbird or something.
+ * NOTE: This is different from contenteditable="plaintext-only"
+ */
+ bool IsPlaintextMailComposer() const {
+ const bool isPlaintextMode =
+ (mFlags & nsIEditor::eEditorPlaintextMask) != 0;
+ MOZ_ASSERT_IF(IsTextEditor(), isPlaintextMode);
+ return isPlaintextMode;
+ }
+
+ protected: // May be called by friends.
+ /****************************************************************************
+ * Some friend classes are allowed to call the following protected methods.
+ * However, those methods won't prepare caches of some objects which are
+ * necessary for them. So, if you call them from friend classes, you need
+ * to make sure that AutoEditActionDataSetter is created.
+ ****************************************************************************/
+
+ /**
+ * InsertBRElement() creates a <br> element and inserts it before
+ * aPointToInsert.
+ *
+ * @param aWithTransaction Whether the inserting is new element is undoable
+ * or not. WithTransaction::No is useful only when
+ * the new element is inserted into a new element
+ * which has not been connected yet.
+ * @param aPointToInsert The DOM point where should be <br> node inserted
+ * before.
+ * @param aSelect If eNone, returns a point to put caret which is
+ * suggested by InsertNodeTransaction.
+ * If eNext, returns a point after the new <br>
+ * element.
+ * If ePrevious, returns a point at the new <br>
+ * element.
+ * @return The new <br> node and suggesting point to put
+ * caret which respects aSelect.
+ */
+ MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult> InsertBRElement(
+ WithTransaction aWithTransaction, const EditorDOMPoint& aPointToInsert,
+ EDirection aSelect = eNone);
+
+ /**
+ * Delete text in the range in aTextNode. If aTextNode is not editable, this
+ * does nothing.
+ *
+ * @param aTextNode The text node which should be modified.
+ * @param aOffset Start offset of removing text in aTextNode.
+ * @param aLength Length of removing text.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ DeleteTextWithTransaction(dom::Text& aTextNode, uint32_t aOffset,
+ uint32_t aLength);
+
+ /**
+ * Replace text in the range with aStringToInsert. If there is a DOM range
+ * exactly same as the replacing range, it'll be collapsed to
+ * {aTextNode, aOffset} because of the order of deletion and insertion.
+ * Therefore, the callers may need to handle `Selection` even when callers
+ * do not want to update `Selection`.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertTextResult, nsresult>
+ ReplaceTextWithTransaction(dom::Text& aTextNode, uint32_t aOffset,
+ uint32_t aLength,
+ const nsAString& aStringToInsert);
+
+ /**
+ * Insert aStringToInsert to aPointToInsert. If the point is not editable,
+ * this returns error.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertTextResult, nsresult>
+ InsertTextWithTransaction(Document& aDocument,
+ const nsAString& aStringToInsert,
+ const EditorDOMPoint& aPointToInsert) final;
+
+ /**
+ * CopyLastEditableChildStyles() clones inline container elements into
+ * aPreviousBlock to aNewBlock to keep using same style in it.
+ *
+ * @param aPreviousBlock The previous block element. All inline
+ * elements which are last sibling of each level
+ * are cloned to aNewBlock.
+ * @param aNewBlock New block container element. All children of
+ * this is deleted first.
+ * @param aEditingHost The editing host.
+ * @return If succeeded, returns a suggesting point to put
+ * caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ CopyLastEditableChildStylesWithTransaction(Element& aPreviousBlock,
+ Element& aNewBlock,
+ const Element& aEditingHost);
+
+ /**
+ * RemoveBlockContainerWithTransaction() removes aElement from the DOM tree
+ * but moves its all children to its parent node and if its parent needs <br>
+ * element to have at least one line-height, this inserts <br> element
+ * automatically.
+ *
+ * @param aElement Block element to be removed.
+ * @return If succeeded, returns a suggesting point to put
+ * caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ RemoveBlockContainerWithTransaction(Element& aElement);
+
+ MOZ_CAN_RUN_SCRIPT nsresult RemoveAttributeOrEquivalent(
+ Element* aElement, nsAtom* aAttribute, bool aSuppressTransaction) final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetAttributeOrEquivalent(
+ Element* aElement, nsAtom* aAttribute, const nsAString& aValue,
+ bool aSuppressTransaction) final;
+ using EditorBase::RemoveAttributeOrEquivalent;
+ using EditorBase::SetAttributeOrEquivalent;
+
+ /**
+ * Returns container element of ranges in Selection. If Selection is
+ * collapsed, returns focus container node (or its parent element).
+ * If Selection selects only one element node, returns the element node.
+ * If Selection is only one range, returns common ancestor of the range.
+ * XXX If there are two or more Selection ranges, this returns parent node
+ * of start container of a range which starts with different node from
+ * start container of the first range.
+ */
+ Element* GetSelectionContainerElement() const;
+
+ /**
+ * DeleteTableCellContentsWithTransaction() removes any contents in cell
+ * elements. If two or more cell elements are selected, this removes
+ * all selected cells' contents. Otherwise, this removes contents of
+ * a cell which contains first selection range. This does not return
+ * error even if selection is not in cell element, just does nothing.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult DeleteTableCellContentsWithTransaction();
+
+ /**
+ * extracts an element from the normal flow of the document and
+ * positions it, and puts it back in the normal flow.
+ * @param aElement [IN] the element
+ * @param aEnabled [IN] true to absolutely position the element,
+ * false to put it back in the normal flow
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetPositionToAbsoluteOrStatic(Element& aElement,
+ bool aEnabled);
+
+ /**
+ * adds aChange to the z-index of an arbitrary element.
+ * @param aElement [IN] the element
+ * @param aChange [IN] relative change to apply to current z-index of
+ * the element
+ * @return The new z-index of the element
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<int32_t, nsresult>
+ AddZIndexWithTransaction(nsStyledElement& aStyledElement, int32_t aChange);
+
+ /**
+ * Join together any adjacent editable text nodes in the range.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult CollapseAdjacentTextNodes(nsRange& aRange);
+
+ static dom::Element* GetLinkElement(nsINode* aNode);
+
+ /**
+ * Helper routines for font size changing.
+ */
+ enum class FontSize { incr, decr };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ SetFontSizeOnTextNode(Text& aTextNode, uint32_t aStartOffset,
+ uint32_t aEndOffset, FontSize aIncrementOrDecrement);
+
+ enum class SplitAtEdges {
+ // SplitNodeDeepWithTransaction() won't split container element
+ // nodes at their edges. I.e., when split point is start or end of
+ // container, it won't be split.
+ eDoNotCreateEmptyContainer,
+ // SplitNodeDeepWithTransaction() always splits containers even
+ // if the split point is at edge of a container. E.g., if split point is
+ // start of an inline element, empty inline element is created as a new left
+ // node.
+ eAllowToCreateEmptyContainer,
+ };
+
+ /**
+ * SplitAncestorStyledInlineElementsAtRangeEdges() splits all ancestor inline
+ * elements in the block at aRange if given style matches with some of them.
+ *
+ * @param aRange Ancestor inline elements of the start and end
+ * boundaries will be split.
+ * @param aStyle The style which you want to split.
+ * RemoveAllStyles instance is allowed to split any
+ * inline elements.
+ * @param aSplitAtEdges Whether this should split elements at start or
+ * end of inline elements or not.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffResult, nsresult>
+ SplitAncestorStyledInlineElementsAtRangeEdges(const EditorDOMRange& aRange,
+ const EditorInlineStyle& aStyle,
+ SplitAtEdges aSplitAtEdges);
+
+ /**
+ * SplitAncestorStyledInlineElementsAt() splits ancestor inline elements at
+ * aPointToSplit if specified style matches with them.
+ *
+ * @param aPointToSplit The point to split style at.
+ * @param aStyle The style which you want to split.
+ * RemoveAllStyles instance is allowed to split any
+ * inline elements.
+ * @param aSplitAtEdges Whether this should split elements at start or
+ * end of inline elements or not.
+ * @return The result of SplitNodeDeepWithTransaction()
+ * with topmost split element. If this didn't
+ * find inline elements to be split, Handled()
+ * returns false.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
+ SplitAncestorStyledInlineElementsAt(const EditorDOMPoint& aPointToSplit,
+ const EditorInlineStyle& aStyle,
+ SplitAtEdges aSplitAtEdges);
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult GetInlinePropertyBase(
+ const EditorInlineStyle& aStyle, const nsAString* aValue, bool* aFirst,
+ bool* aAny, bool* aAll, nsAString* outValue) const;
+
+ /**
+ * ClearStyleAt() splits parent elements to remove the specified style.
+ * If this splits some parent elements at near their start or end, such
+ * empty elements will be removed. Then, remove the specified style
+ * from the point and returns DOM point to put caret.
+ *
+ * @param aPoint The point to clear style at.
+ * @param aStyleToRemove The style which you want to clear.
+ * @param aSpecifiedStyle Whether the class and style attributes should
+ * be preserved or discarded.
+ * @return A candidate position to put caret. If there is
+ * AutoTransactionsConserveSelection instances, this stops
+ * suggesting caret point only in some cases.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ ClearStyleAt(const EditorDOMPoint& aPoint,
+ const EditorInlineStyle& aStyleToRemove,
+ SpecifiedStyle aSpecifiedStyle);
+
+ MOZ_CAN_RUN_SCRIPT nsresult SetPositionToAbsolute(Element& aElement);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ SetPositionToStatic(Element& aElement);
+
+ /**
+ * OnModifyDocument() is called when the editor is changed. This should
+ * be called only by runnable in HTMLEditor::OnDocumentModified() to call
+ * HTMLEditor::OnModifyDocument() with AutoEditActionDataSetter instance.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult OnModifyDocument();
+
+ /**
+ * DoSplitNode() inserts aNewNode and moves all content before or after
+ * aStartOfRightNode to aNewNode.
+ *
+ * @param aStartOfRightNode The point to split. The container will keep
+ * having following or previous content of this.
+ * @param aNewNode The new node called. The previous or following
+ * content of aStartOfRightNode will be moved into
+ * this node.
+ */
+ MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult> DoSplitNode(
+ const EditorDOMPoint& aStartOfRightNode, nsIContent& aNewNode);
+
+ /**
+ * DoJoinNodes() merges contents in aContentToRemove to aContentToKeep and
+ * remove aContentToRemove from the DOM tree. aContentToRemove and
+ * aContentToKeep must have same parent. Additionally, if one of
+ * aContentToRemove or aContentToKeep is a text node, the other must be a
+ * text node.
+ *
+ * @param aContentToKeep The node that will remain after the join.
+ * @param aContentToRemove The node that will be joined with aContentToKeep.
+ * There is no requirement that the two nodes be of
+ * the same type.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DoJoinNodes(nsIContent& aContentToKeep, nsIContent& aContentToRemove);
+
+ /**
+ * Routines for managing the preservation of selection across
+ * various editor actions.
+ */
+ bool ArePreservingSelection() const;
+ void PreserveSelectionAcrossActions();
+ MOZ_CAN_RUN_SCRIPT nsresult RestorePreservedSelection();
+ void StopPreservingSelection();
+
+ /**
+ * Called when JoinNodesTransaction::DoTransaction() did its transaction.
+ * Note that this is not called when undoing nor redoing.
+ *
+ * @param aTransaction The transaction which did join nodes.
+ * @param aDoJoinNodesResult Result of the doing join nodes.
+ */
+ MOZ_CAN_RUN_SCRIPT void DidJoinNodesTransaction(
+ const JoinNodesTransaction& aTransaction, nsresult aDoJoinNodesResult);
+
+ protected: // edit sub-action handler
+ /**
+ * CanHandleHTMLEditSubAction() checks whether there is at least one
+ * selection range or not, and whether the first range is editable.
+ * If it's not editable, `Canceled()` of the result returns true.
+ * If `Selection` is in odd situation, returns an error.
+ *
+ * XXX I think that `IsSelectionEditable()` is better name, but it's already
+ * in `EditorBase`...
+ */
+ enum class CheckSelectionInReplacedElement { Yes, OnlyWhenNotInSameNode };
+ Result<EditActionResult, nsresult> CanHandleHTMLEditSubAction(
+ CheckSelectionInReplacedElement aCheckSelectionInReplacedElement =
+ CheckSelectionInReplacedElement::Yes) const;
+
+ /**
+ * EnsureCaretNotAfterInvisibleBRElement() makes sure that caret is NOT after
+ * padding `<br>` element for preventing insertion after padding `<br>`
+ * element at empty last line.
+ * NOTE: This method should be called only when `Selection` is collapsed
+ * because `Selection` is a pain to work with when not collapsed.
+ * (no good way to extend start or end of selection), so we need to
+ * ignore those types of selections.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ EnsureCaretNotAfterInvisibleBRElement();
+
+ /**
+ * MaybeCreatePaddingBRElementForEmptyEditor() creates padding <br> element
+ * for empty editor if there is no children.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ MaybeCreatePaddingBRElementForEmptyEditor();
+
+ /**
+ * EnsureNoPaddingBRElementForEmptyEditor() removes padding <br> element
+ * for empty editor if there is.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ EnsureNoPaddingBRElementForEmptyEditor();
+
+ /**
+ * ReflectPaddingBRElementForEmptyEditor() scans the tree from the root
+ * element and sets mPaddingBRElementForEmptyEditor if exists, or otherwise
+ * nullptr. Can be used to manage undo/redo.
+ */
+ [[nodiscard]] nsresult ReflectPaddingBRElementForEmptyEditor();
+
+ /**
+ * PrepareInlineStylesForCaret() consider inline styles from top level edit
+ * sub-action and setting it to `mPendingStylesToApplyToNewContent` and clear
+ * inline style cache if necessary.
+ * NOTE: This method should be called only when `Selection` is collapsed.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult PrepareInlineStylesForCaret();
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleInsertText(EditSubAction aEditSubAction,
+ const nsAString& aInsertionString,
+ SelectionHandling aSelectionHandling) final;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertDroppedDataTransferAsAction(
+ AutoEditActionDataSetter& aEditActionData,
+ dom::DataTransfer& aDataTransfer, const EditorDOMPoint& aDroppedAt,
+ nsIPrincipal* aSourcePrincipal) final;
+
+ /**
+ * GetInlineStyles() retrieves the style of aElement and modifies each item of
+ * aPendingStyleCacheArray. This might cause flushing layout at retrieving
+ * computed values of CSS properties.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult GetInlineStyles(
+ Element& aElement, AutoPendingStyleCacheArray& aPendingStyleCacheArray);
+
+ /**
+ * CacheInlineStyles() caches style of aElement into mCachedPendingStyles of
+ * TopLevelEditSubAction. This may cause flushing layout at retrieving
+ * computed value of CSS properties.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ CacheInlineStyles(Element& aElement);
+
+ /**
+ * ReapplyCachedStyles() restores some styles which are disappeared during
+ * handling edit action and it should be restored. This may cause flushing
+ * layout at retrieving computed value of CSS properties.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ReapplyCachedStyles();
+
+ /**
+ * CreateStyleForInsertText() sets CSS properties which are stored in
+ * PendingStyles to proper element node.
+ *
+ * @param aPointToInsertText The point to insert text.
+ * @param aEditingHost The editing host.
+ * @return A suggest point to put caret or unset point.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ CreateStyleForInsertText(const EditorDOMPoint& aPointToInsertText,
+ const Element& aEditingHost);
+
+ /**
+ * GetMostDistantAncestorMailCiteElement() returns most-ancestor mail cite
+ * element. "mail cite element" is <pre> element when it's in plaintext editor
+ * mode or an element with which calling HTMLEditUtils::IsMailCite() returns
+ * true.
+ *
+ * @param aNode The start node to look for parent mail cite elements.
+ */
+ Element* GetMostDistantAncestorMailCiteElement(const nsINode& aNode) const;
+
+ /**
+ * HandleInsertParagraphInMailCiteElement() splits aMailCiteElement at
+ * aPointToSplit.
+ *
+ * @param aMailCiteElement The mail-cite element which should be split.
+ * @param aPointToSplit The point to split.
+ * @param aEditingHost The editing host.
+ * @return Candidate caret position where is at inserted
+ * <br> element into the split point.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ HandleInsertParagraphInMailCiteElement(Element& aMailCiteElement,
+ const EditorDOMPoint& aPointToSplit,
+ const Element& aEditingHost);
+
+ /**
+ * HandleInsertBRElement() inserts a <br> element into aPointToBreak.
+ * This may split container elements at the point and/or may move following
+ * <br> element to immediately after the new <br> element if necessary.
+ *
+ * @param aPointToBreak The point where new <br> element will be
+ * inserted before.
+ * @param aEditingHost Current active editing host.
+ * @return If succeeded, returns new <br> element and
+ * candidate caret point.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ HandleInsertBRElement(const EditorDOMPoint& aPointToBreak,
+ const Element& aEditingHost);
+
+ /**
+ * HandleInsertLinefeed() inserts a linefeed character into aInsertToBreak.
+ *
+ * @param aInsertToBreak The point where new linefeed character will be
+ * inserted before.
+ * @param aEditingHost Current active editing host.
+ * @return A suggest point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ HandleInsertLinefeed(const EditorDOMPoint& aInsertToBreak,
+ const Element& aEditingHost);
+
+ /**
+ * Splits inclusive inline ancestors at both start and end of aRangeItem. If
+ * this splits at every point, this modifies aRangeItem to point each split
+ * point (typically, at right node).
+ *
+ * @param aRangeItem [in/out] One or two DOM points where should be
+ * split. Will be modified to split point if
+ * they're split.
+ * @param aBlockInlineCheck [in] Whether this method considers block vs.
+ * inline with computed style or the default style.
+ * @param aEditingHost [in] The editing host.
+ * @param aAncestorLimiter [in/optional] If specified, this stops splitting
+ * ancestors when meets this node.
+ * @return A suggest point to put caret if succeeded, but
+ * it may be unset.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ SplitInlineAncestorsAtRangeBoundaries(
+ RangeItem& aRangeItem, BlockInlineCheck aBlockInlineCheck,
+ const Element& aEditingHost,
+ const nsIContent* aAncestorLimiter = nullptr);
+
+ /**
+ * SplitElementsAtEveryBRElement() splits before all <br> elements in
+ * aMostAncestorToBeSplit. All <br> nodes will be moved before right node
+ * at splitting its parent. Finally, this returns left node, first <br>
+ * element, next left node, second <br> element... and right-most node.
+ *
+ * @param aMostAncestorToBeSplit Most-ancestor element which should
+ * be split.
+ * @param aOutArrayOfNodes First left node, first <br> element,
+ * Second left node, second <br> element,
+ * ...right-most node. So, all nodes
+ * in this list should be siblings (may be
+ * broken the relation by mutation event
+ * listener though). If first <br> element
+ * is first leaf node of
+ * aMostAncestorToBeSplit, starting from
+ * the first <br> element.
+ * @return A suggest point to put caret if
+ * succeeded, but it may unset.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ SplitElementsAtEveryBRElement(
+ nsIContent& aMostAncestorToBeSplit,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents);
+
+ /**
+ * MaybeSplitElementsAtEveryBRElement() calls SplitElementsAtEveryBRElement()
+ * for each given node when this needs to do that for aEditSubAction.
+ * If split a node, it in aArrayOfContents is replaced with split nodes and
+ * <br> elements.
+ *
+ * @return A suggest point to put caret if
+ * succeeded, but it may unset.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ MaybeSplitElementsAtEveryBRElement(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ EditSubAction aEditSubAction);
+
+ /**
+ * CreateRangeIncludingAdjuscentWhiteSpaces() creates an nsRange instance
+ * which may be expanded from the given range to include adjuscent
+ * white-spaces. If this fails handling something, returns nullptr.
+ */
+ template <typename EditorDOMRangeType>
+ already_AddRefed<nsRange> CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMRangeType& aRange);
+ template <typename EditorDOMPointType1, typename EditorDOMPointType2>
+ already_AddRefed<nsRange> CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMPointType1& aStartPoint,
+ const EditorDOMPointType2& aEndPoint);
+
+ /**
+ * GetRangeExtendedToHardLineEdgesForBlockEditAction() returns an extended
+ * range if aRange should be extended before handling a block level editing.
+ * If aRange start and/or end point <br> or something non-editable point, they
+ * should be moved to nearest text node or something where the other methods
+ * easier to handle edit action.
+ */
+ [[nodiscard]] Result<EditorRawDOMRange, nsresult>
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ const nsRange* aRange, const Element& aEditingHost) const;
+
+ /**
+ * InitializeInsertingElement is a callback type of methods which inserts
+ * an element into the DOM tree. This is called immediately before inserting
+ * aNewElement into the DOM tree.
+ *
+ * @param aHTMLEditor The HTML editor which modifies the DOM tree.
+ * @param aNewElement The new element which will be or was inserted into
+ * the DOM tree.
+ * @param aPointToInsert The position aNewElement will be or was inserted.
+ */
+ using InitializeInsertingElement =
+ std::function<nsresult(HTMLEditor& aHTMLEditor, Element& aNewElement,
+ const EditorDOMPoint& aPointToInsert)>;
+ static InitializeInsertingElement DoNothingForNewElement;
+ static InitializeInsertingElement InsertNewBRElement;
+
+ /**
+ * Helper methods to implement InitializeInsertingElement.
+ */
+ MOZ_CAN_RUN_SCRIPT static Result<CreateElementResult, nsresult>
+ AppendNewElementToInsertingElement(
+ HTMLEditor& aHTMLEditor, const nsStaticAtom& aTagName,
+ Element& aNewElement,
+ const InitializeInsertingElement& aInitializer = DoNothingForNewElement);
+ MOZ_CAN_RUN_SCRIPT static Result<CreateElementResult, nsresult>
+ AppendNewElementWithBRToInsertingElement(HTMLEditor& aHTMLEditor,
+ const nsStaticAtom& aTagName,
+ Element& aNewElement);
+
+ /**
+ * Create an element node whose name is aTag at before aPointToInsert. When
+ * this succeed to create an element node, this inserts the element to
+ * aPointToInsert.
+ *
+ * @param aWithTransaction Whether the inserting is new element is undoable
+ * or not. WithTransaction::No is useful only when
+ * the new element is inserted into a new element
+ * which has not been connected yet.
+ * @param aTagName The element name to create.
+ * @param aPointToInsert The insertion point of new element.
+ * If this refers end of the container or after,
+ * the transaction will append the element to the
+ * container.
+ * Otherwise, will insert the element before the
+ * child node referred by this.
+ * Note that this point will be invalid once this
+ * method inserts the new element.
+ * @param aInitializer A function to initialize the new element before
+ * connecting the element into the DOM tree. Note
+ * that this should not touch outside given element
+ * because doing it would break range updater's
+ * result.
+ * @return The created new element node and candidate caret
+ * position.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ CreateAndInsertElement(
+ WithTransaction aWithTransaction, const nsAtom& aTagName,
+ const EditorDOMPoint& aPointToInsert,
+ const InitializeInsertingElement& aInitializer = DoNothingForNewElement);
+
+ /**
+ * Callback of CopyAttributes().
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aSrcElement The element which have the attribute.
+ * @param aDestElement The element which will have the attribute.
+ * @param aAttr [in] The attribute which will be copied.
+ * @param aValue [in/out] The attribute value which will be copied.
+ * Once updated, the new value is used.
+ * @return true if the attribute should be copied, otherwise,
+ * false.
+ */
+ using AttributeFilter = std::function<bool(
+ HTMLEditor& aHTMLEditor, Element& aSrcElement, Element& aDestElement,
+ const dom::Attr& aAttr, nsString& aValue)>;
+ static AttributeFilter CopyAllAttributes;
+ static AttributeFilter CopyAllAttributesExceptId;
+ static AttributeFilter CopyAllAttributesExceptDir;
+ static AttributeFilter CopyAllAttributesExceptIdAndDir;
+
+ /**
+ * Copy all attributes of aSrcElement to aDestElement as-is. Different from
+ * EditorBase::CloneAttributesWithTransaction(), this does not use
+ * SetAttributeOrEquivalent() nor does not clear existing attributes of
+ * aDestElement.
+ *
+ * @param aWithTransaction Whether recoding with transactions or not.
+ * @param aDestElement The element will have attributes.
+ * @param aSrcElement The element whose attributes will be copied.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult CopyAttributes(
+ WithTransaction aWithTransaction, Element& aDestElement,
+ Element& aSrcElement, const AttributeFilter& = CopyAllAttributes);
+
+ /**
+ * MaybeSplitAncestorsForInsertWithTransaction() does nothing if container of
+ * aStartOfDeepestRightNode can have an element whose tag name is aTag.
+ * Otherwise, looks for an ancestor node which is or is in active editing
+ * host and can have an element whose name is aTag. If there is such
+ * ancestor, its descendants are split.
+ *
+ * Note that this may create empty elements while splitting ancestors.
+ *
+ * @param aTag The name of element to be inserted
+ * after calling this method.
+ * @param aStartOfDeepestRightNode The start point of deepest right node.
+ * This point must be in aEditingHost.
+ * @param aEditingHost The editing host.
+ * @return When succeeded, SplitPoint() returns
+ * the point to insert the element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
+ MaybeSplitAncestorsForInsertWithTransaction(
+ const nsAtom& aTag, const EditorDOMPoint& aStartOfDeepestRightNode,
+ const Element& aEditingHost);
+
+ /**
+ * InsertElementWithSplittingAncestorsWithTransaction() is a wrapper of
+ * MaybeSplitAncestorsForInsertWithTransaction() and CreateAndInsertElement().
+ * I.e., will create an element whose tag name is aTagName and split ancestors
+ * if it's necessary, then, insert it.
+ *
+ * @param aTagName The tag name which you want to insert new
+ * element at aPointToInsert.
+ * @param aPointToInsert The insertion point. New element will be
+ * inserted before here.
+ * @param aBRElementNextToSplitPoint
+ * Whether <br> element should be deleted or
+ * kept if and only if a <br> element follows
+ * split point.
+ * @param aEditingHost The editing host with which we're handling it.
+ * @param aInitializer A function to initialize the new element before
+ * connecting the element into the DOM tree. Note
+ * that this should not touch outside given element
+ * because doing it would break range updater's
+ * result.
+ * @return If succeeded, returns the new element node and
+ * suggesting point to put caret.
+ */
+ enum class BRElementNextToSplitPoint { Keep, Delete };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ InsertElementWithSplittingAncestorsWithTransaction(
+ const nsAtom& aTagName, const EditorDOMPoint& aPointToInsert,
+ BRElementNextToSplitPoint aBRElementNextToSplitPoint,
+ const Element& aEditingHost,
+ const InitializeInsertingElement& aInitializer = DoNothingForNewElement);
+
+ /**
+ * Split aElementToSplit at two points, before aStartOfMiddleElement and after
+ * aEndOfMiddleElement. If they are very start or very end of aBlockElement,
+ * this won't create empty block.
+ *
+ * @param aElementToSplit An element which will be split.
+ * @param aStartOfMiddleElement Start node of middle block element.
+ * @param aEndOfMiddleElement End node of middle block element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
+ SplitRangeOffFromElement(Element& aElementToSplit,
+ nsIContent& aStartOfMiddleElement,
+ nsIContent& aEndOfMiddleElement);
+
+ /**
+ * RemoveBlockContainerElementWithTransactionBetween() splits the nodes
+ * at aStartOfRange and aEndOfRange, then, removes the middle element which
+ * was split off from aBlockContainerElement and moves the ex-children to
+ * where the middle element was. I.e., all nodes between aStartOfRange and
+ * aEndOfRange (including themselves) will be unwrapped from
+ * aBlockContainerElement.
+ *
+ * @param aBlockContainerElement The node which will be split.
+ * @param aStartOfRange The first node which will be unwrapped
+ * from aBlockContainerElement.
+ * @param aEndOfRange The last node which will be unwrapped from
+ * aBlockContainerElement.
+ * @param aBlockInlineCheck Whether this method considers block vs.
+ * inline with computed style or the default
+ * style.
+ * @return The left content is new created left
+ * element of aBlockContainerElement.
+ * The right content is split element,
+ * i.e., must be aBlockContainerElement.
+ * The middle content is nullptr since
+ * removing it is the job of this method.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
+ RemoveBlockContainerElementWithTransactionBetween(
+ Element& aBlockContainerElement, nsIContent& aStartOfRange,
+ nsIContent& aEndOfRange, BlockInlineCheck aBlockInlineCheck);
+
+ /**
+ * WrapContentsInBlockquoteElementsWithTransaction() inserts at least one
+ * <blockquote> element and moves nodes in aArrayOfContents into new
+ * <blockquote> elements. If aArrayOfContents includes a table related element
+ * except <table>, this calls itself recursively to insert <blockquote> into
+ * the cell.
+ *
+ * @param aArrayOfContents Nodes which will be moved into created
+ * <blockquote> elements.
+ * @param aEditingHost The editing host.
+ * @return A blockquote element which is created at last
+ * and a suggest of caret position if succeeded.
+ * The caret suggestion may be unset if there is
+ * no suggestion.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ WrapContentsInBlockquoteElementsWithTransaction(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ const Element& aEditingHost);
+
+ /**
+ * Our traditional formatBlock was same as XUL cmd_paragraphState command.
+ * However, the behavior is pretty different from the others and aligning
+ * the XUL command behavior may break Thunderbird a lot because it handles
+ * <blockquote> in a special path and <div> (generic block element) is not
+ * treated as a format node and these things may be used for designing
+ * current roles of the elements in the email composer of Thunderbird.
+ * Therefore, we create a new mode for HTMLFormatBlockCommand to align
+ * the behavior to the others but does not harm Thunderbird.
+ */
+ enum class FormatBlockMode {
+ // Document.execCommand("formatBlock"). Cannot set new format to "normal"
+ // nor "". So, the paths to handle these ones are not used in this mode.
+ HTMLFormatBlockCommand,
+ // cmd_paragraphState. Can set new format to "normal" or "" to remove
+ // ancestor format blocks.
+ XULParagraphStateCommand,
+ };
+
+ /**
+ * RemoveBlockContainerElementsWithTransaction() removes all format blocks,
+ * table related element, etc in aArrayOfContents from the DOM tree. If
+ * aArrayOfContents has a format node, it will be removed and its contents
+ * will be moved to where it was.
+ * If aArrayOfContents has a table related element, <li>, <blockquote> or
+ * <div>, it will be removed and its contents will be moved to where it was.
+ *
+ * @param aFormatBlockMode Whether HTML formatBlock command or XUL
+ * paragraphState command.
+ *
+ * @return A suggest point to put caret if succeeded, but it may be
+ * unset if there is no suggestion.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ RemoveBlockContainerElementsWithTransaction(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ FormatBlockMode aFormatBlockMode, BlockInlineCheck aBlockInlineCheck);
+
+ /**
+ * CreateOrChangeFormatContainerElement() formats all nodes in
+ * aArrayOfContents with block elements whose name is aNewFormatTagName.
+ *
+ * If aArrayOfContents has an inline element, a block element is created and
+ * the inline element and following inline elements are moved into the new
+ * block element.
+ * If aArrayOfContents has <br> elements, they'll be removed from the DOM tree
+ * and new block element will be created when there are some remaining inline
+ * elements.
+ * If aArrayOfContents has a block element, this calls itself with children of
+ * the block element. Then, new block element will be created when there are
+ * some remaining inline elements.
+ *
+ * @param aArrayOfContents Must be descendants of a node.
+ * @param aNewFormatTagName The element name of new block elements.
+ * @param aFormatBlockMode The replacing block element target type is for
+ * whether HTML formatBLock command or XUL
+ * paragraphState command.
+ * @param aEditingHost The editing host.
+ * @return The latest created new block element and a
+ * suggest point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ CreateOrChangeFormatContainerElement(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ const nsStaticAtom& aNewFormatTagName, FormatBlockMode aFormatBlockMode,
+ const Element& aEditingHost);
+
+ /**
+ * FormatBlockContainerWithTransaction() is implementation of "formatBlock"
+ * command of `Document.execCommand()`. This applies block style or removes
+ * it.
+ *
+ * @param aSelectionRanges The ranges which are cloned by selection or
+ * updated from it with doing something before
+ * calling this.
+ * @param aNewFormatTagName New block tag name.
+ * If nsGkAtoms::normal or nsGkAtoms::_empty,
+ * RemoveBlockContainerElementsWithTransaction()
+ * will be called.
+ * If nsGkAtoms::blockquote,
+ * WrapContentsInBlockquoteElementsWithTransaction()
+ * will be called.
+ * Otherwise, CreateOrChangeBlockContainerElement()
+ * will be called.
+ * @param aFormatBlockMode Whether HTML formatBlock command or XUL
+ * paragraphState command.
+ * @param aEditingHost The editing host.
+ * @return If selection should be finally collapsed in a
+ * created block element, this returns the element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
+ FormatBlockContainerWithTransaction(AutoRangeArray& aSelectionRanges,
+ const nsStaticAtom& aNewFormatTagName,
+ FormatBlockMode aFormatBlockMode,
+ const Element& aEditingHost);
+
+ /**
+ * Determine if aPointToInsert is start of a hard line and end of the line
+ * (i.e, in an empty line) and the line ends with block boundary, inserts a
+ * `<br>` element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ const EditorDOMPoint& aPointToInsert);
+
+ /**
+ * Insert padding `<br>` element for empty last line into aElement if
+ * aElement is a block element and empty.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ InsertPaddingBRElementForEmptyLastLineIfNeeded(Element& aElement);
+
+ /**
+ * This method inserts a padding `<br>` element for empty last line if
+ * selection is collapsed and container of the range needs it.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+
+ /**
+ * SplitParagraphWithTransaction() splits the parent block, aParentDivOrP, at
+ * aStartOfRightNode.
+ *
+ * @param aParentDivOrP The parent block to be split. This must be <p>
+ * or <div> element.
+ * @param aStartOfRightNode The point to be start of right node after
+ * split. This must be descendant of
+ * aParentDivOrP.
+ * @param aMayBecomeVisibleBRElement
+ * Next <br> element of the split point if there
+ * is. Otherwise, nullptr. If this is not nullptr,
+ * the <br> element may be removed if it becomes
+ * visible.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
+ SplitParagraphWithTransaction(Element& aParentDivOrP,
+ const EditorDOMPoint& aStartOfRightNode,
+ dom::HTMLBRElement* aMayBecomeVisibleBRElement);
+
+ /**
+ * HandleInsertParagraphInParagraph() does the right thing for Enter key
+ * press or 'insertParagraph' command in aParentDivOrP. aParentDivOrP will
+ * be split **around** aCandidatePointToSplit. If this thinks that it should
+ * be handled to insert a <br> instead, this returns "not handled".
+ *
+ * @param aParentDivOrP The parent block. This must be <p> or <div>
+ * element.
+ * @param aCandidatePointToSplit
+ * The point where the caller want to split
+ * aParentDivOrP. However, in some cases, this is not
+ * used as-is. E.g., this method avoids to create new
+ * empty <a href> in the right paragraph. So this may
+ * be adjusted to proper position around it.
+ * @param aEditingHost The editing host.
+ * @return If the caller should default to inserting <br>
+ * element, returns "not handled".
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
+ HandleInsertParagraphInParagraph(Element& aParentDivOrP,
+ const EditorDOMPoint& aCandidatePointToSplit,
+ const Element& aEditingHost);
+
+ /**
+ * HandleInsertParagraphInHeadingElement() handles insertParagraph command
+ * (i.e., handling Enter key press) in a heading element. This splits
+ * aHeadingElement element at aPointToSplit. Then, if right heading element
+ * is empty, it'll be removed and new paragraph is created (its type is
+ * decided with default paragraph separator).
+ *
+ * @param aHeadingElement The heading element to be split.
+ * @param aPointToSplit The point to split aHeadingElement.
+ * @return New paragraph element, meaning right heading
+ * element if aHeadingElement is split, or newly
+ * created or existing paragraph element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertParagraphResult, nsresult>
+ HandleInsertParagraphInHeadingElement(Element& aHeadingElement,
+ const EditorDOMPoint& aPointToSplit);
+
+ /**
+ * HandleInsertParagraphInListItemElement() handles insertParagraph command
+ * (i.e., handling Enter key press) in a list item element.
+ *
+ * @param aListItemElement The list item which has the following point.
+ * @param aPointToSplit The point to split aListItemElement.
+ * @param aEditingHost The editing host.
+ * @return New paragraph element, meaning right list item
+ * element if aListItemElement is split, or newly
+ * created paragraph element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertParagraphResult, nsresult>
+ HandleInsertParagraphInListItemElement(Element& aListItemElement,
+ const EditorDOMPoint& aPointToSplit,
+ const Element& aEditingHost);
+
+ /**
+ * InsertParagraphSeparatorAsSubAction() handles insertPargraph commad
+ * (i.e., handling Enter key press) with the above helper methods.
+ *
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ InsertParagraphSeparatorAsSubAction(const Element& aEditingHost);
+
+ /**
+ * InsertLineBreakAsSubAction() inserts a new <br> element or a linefeed
+ * character at selection. If there is non-collapsed selection ranges, the
+ * selected ranges is deleted first.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertLineBreakAsSubAction();
+
+ /**
+ * ChangeListElementType() replaces child list items of aListElement with
+ * new list item element whose tag name is aNewListItemTag.
+ * Note that if there are other list elements as children of aListElement,
+ * this calls itself recursively even though it's invalid structure.
+ *
+ * @param aListElement The list element whose list items will be
+ * replaced.
+ * @param aNewListTag New list tag name.
+ * @param aNewListItemTag New list item tag name.
+ * @return New list element or an error code if it fails.
+ * New list element may be aListElement if its
+ * tag name is same as aNewListTag.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ ChangeListElementType(Element& aListElement, nsAtom& aListType,
+ nsAtom& aItemType);
+
+ class AutoListElementCreator;
+
+ [[nodiscard]] static bool IsFormatElement(FormatBlockMode aFormatBlockMode,
+ const nsIContent& aContent);
+
+ /**
+ * MakeOrChangeListAndListItemAsSubAction() handles create list commands with
+ * current selection. If
+ *
+ * @param aListElementOrListItemElementTagName
+ * The new list element tag name or
+ * new list item tag name.
+ * If the former, list item tag name will
+ * be computed automatically. Otherwise,
+ * list tag name will be computed.
+ * @param aBulletType If this is not empty string, it's set
+ * to `type` attribute of new list item
+ * elements. Otherwise, existing `type`
+ * attributes will be removed.
+ * @param aSelectAllOfCurrentList Yes if this should treat all of
+ * ancestor list element at selection.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ MakeOrChangeListAndListItemAsSubAction(
+ const nsStaticAtom& aListElementOrListItemElementTagName,
+ const nsAString& aBulletType,
+ SelectAllOfCurrentList aSelectAllOfCurrentList);
+
+ /**
+ * DeleteTextAndTextNodesWithTransaction() removes text or text nodes in
+ * the given range.
+ */
+ enum class TreatEmptyTextNodes {
+ // KeepIfContainerOfRangeBoundaries:
+ // Will remove empty text nodes middle of the range, but keep empty
+ // text nodes which are containers of range boundaries.
+ KeepIfContainerOfRangeBoundaries,
+ // Remove:
+ // Will remove all empty text nodes.
+ Remove,
+ // RemoveAllEmptyInlineAncestors:
+ // Will remove all empty text nodes and its inline ancestors which
+ // become empty due to removing empty text nodes.
+ RemoveAllEmptyInlineAncestors,
+ };
+ template <typename EditorDOMPointType>
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ DeleteTextAndTextNodesWithTransaction(
+ const EditorDOMPointType& aStartPoint,
+ const EditorDOMPointType& aEndPoint,
+ TreatEmptyTextNodes aTreatEmptyTextNodes);
+
+ /**
+ * JoinNodesWithTransaction() joins aLeftContent and aRightContent. Content
+ * of aLeftContent will be merged into aRightContent. Actual implemenation of
+ * this method is JoinNodesImpl(). So, see its explanation for the detail.
+ *
+ * @param aLeftContent Will be removed from the DOM tree.
+ * @param aRightContent The node which will be new container of the content
+ * of aLeftContent.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<JoinNodesResult, nsresult>
+ JoinNodesWithTransaction(nsIContent& aLeftContent, nsIContent& aRightContent);
+
+ /**
+ * JoinNearestEditableNodesWithTransaction() joins two editable nodes which
+ * are themselves or the nearest editable node of aLeftNode and aRightNode.
+ * XXX This method's behavior is odd. For example, if user types Backspace
+ * key at the second editable paragraph in this case:
+ * <div contenteditable>
+ * <p>first editable paragraph</p>
+ * <p contenteditable="false">non-editable paragraph</p>
+ * <p>second editable paragraph</p>
+ * </div>
+ * The first editable paragraph's content will be moved into the second
+ * editable paragraph and the non-editable paragraph becomes the first
+ * paragraph of the editor. I don't think that it's expected behavior of
+ * any users...
+ *
+ * @param aLeftNode The node which will be removed.
+ * @param aRightNode The node which will be inserted the content of
+ * aLeftNode.
+ * @param aNewFirstChildOfRightNode
+ * [out] The point at the first child of aRightNode.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ JoinNearestEditableNodesWithTransaction(
+ nsIContent& aLeftNode, nsIContent& aRightNode,
+ EditorDOMPoint* aNewFirstChildOfRightNode);
+
+ /**
+ * ReplaceContainerAndCloneAttributesWithTransaction() creates new element
+ * whose name is aTagName, copies all attributes from aOldContainer to the
+ * new element, moves all children in aOldContainer to the new element, then,
+ * removes aOldContainer from the DOM tree.
+ *
+ * @param aOldContainer The element node which should be replaced
+ * with new element.
+ * @param aTagName The name of new element node.
+ */
+ [[nodiscard]] inline MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ ReplaceContainerAndCloneAttributesWithTransaction(Element& aOldContainer,
+ const nsAtom& aTagName);
+
+ /**
+ * ReplaceContainerWithTransaction() creates new element whose name is
+ * aTagName, sets aAttributes of the new element to aAttributeValue, moves
+ * all children in aOldContainer to the new element, then, removes
+ * aOldContainer from the DOM tree.
+ *
+ * @param aOldContainer The element node which should be replaced
+ * with new element.
+ * @param aTagName The name of new element node.
+ * @param aAttribute Attribute name to be set to the new element.
+ * @param aAttributeValue Attribute value to be set to aAttribute.
+ */
+ [[nodiscard]] inline MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ ReplaceContainerWithTransaction(Element& aOldContainer,
+ const nsAtom& aTagName,
+ const nsAtom& aAttribute,
+ const nsAString& aAttributeValue);
+
+ /**
+ * ReplaceContainerWithTransaction() creates new element whose name is
+ * aTagName, moves all children in aOldContainer to the new element, then,
+ * removes aOldContainer from the DOM tree.
+ *
+ * @param aOldContainer The element node which should be replaced
+ * with new element.
+ * @param aTagName The name of new element node.
+ */
+ [[nodiscard]] inline MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ ReplaceContainerWithTransaction(Element& aOldContainer,
+ const nsAtom& aTagName);
+
+ /**
+ * RemoveContainerWithTransaction() removes aElement from the DOM tree and
+ * moves all its children to the parent of aElement.
+ *
+ * @param aElement The element to be removed.
+ * @return A suggestion point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ RemoveContainerWithTransaction(Element& aElement);
+
+ /**
+ * InsertContainerWithTransaction() creates new element whose name is
+ * aWrapperTagName, moves aContentToBeWrapped into the new element, then,
+ * inserts the new element into where aContentToBeWrapped was.
+ * NOTE: This method does not check if aContentToBeWrapped is valid child
+ * of the new element. So, callers need to guarantee it.
+ *
+ * @param aContentToBeWrapped The content which will be wrapped with new
+ * element.
+ * @param aWrapperTagName Element name of new element which will wrap
+ * aContent and be inserted into where aContent
+ * was.
+ * @param aInitializer A callback to initialize new element before
+ * inserting to the DOM tree.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ InsertContainerWithTransaction(
+ nsIContent& aContentToBeWrapped, const nsAtom& aWrapperTagName,
+ const InitializeInsertingElement& aInitializer = DoNothingForNewElement);
+
+ /**
+ * MoveNodeWithTransaction() moves aContentToMove to aPointToInsert.
+ *
+ * @param aContentToMove The node to be moved.
+ * @param aPointToInsert The point where aContentToMove will be inserted.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<MoveNodeResult, nsresult>
+ MoveNodeWithTransaction(nsIContent& aContentToMove,
+ const EditorDOMPoint& aPointToInsert);
+
+ /**
+ * MoveNodeToEndWithTransaction() moves aContentToMove to end of
+ * aNewContainer.
+ *
+ * @param aContentToMove The node to be moved.
+ * @param aNewContainer The new container which will contain aContentToMove
+ * as its last child.
+ */
+ [[nodiscard]] inline MOZ_CAN_RUN_SCRIPT Result<MoveNodeResult, nsresult>
+ MoveNodeToEndWithTransaction(nsIContent& aContentToMove,
+ nsINode& aNewContainer);
+
+ /**
+ * MoveNodeOrChildrenWithTransaction() moves aContent to aPointToInsert. If
+ * cannot insert aContent due to invalid relation, moves only its children
+ * recursively and removes aContent from the DOM tree.
+ *
+ * @param aContent Content which should be moved.
+ * @param aPointToInsert The point to be inserted aContent or its
+ * descendants.
+ * @param aPreserveWhiteSpaceStyle
+ * If yes and if it's possible to keep white-space
+ * style, this method will set `style` attribute to
+ * moving node or creating new <span> element.
+ * @param aRemoveIfCommentNode
+ * If yes, this removes a comment node instead of
+ * moving it to the destination. Note that this
+ * does not remove comment nodes in moving nodes
+ * because it requires additional scan.
+ */
+ enum class PreserveWhiteSpaceStyle { No, Yes };
+ friend std::ostream& operator<<(
+ std::ostream& aStream,
+ const PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle);
+ enum class RemoveIfCommentNode { No, Yes };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<MoveNodeResult, nsresult>
+ MoveNodeOrChildrenWithTransaction(
+ nsIContent& aContentToMove, const EditorDOMPoint& aPointToInsert,
+ PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle,
+ RemoveIfCommentNode aRemoveIfCommentNode);
+
+ /**
+ * CanMoveNodeOrChildren() returns true if
+ * `MoveNodeOrChildrenWithTransaction()` can move or delete at least a
+ * descendant of aElement into aNewContainer. I.e., when this returns true,
+ * `MoveNodeOrChildrenWithTransaction()` must return "handled".
+ */
+ Result<bool, nsresult> CanMoveNodeOrChildren(
+ const nsIContent& aContent, const nsINode& aNewContainer) const;
+
+ /**
+ * MoveChildrenWithTransaction() moves the children of aElement to
+ * aPointToInsert. If cannot insert some children due to invalid relation,
+ * calls MoveNodeOrChildrenWithTransaction() to remove the children but keep
+ * moving its children.
+ *
+ * @param aElement Container element whose children should be
+ * moved.
+ * @param aPointToInsert The point to be inserted children of aElement
+ * or its descendants.
+ * @param aPreserveWhiteSpaceStyle
+ * If yes and if it's possible to keep white-space
+ * style, this method will set `style` attribute to
+ * moving node or creating new <span> element.
+ * @param aRemoveIfCommentNode
+ * If yes, this removes a comment node instead of
+ * moving it to the destination. Note that this
+ * does not remove comment nodes in moving nodes
+ * because it requires additional scan.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<MoveNodeResult, nsresult>
+ MoveChildrenWithTransaction(Element& aElement,
+ const EditorDOMPoint& aPointToInsert,
+ PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle,
+ RemoveIfCommentNode aRemoveIfCommentNode);
+
+ /**
+ * CanMoveChildren() returns true if `MoveChildrenWithTransaction()` can move
+ * at least a descendant of aElement into aNewContainer. I.e., when this
+ * returns true, `MoveChildrenWithTransaction()` return "handled".
+ */
+ Result<bool, nsresult> CanMoveChildren(const Element& aElement,
+ const nsINode& aNewContainer) const;
+
+ /**
+ * MoveAllChildren() moves all children of aContainer to before
+ * aPointToInsert.GetChild().
+ * See explanation of MoveChildrenBetween() for the detail of the behavior.
+ *
+ * @param aContainer The container node whose all children should
+ * be moved.
+ * @param aPointToInsert The insertion point. The container must not
+ * be a data node like a text node.
+ */
+ [[nodiscard]] nsresult MoveAllChildren(
+ nsINode& aContainer, const EditorRawDOMPoint& aPointToInsert);
+
+ /**
+ * MoveChildrenBetween() moves all children between aFirstChild and aLastChild
+ * to before aPointToInsert.GetChild(). If some children are moved to
+ * different container while this method moves other children, they are just
+ * ignored. If the child node referred by aPointToInsert is moved to different
+ * container while this method moves children, returns error.
+ *
+ * @param aFirstChild The first child which should be moved to
+ * aPointToInsert.
+ * @param aLastChild The last child which should be moved. This
+ * must be a sibling of aFirstChild and it should
+ * be positioned after aFirstChild in the DOM tree
+ * order.
+ * @param aPointToInsert The insertion point. The container must not
+ * be a data node like a text node.
+ */
+ [[nodiscard]] nsresult MoveChildrenBetween(
+ nsIContent& aFirstChild, nsIContent& aLastChild,
+ const EditorRawDOMPoint& aPointToInsert);
+
+ /**
+ * MovePreviousSiblings() moves all siblings before aChild (i.e., aChild
+ * won't be moved) to before aPointToInsert.GetChild().
+ * See explanation of MoveChildrenBetween() for the detail of the behavior.
+ *
+ * @param aChild The node which is next sibling of the last
+ * node to be moved.
+ * @param aPointToInsert The insertion point. The container must not
+ * be a data node like a text node.
+ */
+ [[nodiscard]] nsresult MovePreviousSiblings(
+ nsIContent& aChild, const EditorRawDOMPoint& aPointToInsert);
+
+ /**
+ * MoveInclusiveNextSiblings() moves aChild and all siblings after it to
+ * before aPointToInsert.GetChild().
+ * See explanation of MoveChildrenBetween() for the detail of the behavior.
+ *
+ * @param aChild The node which is first node to be moved.
+ * @param aPointToInsert The insertion point. The container must not
+ * be a data node like a text node.
+ */
+ [[nodiscard]] nsresult MoveInclusiveNextSiblings(
+ nsIContent& aChild, const EditorRawDOMPoint& aPointToInsert);
+
+ /**
+ * SplitNodeWithTransaction() creates a transaction to create a new node
+ * (left node) identical to an existing node (right node), and split the
+ * contents between the same point in both nodes, then, execute the
+ * transaction.
+ *
+ * @param aStartOfRightNode The point to split. Its container will be
+ * the right node, i.e., become the new node's
+ * next sibling. And the point will be start
+ * of the right node.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
+ SplitNodeWithTransaction(const EditorDOMPoint& aStartOfRightNode);
+
+ /**
+ * SplitNodeDeepWithTransaction() splits aMostAncestorToSplit deeply.
+ *
+ * @param aMostAncestorToSplit The most ancestor node which should be
+ * split.
+ * @param aStartOfDeepestRightNode The start point of deepest right node.
+ * This point must be descendant of
+ * aMostAncestorToSplit.
+ * @param aSplitAtEdges Whether the caller allows this to
+ * create empty container element when
+ * split point is start or end of an
+ * element.
+ * @return SplitPoint() returns split point in
+ * aMostAncestorToSplit. The point must
+ * be good to insert something if the
+ * caller want to do it.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
+ SplitNodeDeepWithTransaction(nsIContent& aMostAncestorToSplit,
+ const EditorDOMPoint& aDeepestStartOfRightNode,
+ SplitAtEdges aSplitAtEdges);
+
+ /**
+ * RemoveEmptyInclusiveAncestorInlineElements() removes empty inclusive
+ * ancestor inline elements in inclusive ancestor block element of aContent.
+ *
+ * @param aContent Must be an empty content.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ RemoveEmptyInclusiveAncestorInlineElements(nsIContent& aContent);
+
+ /**
+ * DeleteTextAndNormalizeSurroundingWhiteSpaces() deletes text between
+ * aStartToDelete and immediately before aEndToDelete and return new caret
+ * position. If surrounding characters are white-spaces, this normalize them
+ * too. Finally, inserts `<br>` element if it's required.
+ * Note that if you wants only normalizing white-spaces, you can set same
+ * point to both aStartToDelete and aEndToDelete. Then, this tries to
+ * normalize white-space sequence containing previous character of
+ * aStartToDelete.
+ */
+ enum class DeleteDirection {
+ Forward,
+ Backward,
+ };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ DeleteTextAndNormalizeSurroundingWhiteSpaces(
+ const EditorDOMPointInText& aStartToDelete,
+ const EditorDOMPointInText& aEndToDelete,
+ TreatEmptyTextNodes aTreatEmptyTextNodes,
+ DeleteDirection aDeleteDirection);
+
+ /**
+ * ExtendRangeToDeleteWithNormalizingWhiteSpaces() is a helper method of
+ * DeleteTextAndNormalizeSurroundingWhiteSpaces(). This expands
+ * aStartToDelete and/or aEndToDelete if there are white-spaces which need
+ * normalizing.
+ *
+ * @param aStartToDelete [In/Out] Start to delete. If this point
+ * follows white-spaces, this may be modified.
+ * @param aEndToDelete [In/Out] Next point of last content to be
+ * deleted. If this point is a white-space,
+ * this may be modified.
+ * @param aNormalizedWhiteSpacesInStartNode
+ * [Out] If container text node of aStartToDelete
+ * should be modified, this offers new string
+ * in the range in the text node.
+ * @param aNormalizedWhiteSpacesInEndNode
+ * [Out] If container text node of aEndToDelete
+ * is different from the container of
+ * aStartToDelete and it should be modified, this
+ * offers new string in the range in the text node.
+ */
+ void ExtendRangeToDeleteWithNormalizingWhiteSpaces(
+ EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
+ nsAString& aNormalizedWhiteSpacesInStartNode,
+ nsAString& aNormalizedWhiteSpacesInEndNode) const;
+
+ /**
+ * CharPointType let the following helper methods of
+ * ExtendRangeToDeleteWithNormalizingWhiteSpaces() know what type of
+ * character will be previous or next char point after deletion.
+ */
+ enum class CharPointType {
+ TextEnd, // Start or end of the text (hardline break or replaced inline
+ // element)
+ ASCIIWhiteSpace, // One of ASCII white-spaces (collapsible white-space)
+ NoBreakingSpace, // NBSP
+ VisibleChar, // Non-white-space characters
+ PreformattedChar, // Any character except a linefeed in a preformatted
+ // node.
+ PreformattedLineBreak, // Preformatted linebreak
+ };
+
+ /**
+ * GetPreviousCharPointType() and GetCharPointType() get type of
+ * previous/current char point from current DOM tree. In other words, if the
+ * point will be deleted, you cannot use these methods.
+ */
+ template <typename EditorDOMPointType>
+ static CharPointType GetPreviousCharPointType(
+ const EditorDOMPointType& aPoint);
+ template <typename EditorDOMPointType>
+ static CharPointType GetCharPointType(const EditorDOMPointType& aPoint);
+
+ /**
+ * CharPointData let the following helper methods of
+ * ExtendRangeToDeleteWithNormalizingWhiteSpaces() know what type of
+ * character will be previous or next char point and the point is
+ * in same or different text node after deletion.
+ */
+ class MOZ_STACK_CLASS CharPointData final {
+ public:
+ static CharPointData InDifferentTextNode(CharPointType aCharPointType) {
+ CharPointData result;
+ result.mIsInDifferentTextNode = true;
+ result.mType = aCharPointType;
+ return result;
+ }
+ static CharPointData InSameTextNode(CharPointType aCharPointType) {
+ CharPointData result;
+ // Let's mark this as in different text node if given one indicates
+ // that there is end of text because it means that adjacent content
+ // from point of text node view is another element.
+ result.mIsInDifferentTextNode = aCharPointType == CharPointType::TextEnd;
+ result.mType = aCharPointType;
+ return result;
+ }
+
+ bool AcrossTextNodeBoundary() const { return mIsInDifferentTextNode; }
+ bool IsCollapsibleWhiteSpace() const {
+ return mType == CharPointType::ASCIIWhiteSpace ||
+ mType == CharPointType::NoBreakingSpace;
+ }
+ CharPointType Type() const { return mType; }
+
+ private:
+ CharPointData() = default;
+
+ CharPointType mType;
+ bool mIsInDifferentTextNode;
+ };
+
+ /**
+ * GetPreviousCharPointDataForNormalizingWhiteSpaces() and
+ * GetInclusiveNextCharPointDataForNormalizingWhiteSpaces() is helper methods
+ * of ExtendRangeToDeleteWithNormalizingWhiteSpaces(). This retrieves
+ * previous or inclusive next editable char point and returns its data.
+ */
+ CharPointData GetPreviousCharPointDataForNormalizingWhiteSpaces(
+ const EditorDOMPointInText& aPoint) const;
+ CharPointData GetInclusiveNextCharPointDataForNormalizingWhiteSpaces(
+ const EditorDOMPointInText& aPoint) const;
+
+ /**
+ * GenerateWhiteSpaceSequence() generates white-space sequence which won't
+ * be collapsed.
+ *
+ * @param aResult [out] White space sequence which won't be
+ * collapsed, but wrapable.
+ * @param aLength Length of generating white-space sequence.
+ * Must be 1 or larger.
+ * @param aPreviousCharPointData
+ * Specify the previous char point where it'll be
+ * inserted. Currently, for keepin this method
+ * simple, does not support to generate a part
+ * of white-space sequence in a text node. So,
+ * if the type is white-space, it must indicate
+ * different text nodes white-space.
+ * @param aNextCharPointData Specify the next char point where it'll be
+ * inserted. Same as aPreviousCharPointData,
+ * this must node indidate white-space in same
+ * text node.
+ */
+ static void GenerateWhiteSpaceSequence(
+ nsAString& aResult, uint32_t aLength,
+ const CharPointData& aPreviousCharPointData,
+ const CharPointData& aNextCharPointData);
+
+ /**
+ * ComputeTargetRanges() computes actual delete ranges which will be deleted
+ * unless the following `beforeinput` event is canceled.
+ *
+ * @param aDirectionAndAmount The direction and amount of deletion.
+ * @param aRangesToDelete [In/Out] The ranges to be deleted,
+ * typically, initialized with the
+ * selection ranges. This may be modified
+ * if selection ranges should be extened.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ ComputeTargetRanges(nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete) const;
+
+ /**
+ * This method handles "delete selection" commands.
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aStripWrappers Must be eStrip or eNoStrip.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleDeleteSelection(nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) final;
+
+ class AutoDeleteRangesHandler;
+ class AutoMoveOneLineHandler;
+
+ /**
+ * DeleteMostAncestorMailCiteElementIfEmpty() deletes most ancestor
+ * mail cite element (`<blockquote type="cite">` or
+ * `<span _moz_quote="true">`, the former can be created with middle click
+ * paste with `Control` or `Command` even in the web) of aContent if it
+ * becomes empty.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteMostAncestorMailCiteElementIfEmpty(nsIContent& aContent);
+
+ /**
+ * LiftUpListItemElement() moves aListItemElement outside its parent.
+ * If it's in a middle of a list element, the parent list element is split
+ * before aListItemElement. Then, moves aListItemElement to before its
+ * parent list element. I.e., moves aListItemElement between the 2 list
+ * elements if original parent was split. Then, if new parent becomes not a
+ * list element, the list item element is removed and its contents are moved
+ * to where the list item element was. If aListItemElement becomse not a
+ * child of list element, its contents are unwrapped from aListItemElement.
+ *
+ * @param aListItemElement Must be a <li>, <dt> or <dd> element.
+ * @param aLiftUpFromAllParentListElements
+ * If Yes, this method calls itself recursively
+ * to unwrap the contents in aListItemElement
+ * from any ancestor list elements.
+ * XXX This checks only direct parent of list
+ * elements. Therefore, if a parent list
+ * element in a list item element, the
+ * list item element and its list element
+ * won't be unwrapped.
+ */
+ enum class LiftUpFromAllParentListElements { Yes, No };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult LiftUpListItemElement(
+ dom::Element& aListItemElement,
+ LiftUpFromAllParentListElements aLiftUpFromAllParentListElements);
+
+ /**
+ * DestroyListStructureRecursively() destroys the list structure of
+ * aListElement recursively.
+ * If aListElement has <li>, <dl> or <dt> as a child, the element is removed
+ * but its descendants are moved to where the list item element was.
+ * If aListElement has another <ul>, <ol> or <dl> as a child, this method is
+ * called recursively.
+ * If aListElement has other nodes as its child, they are just removed.
+ * Finally, aListElement is removed. and its all children are moved to
+ * where the aListElement was.
+ *
+ * @param aListElement A <ul>, <ol> or <dl> element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DestroyListStructureRecursively(Element& aListElement);
+
+ /**
+ * RemoveListAtSelectionAsSubAction() removes list elements and list item
+ * elements at Selection. And move contents in them where the removed list
+ * was.
+ *
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ RemoveListAtSelectionAsSubAction(const Element& aEditingHost);
+
+ /**
+ * ChangeMarginStart() changes margin of aElement to indent or outdent.
+ * If it's rtl text, margin-right will be changed. Otherwise, margin-left.
+ * XXX This is not aware of vertical writing-mode.
+ *
+ * @param aElement The element whose start margin should be
+ * changed.
+ * @param aChangeMargin Whether increase or decrease the margin.
+ * @param aEditingHost The editing host.
+ * @return May suggest a suggest point to put caret.
+ */
+ enum class ChangeMargin { Increase, Decrease };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ ChangeMarginStart(Element& aElement, ChangeMargin aChangeMargin,
+ const Element& aEditingHost);
+
+ /**
+ * HandleCSSIndentAroundRanges() indents around aRanges with CSS.
+ *
+ * @param aRanges The ranges where the content should be indented.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleCSSIndentAroundRanges(
+ AutoRangeArray& aRanges, const Element& aEditingHost);
+
+ /**
+ * HandleCSSIndentAroundRanges() indents around aRanges with HTML.
+ *
+ * @param aRanges The ranges where the content should be indented.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleHTMLIndentAroundRanges(
+ AutoRangeArray& aRanges, const Element& aEditingHost);
+
+ /**
+ * HandleIndentAtSelection() indents around Selection with HTML or CSS.
+ *
+ * @param aEditingHost The editing host.
+ */
+ // TODO: Make this take AutoRangeArray instead of retrieving `Selection`
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleIndentAtSelection(const Element& aEditingHost);
+
+ /**
+ * OutdentPartOfBlock() outdents the nodes between aStartOfOutdent and
+ * aEndOfOutdent. This splits the range off from aBlockElement first.
+ * Then, removes the middle element if aIsBlockIndentedWithCSS is false.
+ * Otherwise, decreases the margin of the middle element.
+ *
+ * @param aBlockElement A block element which includes both
+ * aStartOfOutdent and aEndOfOutdent.
+ * @param aStartOfOutdent First node which is descendant of
+ * aBlockElement will be outdented.
+ * @param aEndOfOutdent Last node which is descandant of
+ * aBlockElement will be outdented.
+ * @param aBlockIndentedWith `CSS` if aBlockElement is indented with
+ * CSS margin property.
+ * `HTML` if aBlockElement is `<blockquote>`
+ * or something.
+ * @param aEditingHost The editing host.
+ * @return The left content is new created element
+ * splitting before aStartOfOutdent.
+ * The right content is existing element.
+ * The middle content is outdented element
+ * if aBlockIndentedWith is `CSS`.
+ * Otherwise, nullptr.
+ */
+ enum class BlockIndentedWith { CSS, HTML };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
+ OutdentPartOfBlock(Element& aBlockElement, nsIContent& aStartOfOutdent,
+ nsIContent& aEndOfOutdent,
+ BlockIndentedWith aBlockIndentedWith,
+ const Element& aEditingHost);
+
+ /**
+ * HandleOutdentAtSelectionInternal() outdents contents around Selection.
+ * This method creates AutoSelectionRestorer. Therefore, each caller
+ * needs to check if the editor is still available even if this returns
+ * NS_OK.
+ * NOTE: Call `HandleOutdentAtSelection()` instead.
+ *
+ * @param aEditingHost The editing host.
+ * @return The left content is left content of last
+ * outdented element.
+ * The right content is right content of last
+ * outdented element.
+ * The middle content is middle content of last
+ * outdented element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
+ HandleOutdentAtSelectionInternal(const Element& aEditingHost);
+
+ /**
+ * HandleOutdentAtSelection() outdents contents around Selection.
+ *
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleOutdentAtSelection(const Element& aEditingHost);
+
+ /**
+ * AlignBlockContentsWithDivElement() sets align attribute of <div> element
+ * which is only child of aBlockElement to aAlignType. If aBlockElement
+ * has 2 or more children or does not have a `<div>` element, inserts a
+ * new `<div>` element into aBlockElement and move all children of
+ * aBlockElement into the new `<div>` element.
+ *
+ * @param aBlockElement The element node whose contents should be
+ * aligned to aAlignType. This should be
+ * an element which can have `<div>` element
+ * as its child.
+ * @param aAlignType New value of align attribute of `<div>`
+ * element.
+ * @return A candidate position to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ AlignBlockContentsWithDivElement(Element& aBlockElement,
+ const nsAString& aAlignType);
+
+ /**
+ * AlignContentsInAllTableCellsAndListItems() calls
+ * AlignBlockContentsWithDivElement() for aligning contents in every list
+ * item element and table cell element in aElement.
+ *
+ * @param aElement The node which is or whose descendants should
+ * be aligned to aAlignType.
+ * @param aAlignType New value of `align` attribute of `<div>`.
+ * @return A candidate position to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ AlignContentsInAllTableCellsAndListItems(dom::Element& aElement,
+ const nsAString& aAlignType);
+
+ /**
+ * MakeTransitionList() detects all the transitions in the array, where a
+ * transition means that adjacent nodes in the array don't have the same
+ * parent.
+ */
+ static void MakeTransitionList(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ nsTArray<bool>& aTransitionArray);
+
+ /**
+ * EnsureHardLineBeginsWithFirstChildOf() inserts `<br>` element before
+ * first child of aRemovingContainerElement if it will not be start of a
+ * hard line after removing aRemovingContainerElement.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ EnsureHardLineBeginsWithFirstChildOf(Element& aRemovingContainerElement);
+
+ /**
+ * EnsureHardLineEndsWithLastChildOf() inserts `<br>` element after last
+ * child of aRemovingContainerElement if it will not be end of a hard line
+ * after removing aRemovingContainerElement.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ EnsureHardLineEndsWithLastChildOf(Element& aRemovingContainerElement);
+
+ /**
+ * RemoveAlignFromDescendants() removes align attributes, text-align
+ * properties and <center> elements in aElement.
+ *
+ * @param aElement Alignment information of the node and/or its
+ * descendants will be removed.
+ * NOTE: aElement must not be a `<table>` element.
+ * @param aAlignType New align value to be set only when it's in
+ * CSS mode and this method meets <table> or <hr>.
+ * XXX This is odd and not clear when you see caller of
+ * this method. Do you have better idea?
+ * @param aEditTarget If `OnlyDescendantsExceptTable`, modifies only
+ * descendants of aElement.
+ * If `NodeAndDescendantsExceptTable`, modifies `aElement`
+ * and its descendants.
+ * @return A candidate point to put caret.
+ */
+ enum class EditTarget {
+ OnlyDescendantsExceptTable,
+ NodeAndDescendantsExceptTable
+ };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ RemoveAlignFromDescendants(Element& aElement, const nsAString& aAlignType,
+ EditTarget aEditTarget);
+
+ /**
+ * SetBlockElementAlign() resets `align` attribute, `text-align` property
+ * of descendants of aBlockOrHRElement except `<table>` element descendants.
+ * Then, set `align` attribute or `text-align` property of aBlockOrHRElement.
+ *
+ * @param aBlockOrHRElement The element whose contents will be aligned.
+ * This must be a block element or `<hr>` element.
+ * If we're not in CSS mode, this element has
+ * to support `align` attribute (i.e.,
+ * `HTMLEditUtils::SupportsAlignAttr()` must
+ * return true).
+ * @param aAlignType Boundary or "center" which contents should be
+ * aligned on.
+ * @param aEditTarget If `OnlyDescendantsExceptTable`, modifies only
+ * descendants of aBlockOrHRElement.
+ * If `NodeAndDescendantsExceptTable`, modifies
+ * aBlockOrHRElement and its descendants.
+ * @return A candidate point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ SetBlockElementAlign(Element& aBlockOrHRElement, const nsAString& aAlignType,
+ EditTarget aEditTarget);
+
+ /**
+ * InsertDivElementToAlignContents() inserts a new <div> element (which has
+ * only a padding <br> element) to aPointToInsert for a placeholder whose
+ * contents will be aligned.
+ *
+ * @param aPointToInsert A point to insert new empty <div>.
+ * @param aAlignType New align attribute value where the contents
+ * should be aligned to.
+ * @param aEditingHost The editing host.
+ * @return New <div> element which has only a padding <br>
+ * element and is styled to align contents.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ InsertDivElementToAlignContents(const EditorDOMPoint& aPointToInsert,
+ const nsAString& aAlignType,
+ const Element& aEditingHost);
+
+ /**
+ * AlignNodesAndDescendants() make contents of nodes in aArrayOfContents and
+ * their descendants aligned to aAlignType.
+ *
+ * @param aAlignType New align attribute value where the contents
+ * should be aligned to.
+ * @param aEditingHost The editing host.
+ * @return Last created <div> element which should contain
+ * caret and candidate position which may be
+ * outside the <div> element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ AlignNodesAndDescendants(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ const nsAString& aAlignType, const Element& aEditingHost);
+
+ /**
+ * AlignContentsAtRanges() aligns contents around aRanges to aAlignType.
+ *
+ * @param aRanges The ranges where should be aligned.
+ * @param aAlignType New align attribute value where the contents
+ * should be aligned to.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ AlignContentsAtRanges(AutoRangeArray& aRanges, const nsAString& aAlignType,
+ const Element& aEditingHost);
+
+ /**
+ * AlignAsSubAction() handles "align" command with `Selection`.
+ *
+ * @param aAlignType New align attribute value where the contents
+ * should be aligned to.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ AlignAsSubAction(const nsAString& aAlignType, const Element& aEditingHost);
+
+ /**
+ * AdjustCaretPositionAndEnsurePaddingBRElement() may adjust caret
+ * position to nearest editable content and if padding `<br>` element is
+ * necessary at caret position, this creates it.
+ *
+ * @param aDirectionAndAmount Direction of the edit action.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ AdjustCaretPositionAndEnsurePaddingBRElement(
+ nsIEditor::EDirection aDirectionAndAmount);
+
+ /**
+ * EnsureSelectionInBodyOrDocumentElement() collapse `Selection` to the
+ * primary `<body>` element or document element when `Selection` crosses
+ * `<body>` element's boundary.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ EnsureSelectionInBodyOrDocumentElement();
+
+ /**
+ * InsertBRElementToEmptyListItemsAndTableCellsInRange() inserts
+ * `<br>` element into empty list item or table cell elements between
+ * aStartRef and aEndRef.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ InsertBRElementToEmptyListItemsAndTableCellsInRange(
+ const RawRangeBoundary& aStartRef, const RawRangeBoundary& aEndRef);
+
+ /**
+ * RemoveEmptyNodesIn() removes all empty nodes in aRange. However, if
+ * mail-cite node has only a `<br>` element, the node will be removed
+ * but <br> element is moved to where the mail-cite node was.
+ * XXX This method is expensive if aRange is too wide and may remove
+ * unexpected empty element, e.g., it was created by JS, but we haven't
+ * touched it. Cannot we remove this method and make guarantee that
+ * empty nodes won't be created?
+ *
+ * @param aRange Must be positioned.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ RemoveEmptyNodesIn(const EditorDOMRange& aRange);
+
+ /**
+ * SetSelectionInterlinePosition() may set interline position if caret is
+ * positioned around `<br>` or block boundary. Don't call this when
+ * `Selection` is not collapsed.
+ */
+ void SetSelectionInterlinePosition();
+
+ /**
+ * Called by `HTMLEditor::OnEndHandlingTopLevelEditSubAction()`. This may
+ * adjust Selection, remove unnecessary empty nodes, create `<br>` elements
+ * if needed, etc.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ OnEndHandlingTopLevelEditSubActionInternal();
+
+ /**
+ * MoveSelectedContentsToDivElementToMakeItAbsolutePosition() looks for
+ * a `<div>` element in selection first. If not, creates new `<div>`
+ * element. Then, move all selected contents into the target `<div>`
+ * element.
+ * Note that this creates AutoSelectionRestorer. Therefore, callers need
+ * to check whether we have been destroyed even when this returns NS_OK.
+ *
+ * @param aTargetElement Returns target `<div>` element which should be
+ * changed to absolute positioned.
+ * @param aEditingHost The editing host.
+ */
+ // TODO: Rewrite this with `Result<RefPtr<Element>, nsresult>`.
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ MoveSelectedContentsToDivElementToMakeItAbsolutePosition(
+ RefPtr<Element>* aTargetElement, const Element& aEditingHost);
+
+ /**
+ * SetSelectionToAbsoluteAsSubAction() move selected contents to first
+ * selected `<div>` element or newly created `<div>` element and make
+ * the `<div>` element positioned absolutely.
+ *
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ SetSelectionToAbsoluteAsSubAction(const Element& aEditingHost);
+
+ /**
+ * SetSelectionToStaticAsSubAction() sets the `position` property of a
+ * selection parent's block whose `position` is `absolute` to `static`.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ SetSelectionToStaticAsSubAction();
+
+ /**
+ * AddZIndexAsSubAction() adds aChange to `z-index` of nearest parent
+ * absolute-positioned element from current selection.
+ *
+ * @param aChange Amount to change `z-index`.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ AddZIndexAsSubAction(int32_t aChange);
+
+ /**
+ * OnDocumentModified() is called when editor content is changed.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult OnDocumentModified();
+
+ protected: // Called by helper classes.
+ MOZ_CAN_RUN_SCRIPT void OnStartToHandleTopLevelEditSubAction(
+ EditSubAction aTopLevelEditSubAction,
+ nsIEditor::EDirection aDirectionOfTopLevelEditSubAction,
+ ErrorResult& aRv) final;
+ MOZ_CAN_RUN_SCRIPT nsresult OnEndHandlingTopLevelEditSubAction() final;
+
+ protected: // Shouldn't be used by friend classes
+ virtual ~HTMLEditor();
+
+ /**
+ * InitEditorContentAndSelection() may insert `<br>` elements and padding
+ * `<br>` elements if they are required for `<body>` or document element.
+ * And collapse selection at the end if there is no selection ranges.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InitEditorContentAndSelection();
+
+ /**
+ * Collapse `Selection` to the last leaf content of the <body> or the document
+ * element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ CollapseSelectionToEndOfLastLeafNodeOfDocument() const;
+
+ MOZ_CAN_RUN_SCRIPT nsresult SelectAllInternal() final;
+
+ [[nodiscard]] Element* ComputeEditingHostInternal(
+ const nsIContent* aContent, LimitInBodyElement aLimitInBodyElement) const;
+
+ /**
+ * Creates a range with just the supplied node and appends that to the
+ * selection.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ AppendContentToSelectionAsRange(nsIContent& aContent);
+
+ /**
+ * When you are using AppendContentToSelectionAsRange(), call this first to
+ * start a new selection.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ClearSelection();
+
+ /**
+ * SelectContentInternal() sets Selection to aContentToSelect to
+ * aContentToSelect + 1 in parent of aContentToSelect.
+ *
+ * @param aContentToSelect The content which should be selected.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ SelectContentInternal(nsIContent& aContentToSelect);
+
+ /**
+ * GetInclusiveAncestorByTagNameAtSelection() looks for an element node whose
+ * name matches aTagName from anchor node of Selection to <body> element.
+ *
+ * @param aTagName The tag name which you want to look for.
+ * Must not be nsGkAtoms::_empty.
+ * If nsGkAtoms::list, the result may be <ul>, <ol> or
+ * <dl> element.
+ * If nsGkAtoms::td, the result may be <td> or <th>.
+ * If nsGkAtoms::href, the result may be <a> element
+ * which has "href" attribute with non-empty value.
+ * If nsGkAtoms::anchor, the result may be <a> which
+ * has "name" attribute with non-empty value.
+ * @return If an element which matches aTagName, returns
+ * an Element. Otherwise, nullptr.
+ */
+ Element* GetInclusiveAncestorByTagNameAtSelection(
+ const nsStaticAtom& aTagName) const;
+
+ /**
+ * GetInclusiveAncestorByTagNameInternal() looks for an element node whose
+ * name matches aTagName from aNode to <body> element.
+ *
+ * @param aTagName The tag name which you want to look for.
+ * Must not be nsGkAtoms::_empty.
+ * If nsGkAtoms::list, the result may be <ul>, <ol> or
+ * <dl> element.
+ * If nsGkAtoms::td, the result may be <td> or <th>.
+ * If nsGkAtoms::href, the result may be <a> element
+ * which has "href" attribute with non-empty value.
+ * If nsGkAtoms::anchor, the result may be <a> which
+ * has "name" attribute with non-empty value.
+ * @param aContent Start node to look for the element. This should
+ * not be an orphan node.
+ * @return If an element which matches aTagName, returns
+ * an Element. Otherwise, nullptr.
+ */
+ Element* GetInclusiveAncestorByTagNameInternal(
+ const nsStaticAtom& aTagName, const nsIContent& aContent) const;
+
+ /**
+ * GetSelectedElement() returns a "selected" element node. "selected" means:
+ * - there is only one selection range
+ * - the range starts from an element node or in an element
+ * - the range ends at immediately after same element
+ * - and the range does not include any other element nodes.
+ * Additionally, only when aTagName is nsGkAtoms::href, this thinks that an
+ * <a> element which has non-empty "href" attribute includes the range, the
+ * <a> element is selected.
+ *
+ * NOTE: This method is implementation of nsIHTMLEditor.getSelectedElement()
+ * and comm-central depends on this behavior. Therefore, if you need to use
+ * this method internally but you need to change, perhaps, you should create
+ * another method for avoiding breakage of comm-central apps.
+ *
+ * @param aTagName The atom of tag name in lower case. Set this to
+ * result of EditorUtils::GetTagNameAtom() if you have a
+ * tag name with nsString.
+ * If nullptr, this returns any element node or nullptr.
+ * If nsGkAtoms::href, this returns an <a> element which
+ * has non-empty "href" attribute or nullptr.
+ * If nsGkAtoms::anchor, this returns an <a> element which
+ * has non-empty "name" attribute or nullptr.
+ * Otherwise, returns an element node whose name is
+ * same as aTagName or nullptr.
+ * @param aRv Returns error code.
+ * @return A "selected" element.
+ */
+ already_AddRefed<Element> GetSelectedElement(const nsAtom* aTagName,
+ ErrorResult& aRv);
+
+ /**
+ * GetFirstTableRowElement() returns the first <tr> element in the most
+ * nearest ancestor of aTableOrElementInTable or itself.
+ * When aTableOrElementInTable is neither <table> nor in a <table> element,
+ * returns NS_ERROR_FAILURE. However, if <table> does not have <tr> element,
+ * returns nullptr.
+ *
+ * @param aTableOrElementInTable <table> element or another element.
+ * If this is a <table> element, returns
+ * first <tr> element in it. Otherwise,
+ * returns first <tr> element in nearest
+ * ancestor <table> element.
+ */
+ Result<RefPtr<Element>, nsresult> GetFirstTableRowElement(
+ const Element& aTableOrElementInTable) const;
+
+ /**
+ * GetNextTableRowElement() returns next <tr> element of aTableRowElement.
+ * This won't cross <table> element boundary but may cross table section
+ * elements like <tbody>.
+ * Note that if given element is <tr> but there is no next <tr> element, this
+ * returns nullptr but does not return error.
+ *
+ * @param aTableRowElement A <tr> element.
+ */
+ Result<RefPtr<Element>, nsresult> GetNextTableRowElement(
+ const Element& aTableRowElement) const;
+
+ struct CellData;
+
+ /**
+ * CellIndexes store both row index and column index of a table cell.
+ */
+ struct MOZ_STACK_CLASS CellIndexes final {
+ int32_t mRow;
+ int32_t mColumn;
+
+ /**
+ * This constructor initializes mRowIndex and mColumnIndex with indexes of
+ * aCellElement.
+ *
+ * @param aCellElement An <td> or <th> element.
+ */
+ MOZ_CAN_RUN_SCRIPT CellIndexes(Element& aCellElement, PresShell* aPresShell)
+ : mRow(-1), mColumn(-1) {
+ Update(aCellElement, aPresShell);
+ }
+
+ /**
+ * Update mRowIndex and mColumnIndex with indexes of aCellElement.
+ *
+ * @param See above.
+ */
+ MOZ_CAN_RUN_SCRIPT void Update(Element& aCellElement,
+ PresShell* aPresShell);
+
+ /**
+ * This constructor initializes mRowIndex and mColumnIndex with indexes of
+ * cell element which contains anchor of Selection.
+ *
+ * @param aHTMLEditor The editor which creates the instance.
+ * @param aSelection The Selection for the editor.
+ */
+ MOZ_CAN_RUN_SCRIPT CellIndexes(HTMLEditor& aHTMLEditor,
+ Selection& aSelection)
+ : mRow(-1), mColumn(-1) {
+ Update(aHTMLEditor, aSelection);
+ }
+
+ /**
+ * Update mRowIndex and mColumnIndex with indexes of cell element which
+ * contains anchor of Selection.
+ *
+ * @param See above.
+ */
+ MOZ_CAN_RUN_SCRIPT void Update(HTMLEditor& aHTMLEditor,
+ Selection& aSelection);
+
+ bool operator==(const CellIndexes& aOther) const {
+ return mRow == aOther.mRow && mColumn == aOther.mColumn;
+ }
+ bool operator!=(const CellIndexes& aOther) const {
+ return mRow != aOther.mRow || mColumn != aOther.mColumn;
+ }
+
+ [[nodiscard]] bool isErr() const { return mRow < 0 || mColumn < 0; }
+
+ private:
+ CellIndexes() : mRow(-1), mColumn(-1) {}
+ CellIndexes(int32_t aRowIndex, int32_t aColumnIndex)
+ : mRow(aRowIndex), mColumn(aColumnIndex) {}
+
+ friend struct CellData;
+ };
+
+ struct MOZ_STACK_CLASS CellData final {
+ MOZ_KNOWN_LIVE RefPtr<Element> mElement;
+ // Current indexes which this is initialized with.
+ CellIndexes mCurrent;
+ // First column/row indexes of the cell. When current position is spanned
+ // from other column/row, this value becomes different from mCurrent.
+ CellIndexes mFirst;
+ // Computed rowspan/colspan values which are specified to the cell.
+ // Note that if the cell has larger rowspan/colspan value than actual
+ // table size, these values are the larger values.
+ int32_t mRowSpan = -1;
+ int32_t mColSpan = -1;
+ // Effective rowspan/colspan value at the index. For example, if first
+ // cell element in first row has rowspan="3", then, if this is initialized
+ // with 0-0 indexes, effective rowspan is 3. However, if this is
+ // initialized with 1-0 indexes, effective rowspan is 2.
+ int32_t mEffectiveRowSpan = -1;
+ int32_t mEffectiveColSpan = -1;
+ // mIsSelected is set to true if mElement itself or its parent <tr> or
+ // <table> is selected. Otherwise, e.g., the cell just contains selection
+ // range, this is set to false.
+ bool mIsSelected = false;
+
+ CellData() = delete;
+
+ /**
+ * This returns an instance which is initialized with a <table> element and
+ * both row and column index to specify a cell element.
+ */
+ [[nodiscard]] static CellData AtIndexInTableElement(
+ const HTMLEditor& aHTMLEditor, const Element& aTableElement,
+ int32_t aRowIndex, int32_t aColumnIndex);
+ [[nodiscard]] static CellData AtIndexInTableElement(
+ const HTMLEditor& aHTMLEditor, const Element& aTableElement,
+ const CellIndexes& aIndexes) {
+ MOZ_ASSERT(!aIndexes.isErr());
+ return AtIndexInTableElement(aHTMLEditor, aTableElement, aIndexes.mRow,
+ aIndexes.mColumn);
+ }
+
+ /**
+ * Treated as error if fails to compute current index or first index of the
+ * cell. Note that even if the cell is not found due to no corresponding
+ * frame at current index, it's not an error situation.
+ */
+ [[nodiscard]] bool isOk() const { return !isErr(); }
+ [[nodiscard]] bool isErr() const { return mFirst.isErr(); }
+
+ /**
+ * FailedOrNotFound() returns true if this failed to initialize/update
+ * or succeeded but found no cell element.
+ */
+ [[nodiscard]] bool FailedOrNotFound() const { return isErr() || !mElement; }
+
+ /**
+ * IsSpannedFromOtherRowOrColumn(), IsSpannedFromOtherColumn and
+ * IsSpannedFromOtherRow() return true if there is no cell element
+ * at the index because of spanning from other row and/or column.
+ */
+ [[nodiscard]] bool IsSpannedFromOtherRowOrColumn() const {
+ return mElement && mCurrent != mFirst;
+ }
+ [[nodiscard]] bool IsSpannedFromOtherColumn() const {
+ return mElement && mCurrent.mColumn != mFirst.mColumn;
+ }
+ [[nodiscard]] bool IsSpannedFromOtherRow() const {
+ return mElement && mCurrent.mRow != mFirst.mRow;
+ }
+ [[nodiscard]] bool IsNextColumnSpannedFromOtherColumn() const {
+ return mElement && mCurrent.mColumn + 1 < NextColumnIndex();
+ }
+
+ /**
+ * NextColumnIndex() and NextRowIndex() return column/row index of
+ * next cell. Note that this does not check whether there is next
+ * cell or not actually.
+ */
+ [[nodiscard]] int32_t NextColumnIndex() const {
+ if (NS_WARN_IF(FailedOrNotFound())) {
+ return -1;
+ }
+ return mCurrent.mColumn + mEffectiveColSpan;
+ }
+ [[nodiscard]] int32_t NextRowIndex() const {
+ if (NS_WARN_IF(FailedOrNotFound())) {
+ return -1;
+ }
+ return mCurrent.mRow + mEffectiveRowSpan;
+ }
+
+ /**
+ * LastColumnIndex() and LastRowIndex() return column/row index of
+ * column/row which is spanned by the cell.
+ */
+ [[nodiscard]] int32_t LastColumnIndex() const {
+ if (NS_WARN_IF(FailedOrNotFound())) {
+ return -1;
+ }
+ return NextColumnIndex() - 1;
+ }
+ [[nodiscard]] int32_t LastRowIndex() const {
+ if (NS_WARN_IF(FailedOrNotFound())) {
+ return -1;
+ }
+ return NextRowIndex() - 1;
+ }
+
+ /**
+ * NumberOfPrecedingColmuns() and NumberOfPrecedingRows() return number of
+ * preceding columns/rows if current index is spanned from other column/row.
+ * Otherwise, i.e., current point is not spanned form other column/row,
+ * returns 0.
+ */
+ [[nodiscard]] int32_t NumberOfPrecedingColmuns() const {
+ if (NS_WARN_IF(FailedOrNotFound())) {
+ return -1;
+ }
+ return mCurrent.mColumn - mFirst.mColumn;
+ }
+ [[nodiscard]] int32_t NumberOfPrecedingRows() const {
+ if (NS_WARN_IF(FailedOrNotFound())) {
+ return -1;
+ }
+ return mCurrent.mRow - mFirst.mRow;
+ }
+
+ /**
+ * NumberOfFollowingColumns() and NumberOfFollowingRows() return
+ * number of remaining columns/rows if the cell spans to other
+ * column/row.
+ */
+ [[nodiscard]] int32_t NumberOfFollowingColumns() const {
+ if (NS_WARN_IF(FailedOrNotFound())) {
+ return -1;
+ }
+ return mEffectiveColSpan - 1;
+ }
+ [[nodiscard]] int32_t NumberOfFollowingRows() const {
+ if (NS_WARN_IF(FailedOrNotFound())) {
+ return -1;
+ }
+ return mEffectiveRowSpan - 1;
+ }
+
+ private:
+ explicit CellData(int32_t aCurrentRowIndex, int32_t aCurrentColumnIndex,
+ int32_t aFirstRowIndex, int32_t aFirstColumnIndex)
+ : mCurrent(aCurrentRowIndex, aCurrentColumnIndex),
+ mFirst(aFirstRowIndex, aFirstColumnIndex) {}
+ explicit CellData(Element& aElement, int32_t aRowIndex,
+ int32_t aColumnIndex, nsTableCellFrame& aTableCellFrame,
+ nsTableWrapperFrame& aTableWrapperFrame);
+
+ [[nodiscard]] static CellData Error(int32_t aRowIndex,
+ int32_t aColumnIndex) {
+ return CellData(aRowIndex, aColumnIndex, -1, -1);
+ }
+ [[nodiscard]] static CellData NotFound(int32_t aRowIndex,
+ int32_t aColumnIndex) {
+ return CellData(aRowIndex, aColumnIndex, aRowIndex, aColumnIndex);
+ }
+ };
+
+ /**
+ * TableSize stores and computes number of rows and columns of a <table>
+ * element.
+ */
+ struct MOZ_STACK_CLASS TableSize final {
+ int32_t mRowCount;
+ int32_t mColumnCount;
+
+ TableSize() = delete;
+
+ /**
+ * @param aHTMLEditor The editor which creates the instance.
+ * @param aTableOrElementInTable If a <table> element, computes number
+ * of rows and columns of it.
+ * If another element in a <table> element,
+ * computes number of rows and columns
+ * of nearest ancestor <table> element.
+ * Otherwise, i.e., non-<table> element
+ * not in <table>, returns error.
+ */
+ [[nodiscard]] static Result<TableSize, nsresult> Create(
+ HTMLEditor& aHTMLEditor, Element& aTableOrElementInTable);
+
+ [[nodiscard]] bool IsEmpty() const { return !mRowCount || !mColumnCount; }
+
+ private:
+ TableSize(int32_t aRowCount, int32_t aColumCount)
+ : mRowCount(aRowCount), mColumnCount(aColumCount) {}
+ };
+
+ /**
+ * GetTableCellElementAt() returns a <td> or <th> element of aTableElement
+ * if there is a cell at the indexes.
+ *
+ * @param aTableElement Must be a <table> element.
+ * @param aCellIndexes Indexes of cell which you want.
+ * If rowspan and/or colspan is specified 2 or
+ * larger, any indexes are allowed to retrieve
+ * the cell in the area.
+ * @return The cell element if there is in the <table>.
+ * Returns nullptr without error if the indexes
+ * are out of bounds.
+ */
+ [[nodiscard]] inline Element* GetTableCellElementAt(
+ Element& aTableElement, const CellIndexes& aCellIndexes) const;
+ [[nodiscard]] Element* GetTableCellElementAt(Element& aTableElement,
+ int32_t aRowIndex,
+ int32_t aColumnIndex) const;
+
+ /**
+ * GetSelectedOrParentTableElement() returns <td>, <th>, <tr> or <table>
+ * element:
+ * #1 if the first selection range selects a cell, returns it.
+ * #2 if the first selection range does not select a cell and
+ * the selection anchor refers a <table>, returns it.
+ * #3 if the first selection range does not select a cell and
+ * the selection anchor refers a <tr>, returns it.
+ * #4 if the first selection range does not select a cell and
+ * the selection anchor refers a <td>, returns it.
+ * #5 otherwise, nearest ancestor <td> or <th> element of the
+ * selection anchor if there is.
+ * In #1 and #4, *aIsCellSelected will be set to true (i.e,, when
+ * a selection range selects a cell element).
+ */
+ Result<RefPtr<Element>, nsresult> GetSelectedOrParentTableElement(
+ bool* aIsCellSelected = nullptr) const;
+
+ /**
+ * GetFirstSelectedCellElementInTable() returns <td> or <th> element at
+ * first selection (using GetSelectedOrParentTableElement). If found cell
+ * element is not in <table> or <tr> element, this returns nullptr.
+ */
+ Result<RefPtr<Element>, nsresult> GetFirstSelectedCellElementInTable() const;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandlePaste(
+ AutoEditActionDataSetter& aEditActionData, int32_t aClipboardType) final;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandlePasteAsQuotation(
+ AutoEditActionDataSetter& aEditActionData, int32_t aClipboardType) final;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ HandlePasteTransferable(AutoEditActionDataSetter& aEditActionData,
+ nsITransferable& aTransferable) final;
+
+ /**
+ * PasteInternal() pasts text with replacing selected content.
+ * This tries to dispatch ePaste event first. If its defaultPrevent() is
+ * called, this does nothing but returns NS_OK.
+ *
+ * @param aClipboardType nsIClipboard::kGlobalClipboard or
+ * nsIClipboard::kSelectionClipboard.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult PasteInternal(int32_t aClipboardType);
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ InsertWithQuotationsAsSubAction(const nsAString& aQuotedText) final;
+
+ /**
+ * InsertAsCitedQuotationInternal() inserts a <blockquote> element whose
+ * cite attribute is aCitation and whose content is aQuotedText.
+ * Note that this shouldn't be called when IsPlaintextMailComposer() is true.
+ *
+ * @param aQuotedText HTML source if aInsertHTML is true. Otherwise,
+ * plain text. This is inserted into new <blockquote>
+ * element.
+ * @param aCitation cite attribute value of new <blockquote> element.
+ * @param aInsertHTML true if aQuotedText should be treated as HTML
+ * source.
+ * false if aQuotedText should be treated as plain
+ * text.
+ * @param aNodeInserted [OUT] The new <blockquote> element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertAsCitedQuotationInternal(
+ const nsAString& aQuotedText, const nsAString& aCitation,
+ bool aInsertHTML, nsINode** aNodeInserted);
+
+ /**
+ * InsertNodeIntoProperAncestorWithTransaction() attempts to insert aNode
+ * into the document, at aPointToInsert. Checks with strict dtd to see if
+ * containment is allowed. If not allowed, will attempt to find a parent
+ * in the parent hierarchy of aPointToInsert.GetContainer() that will accept
+ * aNode as a child. If such a parent is found, will split the document
+ * tree from aPointToInsert up to parent, and then insert aNode.
+ * aPointToInsert is then adjusted to point to the actual location that
+ * aNode was inserted at. aSplitAtEdges specifies if the splitting process
+ * is allowed to result in empty nodes.
+ *
+ * @param aContent The content node to insert.
+ * @param aPointToInsert Insertion point.
+ * @param aSplitAtEdges Splitting can result in empty nodes?
+ */
+ template <typename NodeType>
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT
+ Result<CreateNodeResultBase<NodeType>, nsresult>
+ InsertNodeIntoProperAncestorWithTransaction(
+ NodeType& aContent, const EditorDOMPoint& aPointToInsert,
+ SplitAtEdges aSplitAtEdges);
+
+ /**
+ * InsertTextWithQuotationsInternal() replaces selection with new content.
+ * First, this method splits aStringToInsert to multiple chunks which start
+ * with non-linebreaker except first chunk and end with a linebreaker except
+ * last chunk. Then, each chunk starting with ">" is inserted after wrapping
+ * with <span _moz_quote="true">, and each chunk not starting with ">" is
+ * inserted as normal text.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ InsertTextWithQuotationsInternal(const nsAString& aStringToInsert);
+
+ /**
+ * ReplaceContainerWithTransactionInternal() is implementation of
+ * ReplaceContainerWithTransaction() and
+ * ReplaceContainerAndCloneAttributesWithTransaction().
+ *
+ * @param aOldContainer The element which will be replaced with new
+ * element.
+ * @param aTagName The name of new element node.
+ * @param aAttribute Attribute name which will be set to the new
+ * element. This will be ignored if
+ * aCloneAllAttributes is set to true.
+ * @param aAttributeValue Attribute value which will be set to
+ * aAttribute.
+ * @param aCloneAllAttributes If true, all attributes of aOldContainer will
+ * be copied to the new element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ ReplaceContainerWithTransactionInternal(Element& aOldContainer,
+ const nsAtom& aTagName,
+ const nsAtom& aAttribute,
+ const nsAString& aAttributeValue,
+ bool aCloneAllAttributes);
+
+ /**
+ * DeleteSelectionAndCreateElement() creates a element whose name is aTag.
+ * And insert it into the DOM tree after removing the selected content.
+ *
+ * @param aTag The element name to be created.
+ * @param aInitializer A function to initialize the new element before
+ * or after (depends on the pref) connecting the
+ * element into the DOM tree. Note that this should
+ * not touch outside given element because doing it
+ * would break range updater's result.
+ */
+ MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
+ DeleteSelectionAndCreateElement(
+ nsAtom& aTag,
+ const InitializeInsertingElement& aInitializer = DoNothingForNewElement);
+
+ /**
+ * This method first deletes the selection, if it's not collapsed. Then if
+ * the selection lies in a CharacterData node, it splits it. If the
+ * selection is at this point collapsed in a CharacterData node, it's
+ * adjusted to be collapsed right before or after the node instead (which is
+ * always possible, since the node was split).
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult DeleteSelectionAndPrepareToCreateNode();
+
+ /**
+ * PrepareToInsertBRElement() returns a point where new <br> element should
+ * be inserted. If aPointToInsert points middle of a text node, this method
+ * splits the text node and returns the point before right node.
+ *
+ * @param aPointToInsert Candidate point to insert new <br> element.
+ * @return Computed point to insert new <br> element.
+ * If something failed, this return error.
+ */
+ MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult> PrepareToInsertBRElement(
+ const EditorDOMPoint& aPointToInsert);
+
+ /**
+ * IndentAsSubAction() indents the content around Selection.
+ *
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ IndentAsSubAction(const Element& aEditingHost);
+
+ /**
+ * OutdentAsSubAction() outdents the content around Selection.
+ *
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ OutdentAsSubAction(const Element& aEditingHost);
+
+ MOZ_CAN_RUN_SCRIPT nsresult LoadHTML(const nsAString& aInputString);
+
+ /**
+ * UpdateMetaCharsetWithTransaction() scans all <meta> elements in the
+ * document and if and only if there is a <meta> element having `httpEquiv`
+ * attribute and whose value includes `content-type`, updates its `content`
+ * attribute value to aCharacterSet.
+ */
+ MOZ_CAN_RUN_SCRIPT bool UpdateMetaCharsetWithTransaction(
+ Document& aDocument, const nsACString& aCharacterSet);
+
+ /**
+ * SetInlinePropertiesAsSubAction() stores new styles with
+ * mPendingStylesToApplyToNewContent if `Selection` is collapsed. Otherwise,
+ * applying the styles to all selected contents.
+ *
+ * @param aStylesToSet The styles which should be applied to the
+ * selected content.
+ */
+ template <size_t N>
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SetInlinePropertiesAsSubAction(
+ const AutoTArray<EditorInlineStyleAndValue, N>& aStylesToSet);
+
+ /**
+ * SetInlinePropertiesAroundRanges() applying the styles to the ranges even if
+ * the ranges are collapsed.
+ */
+ template <size_t N>
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SetInlinePropertiesAroundRanges(
+ AutoRangeArray& aRanges,
+ const AutoTArray<EditorInlineStyleAndValue, N>& aStylesToSet,
+ const Element& aEditingHost);
+
+ /**
+ * RemoveInlinePropertiesAsSubAction() removes specified styles from
+ * mPendingStylesToApplyToNewContent if `Selection` is collapsed. Otherwise,
+ * removing the style.
+ *
+ * @param aStylesToRemove Styles to remove from the selected contents.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult RemoveInlinePropertiesAsSubAction(
+ const nsTArray<EditorInlineStyle>& aStylesToRemove);
+
+ /**
+ * Helper method to call RemoveInlinePropertiesAsSubAction(). If you want to
+ * remove other elements to remove the style completely, this will append
+ * related elements of aStyleToRemove and aStyleToRemove itself to the array.
+ * E.g., nsGkAtoms::strong and nsGkAtoms::b will be appended if aStyleToRemove
+ * is nsGkAtoms::b.
+ */
+ void AppendInlineStyleAndRelatedStyle(
+ const EditorInlineStyle& aStyleToRemove,
+ nsTArray<EditorInlineStyle>& aStylesToRemove) const;
+
+ /**
+ * ReplaceHeadContentsWithSourceWithTransaction() replaces all children of
+ * <head> element with given source code. This is undoable.
+ *
+ * @param aSourceToInsert HTML source fragment to replace the children
+ * of <head> element.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult ReplaceHeadContentsWithSourceWithTransaction(
+ const nsAString& aSourceToInsert);
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult GetCSSBackgroundColorState(
+ bool* aMixed, nsAString& aOutColor, bool aBlockLevel);
+ nsresult GetHTMLBackgroundColorState(bool* aMixed, nsAString& outColor);
+
+ /**
+ * This sets background on the appropriate container element (table, cell,)
+ * or calls to set the page background.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ SetBlockBackgroundColorWithCSSAsSubAction(const nsAString& aColor);
+ MOZ_CAN_RUN_SCRIPT nsresult
+ SetHTMLBackgroundColorWithTransaction(const nsAString& aColor);
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void InitializeSelectionAncestorLimit(
+ nsIContent& aAncestorLimit) const final;
+
+ /**
+ * Make the given selection span the entire document.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SelectEntireDocument() final;
+
+ /**
+ * Use this to assure that selection is set after attribute nodes when
+ * trying to collapse selection at begining of a block node
+ * e.g., when setting at beginning of a table cell
+ * This will stop at a table, however, since we don't want to
+ * "drill down" into nested tables.
+ */
+ MOZ_CAN_RUN_SCRIPT void CollapseSelectionToDeepestNonTableFirstChild(
+ nsINode* aNode);
+ /**
+ * MaybeCollapseSelectionAtFirstEditableNode() may collapse selection at
+ * proper position to staring to edit. If there is a non-editable node
+ * before any editable text nodes or inline elements which can have text
+ * nodes as their children, collapse selection at start of the editing
+ * host. If there is an editable text node which is not collapsed, collapses
+ * selection at the start of the text node. If there is an editable inline
+ * element which cannot have text nodes as its child, collapses selection at
+ * before the element node. Otherwise, collapses selection at start of the
+ * editing host.
+ *
+ * @param aIgnoreIfSelectionInEditingHost
+ * This method does nothing if selection is in the
+ * editing host except if it's collapsed at start of
+ * the editing host.
+ * Note that if selection ranges were outside of
+ * current selection limiter, selection was collapsed
+ * at the start of the editing host therefore, if
+ * you call this with setting this to true, you can
+ * keep selection ranges if user has already been
+ * changed.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ MaybeCollapseSelectionAtFirstEditableNode(
+ bool aIgnoreIfSelectionInEditingHost) const;
+
+ class BlobReader final {
+ using AutoEditActionDataSetter = EditorBase::AutoEditActionDataSetter;
+
+ public:
+ MOZ_CAN_RUN_SCRIPT BlobReader(dom::BlobImpl* aBlob, HTMLEditor* aHTMLEditor,
+ SafeToInsertData aSafeToInsertData,
+ const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent);
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BlobReader)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(BlobReader)
+
+ MOZ_CAN_RUN_SCRIPT nsresult OnResult(const nsACString& aResult);
+ nsresult OnError(const nsAString& aErrorName);
+
+ private:
+ ~BlobReader() = default;
+
+ RefPtr<dom::BlobImpl> mBlob;
+ RefPtr<HTMLEditor> mHTMLEditor;
+ RefPtr<dom::DataTransfer> mDataTransfer;
+ EditorDOMPoint mPointToInsert;
+ EditAction mEditAction;
+ SafeToInsertData mSafeToInsertData;
+ DeleteSelectedContent mDeleteSelectedContent;
+ bool mNeedsToDispatchBeforeInputEvent;
+ };
+
+ void CreateEventListeners() final;
+ nsresult InstallEventListeners() final;
+
+ bool ShouldReplaceRootElement() const;
+ MOZ_CAN_RUN_SCRIPT void NotifyRootChanged();
+ Element* GetBodyElement() const;
+
+ /**
+ * Get the focused node of this editor.
+ * @return If the editor has focus, this returns the focused node.
+ * Otherwise, returns null.
+ */
+ nsINode* GetFocusedNode() const;
+
+ already_AddRefed<Element> GetInputEventTargetElement() const final;
+
+ /**
+ * Return TRUE if aElement is a table-related elemet and caret was set.
+ */
+ MOZ_CAN_RUN_SCRIPT bool SetCaretInTableCell(dom::Element* aElement);
+
+ /**
+ * HandleTabKeyPressInTable() handles "Tab" key press in table if selection
+ * is in a `<table>` element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleTabKeyPressInTable(WidgetKeyboardEvent* aKeyboardEvent);
+
+ /**
+ * InsertPosition is an enum to indicate where the method should insert to.
+ */
+ enum class InsertPosition {
+ // Before selected cell or a cell containing first selection range.
+ eBeforeSelectedCell,
+ // After selected cell or a cell containing first selection range.
+ eAfterSelectedCell,
+ };
+
+ /**
+ * InsertTableCellsWithTransaction() inserts <td> elements at aPointToInsert.
+ * Note that this simply inserts <td> elements, i.e., colspan and rowspan
+ * around the cell containing selection are not modified. So, for example,
+ * adding a cell to rectangular table changes non-rectangular table.
+ * And if the cell containing selection is at left of row-spanning cell,
+ * it may be moved to right side of the row-spanning cell after inserting
+ * some cell elements before it. Similarly, colspan won't be adjusted
+ * for keeping table rectangle.
+ * Finally, puts caret into previous cell of the insertion point or the
+ * first inserted cell if aPointToInsert is start of the row.
+ *
+ * @param aPointToInsert The place to insert one or more cell
+ * elements. The container must be a
+ * <tr> element.
+ * @param aNumberOfCellsToInsert Number of cells to insert.
+ * @return The first inserted cell element and
+ * start of the last inserted cell element
+ * as a point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ InsertTableCellsWithTransaction(const EditorDOMPoint& aPointToInsert,
+ int32_t aNumberOfCellsToInsert);
+
+ /**
+ * InsertTableColumnsWithTransaction() inserts cell elements to every rows
+ * at same column index as the cell specified by aPointToInsert.
+ *
+ * @param aNumberOfColumnsToInsert Number of columns to insert.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertTableColumnsWithTransaction(
+ const EditorDOMPoint& aPointToInsert, int32_t aNumberOfColumnsToInsert);
+
+ /**
+ * InsertTableRowsWithTransaction() inserts <tr> elements before or after
+ * aCellElement. When aCellElement spans rows and aInsertPosition is
+ * eAfterSelectedCell, new rows will be inserted after the most-bottom row
+ * which contains the cell.
+ *
+ * @param aCellElement The cell element pinting where this will
+ * insert a row before or after.
+ * @param aNumberOfRowsToInsert Number of rows to insert.
+ * @param aInsertPosition Before or after the target cell which
+ * contains first selection range.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult InsertTableRowsWithTransaction(
+ Element& aCellElement, int32_t aNumberOfRowsToInsert,
+ InsertPosition aInsertPosition);
+
+ /**
+ * Insert a new cell after or before supplied aCell.
+ * Optional: If aNewCell supplied, returns the newly-created cell (addref'd,
+ * of course)
+ * This doesn't change or use the current selection.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult InsertCell(Element* aCell, int32_t aRowSpan,
+ int32_t aColSpan, bool aAfter,
+ bool aIsHeader, Element** aNewCell);
+
+ /**
+ * DeleteSelectedTableColumnsWithTransaction() removes cell elements which
+ * belong to same columns of selected cell elements.
+ * If only one cell element is selected or first selection range is
+ * in a cell, removes cell elements which belong to same column.
+ * If 2 or more cell elements are selected, removes cell elements which
+ * belong to any of all selected columns. In this case,
+ * aNumberOfColumnsToDelete is ignored.
+ * If there is no selection ranges, returns error.
+ * If selection is not in a cell element, this does not return error,
+ * just does nothing.
+ * WARNING: This does not remove <col> nor <colgroup> elements.
+ *
+ * @param aNumberOfColumnsToDelete Number of columns to remove. This is
+ * ignored if 2 ore more cells are
+ * selected.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteSelectedTableColumnsWithTransaction(int32_t aNumberOfColumnsToDelete);
+
+ /**
+ * DeleteTableColumnWithTransaction() removes cell elements which belong
+ * to the specified column.
+ * This method adjusts colspan attribute value if cells spanning the
+ * column to delete.
+ * WARNING: This does not remove <col> nor <colgroup> elements.
+ *
+ * @param aTableElement The <table> element which contains the
+ * column which you want to remove.
+ * @param aRowIndex Index of the column which you want to remove.
+ * 0 is the first column.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult DeleteTableColumnWithTransaction(
+ Element& aTableElement, int32_t aColumnIndex);
+
+ /**
+ * DeleteSelectedTableRowsWithTransaction() removes <tr> elements.
+ * If only one cell element is selected or first selection range is
+ * in a cell, removes <tr> elements starting from a <tr> element
+ * containing the selected cell or first selection range.
+ * If 2 or more cell elements are selected, all <tr> elements
+ * which contains selected cell(s). In this case, aNumberOfRowsToDelete
+ * is ignored.
+ * If there is no selection ranges, returns error.
+ * If selection is not in a cell element, this does not return error,
+ * just does nothing.
+ *
+ * @param aNumberOfRowsToDelete Number of rows to remove. This is ignored
+ * if 2 or more cells are selected.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteSelectedTableRowsWithTransaction(int32_t aNumberOfRowsToDelete);
+
+ /**
+ * DeleteTableRowWithTransaction() removes a <tr> element whose index in
+ * the <table> is aRowIndex.
+ * This method adjusts rowspan attribute value if the <tr> element contains
+ * cells which spans rows.
+ *
+ * @param aTableElement The <table> element which contains the
+ * <tr> element which you want to remove.
+ * @param aRowIndex Index of the <tr> element which you want to
+ * remove. 0 is the first row.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteTableRowWithTransaction(Element& aTableElement, int32_t aRowIndex);
+
+ /**
+ * DeleteTableCellWithTransaction() removes table cell elements. If two or
+ * more cell elements are selected, this removes all selected cell elements.
+ * Otherwise, this removes some cell elements starting from selected cell
+ * element or a cell containing first selection range. When this removes
+ * last cell element in <tr> or <table>, this removes the <tr> or the
+ * <table> too. Note that when removing a cell causes number of its row
+ * becomes less than the others, this method does NOT fill the place with
+ * rowspan nor colspan. This does not return error even if selection is not
+ * in cell element, just does nothing.
+ *
+ * @param aNumberOfCellsToDelete Number of cells to remove. This is ignored
+ * if 2 or more cells are selected.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteTableCellWithTransaction(int32_t aNumberOfCellsToDelete);
+
+ /**
+ * DeleteAllChildrenWithTransaction() removes all children of aElement from
+ * the tree.
+ *
+ * @param aElement The element whose children you want to remove.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteAllChildrenWithTransaction(Element& aElement);
+
+ /**
+ * Move all contents from aCellToMerge into aTargetCell (append at end).
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult MergeCells(RefPtr<Element> aTargetCell,
+ RefPtr<Element> aCellToMerge,
+ bool aDeleteCellToMerge);
+
+ /**
+ * DeleteTableElementAndChildren() removes aTableElement (and its children)
+ * from the DOM tree with transaction.
+ *
+ * @param aTableElement The <table> element which you want to remove.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteTableElementAndChildrenWithTransaction(Element& aTableElement);
+
+ MOZ_CAN_RUN_SCRIPT nsresult SetColSpan(Element* aCell, int32_t aColSpan);
+ MOZ_CAN_RUN_SCRIPT nsresult SetRowSpan(Element* aCell, int32_t aRowSpan);
+
+ /**
+ * Helper used to get nsTableWrapperFrame for a table.
+ */
+ static nsTableWrapperFrame* GetTableFrame(const Element* aTable);
+
+ /**
+ * GetNumberOfCellsInRow() returns number of actual cell elements in the row.
+ * If some cells appear by "rowspan" in other rows, they are ignored.
+ *
+ * @param aTableElement The <table> element.
+ * @param aRowIndex Valid row index in aTableElement. This method
+ * counts cell elements in the row.
+ * @return -1 if this meets unexpected error.
+ * Otherwise, number of cells which this method found.
+ */
+ int32_t GetNumberOfCellsInRow(Element& aTableElement, int32_t aRowIndex);
+
+ /**
+ * Test if all cells in row or column at given index are selected.
+ */
+ bool AllCellsInRowSelected(Element* aTable, int32_t aRowIndex,
+ int32_t aNumberOfColumns);
+ bool AllCellsInColumnSelected(Element* aTable, int32_t aColIndex,
+ int32_t aNumberOfRows);
+
+ bool IsEmptyCell(Element* aCell);
+
+ /**
+ * Most insert methods need to get the same basic context data.
+ * Any of the pointers may be null if you don't need that datum (for more
+ * efficiency).
+ * Input: *aCell is a known cell,
+ * if null, cell is obtained from the anchor node of the selection.
+ * Returns NS_EDITOR_ELEMENT_NOT_FOUND if cell is not found even if aCell is
+ * null.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult GetCellContext(Element** aTable, Element** aCell,
+ nsINode** aCellParent,
+ int32_t* aCellOffset,
+ int32_t* aRowIndex,
+ int32_t* aColIndex);
+
+ nsresult GetCellSpansAt(Element* aTable, int32_t aRowIndex, int32_t aColIndex,
+ int32_t& aActualRowSpan, int32_t& aActualColSpan);
+
+ MOZ_CAN_RUN_SCRIPT nsresult SplitCellIntoColumns(
+ Element* aTable, int32_t aRowIndex, int32_t aColIndex,
+ int32_t aColSpanLeft, int32_t aColSpanRight, Element** aNewCell);
+
+ MOZ_CAN_RUN_SCRIPT nsresult SplitCellIntoRows(
+ Element* aTable, int32_t aRowIndex, int32_t aColIndex,
+ int32_t aRowSpanAbove, int32_t aRowSpanBelow, Element** aNewCell);
+
+ MOZ_CAN_RUN_SCRIPT nsresult CopyCellBackgroundColor(Element* aDestCell,
+ Element* aSourceCell);
+
+ /**
+ * Reduce rowspan/colspan when cells span into nonexistent rows/columns.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult FixBadRowSpan(Element* aTable, int32_t aRowIndex,
+ int32_t& aNewRowCount);
+ MOZ_CAN_RUN_SCRIPT nsresult FixBadColSpan(Element* aTable, int32_t aColIndex,
+ int32_t& aNewColCount);
+
+ /**
+ * XXX NormalizeTableInternal() is broken. If it meets a cell which has
+ * bigger or smaller rowspan or colspan than actual number of cells,
+ * this always failed to scan the table. Therefore, this does nothing
+ * when the table should be normalized.
+ *
+ * @param aTableOrElementInTable An element which is in a <table> element
+ * or <table> element itself. Otherwise,
+ * this returns NS_OK but does nothing.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ NormalizeTableInternal(Element& aTableOrElementInTable);
+
+ /**
+ * Fallback method: Call this after using ClearSelection() and you
+ * failed to set selection to some other content in the document.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SetSelectionAtDocumentStart();
+
+ // Methods for handling plaintext quotations
+ MOZ_CAN_RUN_SCRIPT nsresult PasteAsPlaintextQuotation(int32_t aSelectionType);
+
+ /**
+ * Insert a string as quoted text, replacing the selected text (if any).
+ * @param aQuotedText The string to insert.
+ * @param aAddCites Whether to prepend extra ">" to each line
+ * (usually true, unless those characters
+ * have already been added.)
+ * @return aNodeInserted The node spanning the insertion, if applicable.
+ * If aAddCites is false, this will be null.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertAsPlaintextQuotation(
+ const nsAString& aQuotedText, bool aAddCites, nsINode** aNodeInserted);
+
+ /**
+ * InsertObject() inserts given object at aPointToInsert.
+ *
+ * @param aType one of kFileMime, kJPEGImageMime, kJPGImageMime,
+ * kPNGImageMime, kGIFImageMime.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult InsertObject(
+ const nsACString& aType, nsISupports* aObject,
+ SafeToInsertData aSafeToInsertData, const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent);
+
+ class HTMLTransferablePreparer;
+ nsresult PrepareHTMLTransferable(nsITransferable** aTransferable) const;
+
+ enum class HavePrivateHTMLFlavor { No, Yes };
+ MOZ_CAN_RUN_SCRIPT nsresult InsertFromTransferableAtSelection(
+ nsITransferable* aTransferable, const nsAString& aContextStr,
+ const nsAString& aInfoStr, HavePrivateHTMLFlavor aHavePrivateHTMLFlavor);
+
+ /**
+ * InsertFromDataTransfer() is called only when user drops data into
+ * this editor. Don't use this method for other purposes.
+ *
+ * @param aIndex index of aDataTransfer's item to insert.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult InsertFromDataTransfer(
+ const dom::DataTransfer* aDataTransfer, uint32_t aIndex,
+ nsIPrincipal* aSourcePrincipal, const EditorDOMPoint& aDroppedAt,
+ DeleteSelectedContent aDeleteSelectedContent);
+
+ static HavePrivateHTMLFlavor ClipboardHasPrivateHTMLFlavor(
+ nsIClipboard* clipboard);
+
+ /**
+ * CF_HTML:
+ * <https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format>.
+ *
+ * @param[in] aCfhtml a CF_HTML string as defined above.
+ * @param[out] aStuffToPaste the fragment, excluding context.
+ * @param[out] aCfcontext the context, excluding the fragment, including a
+ * marker (`kInsertionCookie`) indicating where the
+ * fragment begins.
+ */
+ nsresult ParseCFHTML(const nsCString& aCfhtml, char16_t** aStuffToPaste,
+ char16_t** aCfcontext);
+
+ /**
+ * AutoHTMLFragmentBoundariesFixer fixes both edges of topmost child contents
+ * which are created with SubtreeContentIterator.
+ */
+ class MOZ_STACK_CLASS AutoHTMLFragmentBoundariesFixer final {
+ public:
+ /**
+ * @param aArrayOfTopMostChildContents
+ * [in/out] The topmost child contents which will be
+ * inserted into the DOM tree. Both edges, i.e.,
+ * first node and last node in this array will be
+ * checked whether they can be inserted into
+ * another DOM tree. If not, it'll replaces some
+ * orphan nodes around nodes with proper parent.
+ */
+ explicit AutoHTMLFragmentBoundariesFixer(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents);
+
+ private:
+ /**
+ * EnsureBeginsOrEndsWithValidContent() replaces some nodes starting from
+ * start or end with proper element node if it's necessary.
+ * If first or last node of aArrayOfTopMostChildContents is in list and/or
+ * `<table>` element, looks for topmost list element or `<table>` element
+ * with `CollectTableAndAnyListElementsOfInclusiveAncestorsAt()` and
+ * `GetMostDistantAncestorListOrTableElement()`. Then, checks
+ * whether some nodes are in aArrayOfTopMostChildContents are the topmost
+ * list/table element or its descendant and if so, removes the nodes from
+ * aArrayOfTopMostChildContents and inserts the list/table element instead.
+ * Then, aArrayOfTopMostChildContents won't start/end with list-item nor
+ * table cells.
+ */
+ enum class StartOrEnd { start, end };
+ void EnsureBeginsOrEndsWithValidContent(
+ StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents)
+ const;
+
+ /**
+ * CollectTableAndAnyListElementsOfInclusiveAncestorsAt() collects list
+ * elements and table related elements from the inclusive ancestors
+ * (https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor) of aNode.
+ */
+ static void CollectTableAndAnyListElementsOfInclusiveAncestorsAt(
+ nsIContent& aContent,
+ nsTArray<OwningNonNull<Element>>& aOutArrayOfListAndTableElements);
+
+ /**
+ * GetMostDistantAncestorListOrTableElement() returns a list or a
+ * `<table>` element which is in
+ * aInclusiveAncestorsTableOrListElements and they are actually
+ * valid ancestor of at least one of aArrayOfTopMostChildContents.
+ */
+ static Element* GetMostDistantAncestorListOrTableElement(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents,
+ const nsTArray<OwningNonNull<Element>>&
+ aInclusiveAncestorsTableOrListElements);
+
+ /**
+ * FindReplaceableTableElement() is a helper method of
+ * EnsureBeginsOrEndsWithValidContent(). If aNodeMaybeInTableElement is
+ * a descendant of aTableElement, returns aNodeMaybeInTableElement or its
+ * nearest ancestor whose tag name is `<td>`, `<th>`, `<tr>`, `<thead>`,
+ * `<tfoot>`, `<tbody>` or `<caption>`.
+ *
+ * @param aTableElement Must be a `<table>` element.
+ * @param aContentMaybeInTableElement A node which may be in aTableElement.
+ */
+ Element* FindReplaceableTableElement(
+ Element& aTableElement, nsIContent& aContentMaybeInTableElement) const;
+
+ /**
+ * IsReplaceableListElement() is a helper method of
+ * EnsureBeginsOrEndsWithValidContent(). If aNodeMaybeInListElement is a
+ * descendant of aListElement, returns true. Otherwise, false.
+ *
+ * @param aListElement Must be a list element.
+ * @param aContentMaybeInListElement A node which may be in aListElement.
+ */
+ bool IsReplaceableListElement(Element& aListElement,
+ nsIContent& aContentMaybeInListElement) const;
+ };
+
+ /**
+ * MakeDefinitionListItemWithTransaction() replaces parent list of current
+ * selection with <dl> or create new <dl> element and creates a definition
+ * list item whose name is aTagName.
+ *
+ * @param aTagName Must be nsGkAtoms::dt or nsGkAtoms::dd.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ MakeDefinitionListItemWithTransaction(nsAtom& aTagName);
+
+ /**
+ * FormatBlockContainerAsSubAction() inserts a block element whose name
+ * is aTagName at selection. If selection is not collapsed and aTagName is
+ * nsGkAtoms::normal or nsGkAtoms::_empty, this removes block containers.
+ *
+ * @param aTagName A block level element name. Must NOT be
+ * nsGkAtoms::dt nor nsGkAtoms::dd.
+ * @param aFormatBlockMode Whether HTML formatBlock command or XUL
+ * paragraphState command.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult FormatBlockContainerAsSubAction(
+ const nsStaticAtom& aTagName, FormatBlockMode aFormatBlockMode);
+
+ /**
+ * Increase/decrease the font size of selection.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ IncrementOrDecrementFontSizeAsSubAction(FontSize aIncrementOrDecrement);
+
+ /**
+ * Wrap aContent in <big> or <small> element and make children of
+ * <font size=n> wrap with <big> or <small> too. Note that if there is
+ * opposite element for aIncrementOrDecrement, their children will be just
+ * unwrapped.
+ *
+ * @param aDir Whether increase or decrease the font size of aContent.
+ * @param aContent The content node whose font size will be changed.
+ * @return A suggest point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ SetFontSizeWithBigOrSmallElement(nsIContent& aContent,
+ FontSize aIncrementOrDecrement);
+
+ /**
+ * Adjust font size of font element children recursively with handling
+ * <big> and <small> elements.
+ *
+ * @param aDir Whether increase or decrease the font size of aContent.
+ * @param aContent The content node whose font size will be changed.
+ * All descendants will be handled recursively.
+ * @return A suggest point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ SetFontSizeOfFontElementChildren(nsIContent& aContent,
+ FontSize aIncrementOrDecrement);
+
+ /**
+ * Get extended range to select element whose all children are selected by
+ * aRange.
+ */
+ EditorRawDOMRange GetExtendedRangeWrappingEntirelySelectedElements(
+ const EditorRawDOMRange& aRange) const;
+
+ /**
+ * Get extended range to select ancestor <a name> elements.
+ */
+ EditorRawDOMRange GetExtendedRangeWrappingNamedAnchor(
+ const EditorRawDOMRange& aRange) const;
+
+ // Declared in HTMLEditorNestedClasses.h and defined in HTMLStyleEditor.cpp
+ class AutoInlineStyleSetter;
+
+ /**
+ * RemoveStyleInside() removes elements which represent aStyleToRemove
+ * and removes CSS style. This handles aElement and all its descendants
+ * (including leaf text nodes) recursively.
+ * TODO: Rename this to explain that this maybe remove aElement from the DOM
+ * tree.
+ *
+ * @param aSpecifiedStyle Whether the class and style attributes should
+ * be preserved or discarded.
+ * @return A suggest point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ RemoveStyleInside(Element& aElement, const EditorInlineStyle& aStyleToRemove,
+ SpecifiedStyle aSpecifiedStyle);
+
+ /**
+ * CollectEditableLeafTextNodes() collects text nodes in aElement.
+ */
+ void CollectEditableLeafTextNodes(
+ Element& aElement, nsTArray<OwningNonNull<Text>>& aLeafTextNodes) const;
+
+ /**
+ * IsRemovableParentStyleWithNewSpanElement() checks whether aStyle of parent
+ * block can be removed from aContent with creating `<span>` element. Note
+ * that this does NOT check whether the specified style comes from parent
+ * block or not.
+ * XXX This may destroy the editor, but using `Result<bool, nsresult>`
+ * is not reasonable because code for accessing the result becomes
+ * messy. However, anybody must forget to check `Destroyed()` after
+ * calling this. Which is the way to smart to make every caller
+ * must check the editor state?
+ */
+ MOZ_CAN_RUN_SCRIPT Result<bool, nsresult>
+ IsRemovableParentStyleWithNewSpanElement(
+ nsIContent& aContent, const EditorInlineStyle& aStyle) const;
+
+ /**
+ * HasStyleOrIdOrClassAttribute() returns true when at least one of
+ * `style`, `id` or `class` attribute value of aElement is not empty.
+ */
+ static bool HasStyleOrIdOrClassAttribute(Element& aElement);
+
+ /**
+ * Whether the outer window of the DOM event target has focus or not.
+ */
+ bool OurWindowHasFocus() const;
+
+ class HTMLWithContextInserter;
+
+ /**
+ * This function is used to insert a string of HTML input optionally with some
+ * context information into the editable field. The HTML input either comes
+ * from a transferable object created as part of a drop/paste operation, or
+ * from the InsertHTML method. We may want the HTML input to be sanitized
+ * (for example, if it's coming from a transferable object), in which case
+ * aTrustedInput should be set to false, otherwise, the caller should set it
+ * to true, which means that the HTML will be inserted in the DOM verbatim.
+ */
+ enum class InlineStylesAtInsertionPoint {
+ Preserve, // If you want the paste to be affected by local style, e.g.,
+ // for the insertHTML command, use "Preserve"
+ Clear, // If you want the paste to be keep its own style, e.g., pasting
+ // from clipboard, use "Clear"
+ };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertHTMLWithContextAsSubAction(
+ const nsAString& aInputString, const nsAString& aContextStr,
+ const nsAString& aInfoStr, const nsAString& aFlavor,
+ SafeToInsertData aSafeToInsertData, const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint aInlineStylesAtInsertionPoint);
+
+ /**
+ * sets the position of an element; warning it does NOT check if the
+ * element is already positioned or not and that's on purpose.
+ * @param aStyledElement [IN] the element
+ * @param aX [IN] the x position in pixels.
+ * @param aY [IN] the y position in pixels.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SetTopAndLeftWithTransaction(
+ nsStyledElement& aStyledElement, int32_t aX, int32_t aY);
+
+ /**
+ * Reset a selected cell or collapsed selection (the caret) after table
+ * editing.
+ *
+ * @param aTable A table in the document.
+ * @param aRow The row ...
+ * @param aCol ... and column defining the cell where we will try to
+ * place the caret.
+ * @param aSelected If true, we select the whole cell instead of setting
+ * caret.
+ * @param aDirection If cell at (aCol, aRow) is not found, search for
+ * previous cell in the same column (aPreviousColumn) or
+ * row (ePreviousRow) or don't search for another cell
+ * (aNoSearch). If no cell is found, caret is place just
+ * before table; and if that fails, at beginning of
+ * document. Thus we generally don't worry about the
+ * return value and can use the
+ * AutoSelectionSetterAfterTableEdit stack-based object to
+ * insure we reset the caret in a table-editing method.
+ */
+ MOZ_CAN_RUN_SCRIPT void SetSelectionAfterTableEdit(Element* aTable,
+ int32_t aRow, int32_t aCol,
+ int32_t aDirection,
+ bool aSelected);
+
+ void RemoveListenerAndDeleteRef(const nsAString& aEvent,
+ nsIDOMEventListener* aListener,
+ bool aUseCapture, ManualNACPtr aElement,
+ PresShell* aPresShell);
+ void DeleteRefToAnonymousNode(ManualNACPtr aContent, PresShell* aPresShell);
+
+ /**
+ * RefreshEditingUI() may refresh editing UIs for current Selection, focus,
+ * etc. If this shows or hides some UIs, it causes reflow. So, this is
+ * not safe method.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult RefreshEditingUI();
+
+ /**
+ * Returns the offset of an element's frame to its absolute containing block.
+ */
+ nsresult GetElementOrigin(Element& aElement, int32_t& aX, int32_t& aY);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult GetPositionAndDimensions(
+ Element& aElement, int32_t& aX, int32_t& aY, int32_t& aW, int32_t& aH,
+ int32_t& aBorderLeft, int32_t& aBorderTop, int32_t& aMarginLeft,
+ int32_t& aMarginTop);
+
+ bool IsInObservedSubtree(nsIContent* aChild);
+
+ void UpdateRootElement();
+
+ /**
+ * SetAllResizersPosition() moves all resizers to proper position.
+ * If the resizers are hidden or replaced with another set of resizers
+ * while this is running, this returns error. So, callers shouldn't
+ * keep handling the resizers if this returns error.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SetAllResizersPosition();
+
+ /**
+ * Shows active resizers around an element's frame
+ * @param aResizedElement [IN] a DOM Element
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult ShowResizersInternal(Element& aResizedElement);
+
+ /**
+ * Hide resizers if they are visible. If this is called while there is no
+ * visible resizers, this does not return error, but does nothing.
+ */
+ nsresult HideResizersInternal();
+
+ /**
+ * RefreshResizersInternal() moves resizers to proper position. This does
+ * nothing if there is no resizing target.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult RefreshResizersInternal();
+
+ ManualNACPtr CreateResizer(int16_t aLocation, nsIContent& aParentContent);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ SetAnonymousElementPositionWithoutTransaction(nsStyledElement& aStyledElement,
+ int32_t aX, int32_t aY);
+
+ ManualNACPtr CreateShadow(nsIContent& aParentContent,
+ Element& aOriginalObject);
+
+ /**
+ * SetShadowPosition() moves the shadow element to proper position.
+ *
+ * @param aShadowElement Must be mResizingShadow or mPositioningShadow.
+ * @param aElement The element which has the shadow.
+ * @param aElementX Left of aElement.
+ * @param aElementY Top of aElement.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ SetShadowPosition(Element& aShadowElement, Element& aElement,
+ int32_t aElementLeft, int32_t aElementTop);
+
+ ManualNACPtr CreateResizingInfo(nsIContent& aParentContent);
+ MOZ_CAN_RUN_SCRIPT nsresult SetResizingInfoPosition(int32_t aX, int32_t aY,
+ int32_t aW, int32_t aH);
+
+ enum class ResizeAt {
+ eX,
+ eY,
+ eWidth,
+ eHeight,
+ };
+ [[nodiscard]] int32_t GetNewResizingIncrement(int32_t aX, int32_t aY,
+ ResizeAt aResizeAt) const;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult StartResizing(Element& aHandle);
+ int32_t GetNewResizingX(int32_t aX, int32_t aY);
+ int32_t GetNewResizingY(int32_t aX, int32_t aY);
+ int32_t GetNewResizingWidth(int32_t aX, int32_t aY);
+ int32_t GetNewResizingHeight(int32_t aX, int32_t aY);
+ void HideShadowAndInfo();
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ SetFinalSizeWithTransaction(int32_t aX, int32_t aY);
+ void SetResizeIncrements(int32_t aX, int32_t aY, int32_t aW, int32_t aH,
+ bool aPreserveRatio);
+
+ /**
+ * HideAnonymousEditingUIs() forcibly hides all editing UIs (resizers,
+ * inline-table-editing UI, absolute positioning UI).
+ */
+ void HideAnonymousEditingUIs();
+
+ /**
+ * HideAnonymousEditingUIsIfUnnecessary() hides all editing UIs if some of
+ * visible UIs are now unnecessary.
+ */
+ void HideAnonymousEditingUIsIfUnnecessary();
+
+ /**
+ * sets the z-index of an element.
+ * @param aElement [IN] the element
+ * @param aZorder [IN] the z-index
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ SetZIndexWithTransaction(nsStyledElement& aElement, int32_t aZIndex);
+
+ /**
+ * shows a grabber attached to an arbitrary element. The grabber is an image
+ * positioned on the left hand side of the top border of the element. Draggin
+ * and dropping it allows to change the element's absolute position in the
+ * document. See chrome://editor/content/images/grabber.gif
+ * @param aElement [IN] the element
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult ShowGrabberInternal(Element& aElement);
+
+ /**
+ * Setting grabber to proper position for current mAbsolutelyPositionedObject.
+ * For example, while an element has grabber, the element may be resized
+ * or repositioned by script or something. Then, you need to reset grabber
+ * position with this.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult RefreshGrabberInternal();
+
+ /**
+ * hide the grabber if it shown.
+ */
+ void HideGrabberInternal();
+
+ /**
+ * CreateGrabberInternal() creates a grabber for moving aParentContent.
+ * This sets mGrabber to the new grabber. If this returns true, it's
+ * always non-nullptr. Otherwise, i.e., the grabber is hidden during
+ * creation, this returns false.
+ */
+ bool CreateGrabberInternal(nsIContent& aParentContent);
+
+ MOZ_CAN_RUN_SCRIPT nsresult StartMoving();
+ MOZ_CAN_RUN_SCRIPT nsresult SetFinalPosition(int32_t aX, int32_t aY);
+ void SnapToGrid(int32_t& newX, int32_t& newY) const;
+ nsresult GrabberClicked();
+ MOZ_CAN_RUN_SCRIPT nsresult EndMoving();
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ GetTemporaryStyleForFocusedPositionedElement(Element& aElement,
+ nsAString& aReturn);
+
+ /**
+ * Shows inline table editing UI around a <table> element which contains
+ * aCellElement. This returns error if creating UI is hidden during this,
+ * or detects another set of UI during this. In such case, callers
+ * shouldn't keep handling anything for the UI.
+ *
+ * @param aCellElement Must be an <td> or <th> element.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ ShowInlineTableEditingUIInternal(Element& aCellElement);
+
+ /**
+ * Hide all inline table editing UI.
+ */
+ void HideInlineTableEditingUIInternal();
+
+ /**
+ * RefreshInlineTableEditingUIInternal() moves inline table editing UI to
+ * proper position. This returns error if the UI is hidden or replaced
+ * during moving.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ RefreshInlineTableEditingUIInternal();
+
+ enum class ContentNodeIs { Inserted, Appended };
+ MOZ_CAN_RUN_SCRIPT void DoContentInserted(nsIContent* aChild,
+ ContentNodeIs aContentNodeIs);
+
+ /**
+ * Returns an anonymous Element of type aTag,
+ * child of aParentContent. If aIsCreatedHidden is true, the class
+ * "hidden" is added to the created element. If aAnonClass is not
+ * the empty string, it becomes the value of the attribute "_moz_anonclass"
+ * @return a Element
+ * @param aTag [IN] desired type of the element to create
+ * @param aParentContent [IN] the parent node of the created anonymous
+ * element
+ * @param aAnonClass [IN] contents of the _moz_anonclass attribute
+ * @param aIsCreatedHidden [IN] a boolean specifying if the class "hidden"
+ * is to be added to the created anonymous
+ * element
+ */
+ ManualNACPtr CreateAnonymousElement(nsAtom* aTag, nsIContent& aParentContent,
+ const nsAString& aAnonClass,
+ bool aIsCreatedHidden);
+
+ /**
+ * Reads a blob into memory and notifies the BlobReader object when the read
+ * operation is finished.
+ *
+ * @param aBlob The input blob
+ * @param aGlobal The global object under which the read should happen.
+ * @param aBlobReader The blob reader object to be notified when finished.
+ */
+ static nsresult SlurpBlob(dom::Blob* aBlob, nsIGlobalObject* aGlobal,
+ BlobReader* aBlobReader);
+
+ /**
+ * OnModifyDocumentInternal() is called by OnModifyDocument().
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult OnModifyDocumentInternal();
+
+ /**
+ * For saving allocation cost in the constructor of
+ * EditorBase::TopLevelEditSubActionData, we should reuse same RangeItem
+ * instance with all top level edit sub actions.
+ * The instance is always cleared when TopLevelEditSubActionData is
+ * destructed and the class is stack only class so that we don't need
+ * to (and also should not) add the RangeItem into the cycle collection.
+ */
+ [[nodiscard]] inline already_AddRefed<RangeItem>
+ GetSelectedRangeItemForTopLevelEditSubAction() const;
+
+ /**
+ * For saving allocation cost in the constructor of
+ * EditorBase::TopLevelEditSubActionData, we should reuse same nsRange
+ * instance with all top level edit sub actions.
+ * The instance is always cleared when TopLevelEditSubActionData is
+ * destructed, but AbstractRange::mOwner keeps grabbing the owner document
+ * so that we need to make it in the cycle collection.
+ */
+ [[nodiscard]] inline already_AddRefed<nsRange>
+ GetChangedRangeForTopLevelEditSubAction() const;
+
+ MOZ_CAN_RUN_SCRIPT void DidDoTransaction(
+ TransactionManager& aTransactionManager, nsITransaction& aTransaction,
+ nsresult aDoTransactionResult) {
+ if (mComposerCommandsUpdater) {
+ RefPtr<ComposerCommandsUpdater> updater(mComposerCommandsUpdater);
+ updater->DidDoTransaction(aTransactionManager);
+ }
+ }
+
+ MOZ_CAN_RUN_SCRIPT void DidUndoTransaction(
+ TransactionManager& aTransactionManager, nsITransaction& aTransaction,
+ nsresult aUndoTransactionResult) {
+ if (mComposerCommandsUpdater) {
+ RefPtr<ComposerCommandsUpdater> updater(mComposerCommandsUpdater);
+ updater->DidUndoTransaction(aTransactionManager);
+ }
+ }
+
+ MOZ_CAN_RUN_SCRIPT void DidRedoTransaction(
+ TransactionManager& aTransactionManager, nsITransaction& aTransaction,
+ nsresult aRedoTransactionResult) {
+ if (mComposerCommandsUpdater) {
+ RefPtr<ComposerCommandsUpdater> updater(mComposerCommandsUpdater);
+ updater->DidRedoTransaction(aTransactionManager);
+ }
+ }
+
+ protected:
+ /**
+ * IndentListChildWithTransaction() is a helper method of
+ * Handle(CSS|HTML)IndentAtSelectionInternal().
+ *
+ * @param aSubListElement [in/out] Specify a sub-list element of the
+ * container of aPointInListElement or nullptr.
+ * When there is no proper sub-list element to
+ * move aContentMovingToSubList, this method
+ * inserts a new sub-list element and update this
+ * to it.
+ * @param aPointInListElement A point in a list element whose child should
+ * be indented. If this method creates new list
+ * element into the list element, this inserts
+ * the new list element to this point.
+ * @param aContentMovingToSubList
+ * A content node which is a child of a list
+ * element and should be moved into a sub-list
+ * element.
+ * @param aEditingHost The editing host.
+ * @return A candidate caret position.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ IndentListChildWithTransaction(RefPtr<Element>* aSubListElement,
+ const EditorDOMPoint& aPointInListElement,
+ nsIContent& aContentMovingToSubList,
+ const Element& aEditingHost);
+
+ /**
+ * Stack based helper class for saving/restoring selection. Note that this
+ * assumes that the nodes involved are still around afterwords!
+ */
+ class AutoSelectionRestorer final {
+ public:
+ AutoSelectionRestorer() = delete;
+ explicit AutoSelectionRestorer(const AutoSelectionRestorer& aOther) =
+ delete;
+ AutoSelectionRestorer(AutoSelectionRestorer&& aOther) = delete;
+
+ /**
+ * Constructor responsible for remembering all state needed to restore
+ * aSelection.
+ * XXX This constructor and the destructor should be marked as
+ * `MOZ_CAN_RUN_SCRIPT`, but it's impossible due to this may be used
+ * with `Maybe`.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY explicit AutoSelectionRestorer(
+ HTMLEditor& aHTMLEditor);
+
+ /**
+ * Destructor restores mSelection to its former state
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY ~AutoSelectionRestorer();
+
+ /**
+ * Abort() cancels to restore the selection.
+ */
+ void Abort();
+
+ bool MaybeRestoreSelectionLater() const { return !!mHTMLEditor; }
+
+ protected:
+ // The lifetime must be guaranteed by the creator of this instance.
+ MOZ_KNOWN_LIVE HTMLEditor* mHTMLEditor = nullptr;
+ };
+
+ /**
+ * Stack based helper class for calling EditorBase::EndTransactionInternal().
+ * NOTE: This does not suppress multiple input events. In most cases,
+ * only one "input" event should be fired for an edit action rather
+ * than per edit sub-action. In such case, you should use
+ * EditorBase::AutoPlaceholderBatch instead.
+ */
+ class MOZ_RAII AutoTransactionBatch final {
+ public:
+ /**
+ * @param aRequesterFuncName function name which wants to end the batch.
+ * This won't be stored nor exposed to selection listeners etc, used only
+ * for logging. This MUST be alive when the destructor runs.
+ */
+ MOZ_CAN_RUN_SCRIPT explicit AutoTransactionBatch(
+ HTMLEditor& aHTMLEditor, const char* aRequesterFuncName)
+ : mHTMLEditor(aHTMLEditor), mRequesterFuncName(aRequesterFuncName) {
+ MOZ_KnownLive(mHTMLEditor).BeginTransactionInternal(mRequesterFuncName);
+ }
+
+ MOZ_CAN_RUN_SCRIPT ~AutoTransactionBatch() {
+ MOZ_KnownLive(mHTMLEditor).EndTransactionInternal(mRequesterFuncName);
+ }
+
+ protected:
+ // The lifetime must be guaranteed by the creator of this instance.
+ MOZ_KNOWN_LIVE HTMLEditor& mHTMLEditor;
+ const char* const mRequesterFuncName;
+ };
+
+ RefPtr<PendingStyles> mPendingStylesToApplyToNewContent;
+ RefPtr<ComposerCommandsUpdater> mComposerCommandsUpdater;
+
+ // Used by TopLevelEditSubActionData::mSelectedRange.
+ mutable RefPtr<RangeItem> mSelectedRangeForTopLevelEditSubAction;
+ // Used by TopLevelEditSubActionData::mChangedRange.
+ mutable RefPtr<nsRange> mChangedRangeForTopLevelEditSubAction;
+
+ RefPtr<Runnable> mPendingRootElementUpdatedRunner;
+ RefPtr<Runnable> mPendingDocumentModifiedRunner;
+
+ // mPaddingBRElementForEmptyEditor should be used for placing caret
+ // at proper position when editor is empty.
+ RefPtr<dom::HTMLBRElement> mPaddingBRElementForEmptyEditor;
+
+ bool mCRInParagraphCreatesParagraph;
+
+ // resizing
+ bool mIsObjectResizingEnabled;
+ bool mIsResizing;
+ bool mPreserveRatio;
+ bool mResizedObjectIsAnImage;
+
+ // absolute positioning
+ bool mIsAbsolutelyPositioningEnabled;
+ bool mResizedObjectIsAbsolutelyPositioned;
+ bool mGrabberClicked;
+ bool mIsMoving;
+
+ bool mSnapToGridEnabled;
+
+ // inline table editing
+ bool mIsInlineTableEditingEnabled;
+
+ bool mIsCSSPrefChecked;
+
+ // resizing
+ ManualNACPtr mTopLeftHandle;
+ ManualNACPtr mTopHandle;
+ ManualNACPtr mTopRightHandle;
+ ManualNACPtr mLeftHandle;
+ ManualNACPtr mRightHandle;
+ ManualNACPtr mBottomLeftHandle;
+ ManualNACPtr mBottomHandle;
+ ManualNACPtr mBottomRightHandle;
+
+ RefPtr<Element> mActivatedHandle;
+
+ ManualNACPtr mResizingShadow;
+ ManualNACPtr mResizingInfo;
+
+ RefPtr<Element> mResizedObject;
+
+ int32_t mOriginalX;
+ int32_t mOriginalY;
+
+ int32_t mResizedObjectX;
+ int32_t mResizedObjectY;
+ int32_t mResizedObjectWidth;
+ int32_t mResizedObjectHeight;
+
+ int32_t mResizedObjectMarginLeft;
+ int32_t mResizedObjectMarginTop;
+ int32_t mResizedObjectBorderLeft;
+ int32_t mResizedObjectBorderTop;
+
+ int32_t mXIncrementFactor;
+ int32_t mYIncrementFactor;
+ int32_t mWidthIncrementFactor;
+ int32_t mHeightIncrementFactor;
+
+ int8_t mInfoXIncrement;
+ int8_t mInfoYIncrement;
+
+ // absolute positioning
+ int32_t mPositionedObjectX;
+ int32_t mPositionedObjectY;
+ int32_t mPositionedObjectWidth;
+ int32_t mPositionedObjectHeight;
+
+ int32_t mPositionedObjectMarginLeft;
+ int32_t mPositionedObjectMarginTop;
+ int32_t mPositionedObjectBorderLeft;
+ int32_t mPositionedObjectBorderTop;
+
+ RefPtr<Element> mAbsolutelyPositionedObject;
+ ManualNACPtr mGrabber;
+ ManualNACPtr mPositioningShadow;
+
+ int32_t mGridSize;
+
+ // inline table editing
+ RefPtr<Element> mInlineEditedCell;
+
+ ManualNACPtr mAddColumnBeforeButton;
+ ManualNACPtr mRemoveColumnButton;
+ ManualNACPtr mAddColumnAfterButton;
+
+ ManualNACPtr mAddRowBeforeButton;
+ ManualNACPtr mRemoveRowButton;
+ ManualNACPtr mAddRowAfterButton;
+
+ void AddMouseClickListener(Element* aElement);
+ void RemoveMouseClickListener(Element* aElement);
+
+ bool mDisabledLinkHandling = false;
+ bool mOldLinkHandlingEnabled = false;
+
+ bool mHasBeforeInputBeenCanceled = false;
+
+ ParagraphSeparator mDefaultParagraphSeparator;
+
+ friend class AlignStateAtSelection; // CollectEditableTargetNodes,
+ // CollectNonEditableNodes
+ friend class AutoRangeArray; // RangeUpdaterRef, SplitNodeWithTransaction,
+ // SplitInlineAncestorsAtRangeBoundaries
+ friend class AutoSelectionSetterAfterTableEdit; // SetSelectionAfterEdit
+ friend class
+ AutoSetTemporaryAncestorLimiter; // InitializeSelectionAncestorLimit
+ friend class CSSEditUtils; // DoTransactionInternal, HasAttributes,
+ // RemoveContainerWithTransaction
+ friend class EditorBase; // ComputeTargetRanges,
+ // GetChangedRangeForTopLevelEditSubAction,
+ // GetSelectedRangeItemForTopLevelEditSubAction,
+ // MaybeCreatePaddingBRElementForEmptyEditor,
+ // PrepareToInsertBRElement,
+ // ReflectPaddingBRElementForEmptyEditor,
+ // RefreshEditingUI,
+ // RemoveEmptyInclusiveAncestorInlineElements,
+ // mComposerUpdater, mHasBeforeInputBeenCanceled
+ friend class JoinNodesTransaction; // DidJoinNodesTransaction, DoJoinNodes,
+ // DoSplitNode, // RangeUpdaterRef
+ friend class ListElementSelectionState; // CollectEditTargetNodes,
+ // CollectNonEditableNodes
+ friend class ListItemElementSelectionState; // CollectEditTargetNodes,
+ // CollectNonEditableNodes
+ friend class MoveNodeTransaction; // AllowsTransactionsToChangeSelection,
+ // CollapseSelectionTo, MarkElementDirty,
+ // RangeUpdaterRef
+ friend class ParagraphStateAtSelection; // CollectChildren,
+ // CollectEditTargetNodes,
+ // CollectListChildren,
+ // CollectNonEditableNodes,
+ // CollectTableChildren
+ friend class SlurpBlobEventListener; // BlobReader
+ friend class SplitNodeTransaction; // DoJoinNodes, DoSplitNode
+ friend class TransactionManager; // DidDoTransaction, DidRedoTransaction,
+ // DidUndoTransaction
+ friend class
+ WhiteSpaceVisibilityKeeper; // AutoMoveOneLineHandler
+ // CanMoveChildren,
+ // ChangeListElementType,
+ // DeleteNodeWithTransaction,
+ // DeleteTextAndTextNodesWithTransaction,
+ // JoinNearestEditableNodesWithTransaction,
+ // MoveChildrenWithTransaction,
+ // SplitAncestorStyledInlineElementsAt,
+ // TreatEmptyTextNodes
+};
+
+/**
+ * ListElementSelectionState class gets which list element is selected right
+ * now.
+ */
+class MOZ_STACK_CLASS ListElementSelectionState final {
+ public:
+ ListElementSelectionState() = delete;
+ ListElementSelectionState(HTMLEditor& aHTMLEditor, ErrorResult& aRv);
+
+ bool IsOLElementSelected() const { return mIsOLElementSelected; }
+ bool IsULElementSelected() const { return mIsULElementSelected; }
+ bool IsDLElementSelected() const { return mIsDLElementSelected; }
+ bool IsNotOneTypeListElementSelected() const {
+ return (mIsOLElementSelected + mIsULElementSelected + mIsDLElementSelected +
+ mIsOtherContentSelected) > 1;
+ }
+
+ private:
+ bool mIsOLElementSelected = false;
+ bool mIsULElementSelected = false;
+ bool mIsDLElementSelected = false;
+ bool mIsOtherContentSelected = false;
+};
+
+/**
+ * ListItemElementSelectionState class gets which list item element is selected
+ * right now.
+ */
+class MOZ_STACK_CLASS ListItemElementSelectionState final {
+ public:
+ ListItemElementSelectionState() = delete;
+ ListItemElementSelectionState(HTMLEditor& aHTMLEditor, ErrorResult& aRv);
+
+ bool IsLIElementSelected() const { return mIsLIElementSelected; }
+ bool IsDTElementSelected() const { return mIsDTElementSelected; }
+ bool IsDDElementSelected() const { return mIsDDElementSelected; }
+ bool IsNotOneTypeDefinitionListItemElementSelected() const {
+ return (mIsDTElementSelected + mIsDDElementSelected +
+ mIsOtherElementSelected) > 1;
+ }
+
+ private:
+ bool mIsLIElementSelected = false;
+ bool mIsDTElementSelected = false;
+ bool mIsDDElementSelected = false;
+ bool mIsOtherElementSelected = false;
+};
+
+/**
+ * AlignStateAtSelection class gets alignment at selection.
+ * XXX This currently returns only first alignment.
+ */
+class MOZ_STACK_CLASS AlignStateAtSelection final {
+ public:
+ AlignStateAtSelection() = delete;
+ MOZ_CAN_RUN_SCRIPT AlignStateAtSelection(HTMLEditor& aHTMLEditor,
+ ErrorResult& aRv);
+
+ nsIHTMLEditor::EAlignment AlignmentAtSelectionStart() const {
+ return mFirstAlign;
+ }
+ bool IsSelectionRangesFound() const { return mFoundSelectionRanges; }
+
+ private:
+ nsIHTMLEditor::EAlignment mFirstAlign = nsIHTMLEditor::eLeft;
+ bool mFoundSelectionRanges = false;
+};
+
+/**
+ * ParagraphStateAtSelection class gets format block types around selection.
+ */
+class MOZ_STACK_CLASS ParagraphStateAtSelection final {
+ public:
+ using FormatBlockMode = HTMLEditor::FormatBlockMode;
+
+ ParagraphStateAtSelection() = delete;
+ /**
+ * @param aFormatBlockMode Whether HTML formatBlock command or XUL
+ * paragraphState command.
+ */
+ ParagraphStateAtSelection(HTMLEditor& aHTMLEditor,
+ FormatBlockMode aFormatBlockMode, ErrorResult& aRv);
+
+ /**
+ * GetFirstParagraphStateAtSelection() returns:
+ * - nullptr if there is no format blocks nor inline nodes.
+ * - nsGkAtoms::_empty if first node is not in any format block.
+ * - a tag name of format block at first node.
+ * XXX See the private method explanations. If selection ranges contains
+ * non-format block first, it'll be check after its siblings. Therefore,
+ * this may return non-first paragraph state.
+ */
+ nsAtom* GetFirstParagraphStateAtSelection() const {
+ return mIsMixed && mIsInDLElement ? nsGkAtoms::dl
+ : mFirstParagraphState.get();
+ }
+
+ /**
+ * If selected nodes are not in same format node nor only in no-format blocks,
+ * this returns true.
+ */
+ bool IsMixed() const { return mIsMixed && !mIsInDLElement; }
+
+ private:
+ using EditorType = EditorBase::EditorType;
+
+ [[nodiscard]] static bool IsFormatElement(FormatBlockMode aFormatBlockMode,
+ const nsIContent& aContent);
+
+ /**
+ * AppendDescendantFormatNodesAndFirstInlineNode() appends descendant
+ * format blocks and first inline child node in aNonFormatBlockElement to
+ * the last of the array (not inserting where aNonFormatBlockElement is,
+ * so that the node order becomes randomly).
+ *
+ * @param aArrayOfContents [in/out] Found descendant format blocks
+ * and first inline node in each non-format
+ * block will be appended to this.
+ * @param aFormatBlockMode Whether HTML formatBlock command or XUL
+ * paragraphState command.
+ * @param aNonFormatBlockElement Must be a non-format block element.
+ */
+ static void AppendDescendantFormatNodesAndFirstInlineNode(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ FormatBlockMode aFormatBlockMode, dom::Element& aNonFormatBlockElement);
+
+ /**
+ * CollectEditableFormatNodesInSelection() collects only editable nodes
+ * around selection ranges (with `AutoRangeArray::ExtendRangesToWrapLines()`
+ * and `HTMLEditor::CollectEditTargetNodes()`, see its document for the
+ * detail). If it includes list, list item or table related elements, they
+ * will be replaced their children.
+ *
+ * @param aFormatBlockMode Whether HTML formatBlock command or XUL
+ * paragraphState command.
+ */
+ static nsresult CollectEditableFormatNodesInSelection(
+ HTMLEditor& aHTMLEditor, FormatBlockMode aFormatBlockMode,
+ const dom::Element& aEditingHost,
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents);
+
+ RefPtr<nsAtom> mFirstParagraphState;
+ bool mIsInDLElement = false;
+ bool mIsMixed = false;
+};
+
+} // namespace mozilla
+
+mozilla::HTMLEditor* nsIEditor::AsHTMLEditor() {
+ MOZ_DIAGNOSTIC_ASSERT(IsHTMLEditor());
+ return static_cast<mozilla::HTMLEditor*>(this);
+}
+
+const mozilla::HTMLEditor* nsIEditor::AsHTMLEditor() const {
+ MOZ_DIAGNOSTIC_ASSERT(IsHTMLEditor());
+ return static_cast<const mozilla::HTMLEditor*>(this);
+}
+
+mozilla::HTMLEditor* nsIEditor::GetAsHTMLEditor() {
+ return AsEditorBase()->IsHTMLEditor() ? AsHTMLEditor() : nullptr;
+}
+
+const mozilla::HTMLEditor* nsIEditor::GetAsHTMLEditor() const {
+ return AsEditorBase()->IsHTMLEditor() ? AsHTMLEditor() : nullptr;
+}
+
+#endif // #ifndef mozilla_HTMLEditor_h
diff --git a/editor/libeditor/HTMLEditorCommands.cpp b/editor/libeditor/HTMLEditorCommands.cpp
new file mode 100644
index 0000000000..1864299a3a
--- /dev/null
+++ b/editor/libeditor/HTMLEditorCommands.cpp
@@ -0,0 +1,1349 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EditorCommands.h"
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/EditorBase.h" // for EditorBase
+#include "mozilla/ErrorResult.h"
+#include "mozilla/HTMLEditor.h" // for HTMLEditor
+#include "mozilla/dom/Element.h"
+#include "nsAString.h"
+#include "nsAtom.h" // for nsAtom, nsStaticAtom, etc
+#include "nsCommandParams.h" // for nsCommandParams, etc
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::font, etc
+#include "nsIClipboard.h" // for nsIClipboard, etc
+#include "nsIEditingSession.h"
+#include "nsIPrincipal.h" // for nsIPrincipal
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsReadableUtils.h" // for EmptyString
+#include "nsString.h" // for nsAutoString, nsString, etc
+#include "nsStringFwd.h" // for nsString
+
+class nsISupports;
+
+namespace mozilla {
+using dom::Element;
+
+// prototype
+static nsresult GetListState(HTMLEditor* aHTMLEditor, bool* aMixed,
+ nsAString& aLocalName);
+
+// defines
+#define STATE_ENABLED "state_enabled"
+#define STATE_ALL "state_all"
+#define STATE_ANY "state_any"
+#define STATE_MIXED "state_mixed"
+#define STATE_BEGIN "state_begin"
+#define STATE_END "state_end"
+#define STATE_ATTRIBUTE "state_attribute"
+#define STATE_DATA "state_data"
+
+/*****************************************************************************
+ * mozilla::StateUpdatingCommandBase
+ *****************************************************************************/
+
+bool StateUpdatingCommandBase::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ if (!htmlEditor->IsSelectionEditable()) {
+ return false;
+ }
+ if (aCommand == Command::FormatAbsolutePosition) {
+ return htmlEditor->IsAbsolutePositionEditorEnabled();
+ }
+ return true;
+}
+
+nsresult StateUpdatingCommandBase::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsStaticAtom* tagName = GetTagName(aCommand);
+ if (NS_WARN_IF(!tagName)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsresult rv = ToggleState(MOZ_KnownLive(*tagName), MOZ_KnownLive(*htmlEditor),
+ aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "StateUpdatingCommandBase::ToggleState() failed");
+ return rv;
+}
+
+nsresult StateUpdatingCommandBase::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ if (!aEditorBase) {
+ return NS_OK;
+ }
+ HTMLEditor* htmlEditor = aEditorBase->GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsStaticAtom* tagName = GetTagName(aCommand);
+ if (NS_WARN_IF(!tagName)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ // MOZ_KnownLive(htmlEditor) because the lifetime of aEditorBase is guaranteed
+ // by the callers.
+ // MOZ_KnownLive(tagName) because nsStaticAtom instances are alive until
+ // shutting down.
+ nsresult rv = GetCurrentState(MOZ_KnownLive(*tagName),
+ MOZ_KnownLive(*htmlEditor), aParams);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "StateUpdatingCommandBase::GetCurrentState() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::PasteNoFormattingCommand
+ *****************************************************************************/
+
+StaticRefPtr<PasteNoFormattingCommand> PasteNoFormattingCommand::sInstance;
+
+bool PasteNoFormattingCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ return htmlEditor->CanPaste(nsIClipboard::kGlobalClipboard);
+}
+
+nsresult PasteNoFormattingCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+ // Known live because we hold a ref above in "editor"
+ nsresult rv = MOZ_KnownLive(htmlEditor)
+ ->PasteNoFormattingAsAction(
+ nsIClipboard::kGlobalClipboard,
+ EditorBase::DispatchPasteEvent::Yes, aPrincipal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PasteNoFormattingAsAction(DispatchPasteEvent::Yes) failed");
+ return rv;
+}
+
+nsresult PasteNoFormattingCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::StyleUpdatingCommand
+ *****************************************************************************/
+
+StaticRefPtr<StyleUpdatingCommand> StyleUpdatingCommand::sInstance;
+
+nsresult StyleUpdatingCommand::GetCurrentState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsCommandParams& aParams) const {
+ bool firstOfSelectionHasProp = false;
+ bool anyOfSelectionHasProp = false;
+ bool allOfSelectionHasProp = false;
+
+ nsresult rv = aHTMLEditor.GetInlineProperty(
+ aTagName, nullptr, u""_ns, &firstOfSelectionHasProp,
+ &anyOfSelectionHasProp, &allOfSelectionHasProp);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetInlineProperty() failed");
+
+ aParams.SetBool(STATE_ENABLED, NS_SUCCEEDED(rv));
+ aParams.SetBool(STATE_ALL, allOfSelectionHasProp);
+ aParams.SetBool(STATE_ANY, anyOfSelectionHasProp);
+ aParams.SetBool(STATE_MIXED, anyOfSelectionHasProp && !allOfSelectionHasProp);
+ aParams.SetBool(STATE_BEGIN, firstOfSelectionHasProp);
+ aParams.SetBool(STATE_END, allOfSelectionHasProp); // not completely accurate
+ return NS_OK;
+}
+
+nsresult StyleUpdatingCommand::ToggleState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsIPrincipal* aPrincipal) const {
+ RefPtr<nsCommandParams> params = new nsCommandParams();
+
+ // tags "href" and "name" are special cases in the core editor
+ // they are used to remove named anchor/link and shouldn't be used for
+ // insertion
+ bool doTagRemoval;
+ if (&aTagName == nsGkAtoms::href || &aTagName == nsGkAtoms::name) {
+ doTagRemoval = true;
+ } else {
+ // check current selection; set doTagRemoval if formatting should be removed
+ nsresult rv = GetCurrentState(aTagName, aHTMLEditor, *params);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("StyleUpdatingCommand::GetCurrentState() failed");
+ return rv;
+ }
+ ErrorResult error;
+ doTagRemoval = params->GetBool(STATE_ALL, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+ }
+
+ if (doTagRemoval) {
+ nsresult rv =
+ aHTMLEditor.RemoveInlinePropertyAsAction(aTagName, nullptr, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveInlinePropertyAsAction() failed");
+ return rv;
+ }
+
+ nsresult rv = aHTMLEditor.SetInlinePropertyAsAction(aTagName, nullptr, u""_ns,
+ aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetInlinePropertyAsAction() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::ListCommand
+ *****************************************************************************/
+
+StaticRefPtr<ListCommand> ListCommand::sInstance;
+
+nsresult ListCommand::GetCurrentState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsCommandParams& aParams) const {
+ bool bMixed;
+ nsAutoString localName;
+ nsresult rv = GetListState(&aHTMLEditor, &bMixed, localName);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("GetListState() failed");
+ return rv;
+ }
+
+ bool inList = aTagName.Equals(localName);
+ aParams.SetBool(STATE_ALL, !bMixed && inList);
+ aParams.SetBool(STATE_MIXED, bMixed);
+ aParams.SetBool(STATE_ENABLED, true);
+ return NS_OK;
+}
+
+nsresult ListCommand::ToggleState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsIPrincipal* aPrincipal) const {
+ RefPtr<nsCommandParams> params = new nsCommandParams();
+ nsresult rv = GetCurrentState(aTagName, aHTMLEditor, *params);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("ListCommand::GetCurrentState() failed");
+ return rv;
+ }
+
+ ErrorResult error;
+ bool inList = params->GetBool(STATE_ALL, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+
+ nsDependentAtomString listType(&aTagName);
+ if (inList) {
+ nsresult rv = aHTMLEditor.RemoveListAsAction(listType, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveListAsAction() failed");
+ return rv;
+ }
+
+ rv = aHTMLEditor.MakeOrChangeListAsAction(
+ aTagName, u""_ns, HTMLEditor::SelectAllOfCurrentList::No, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::MakeOrChangeListAsAction() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::ListItemCommand
+ *****************************************************************************/
+
+StaticRefPtr<ListItemCommand> ListItemCommand::sInstance;
+
+nsresult ListItemCommand::GetCurrentState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsCommandParams& aParams) const {
+ ErrorResult error;
+ ListItemElementSelectionState state(aHTMLEditor, error);
+ if (error.Failed()) {
+ NS_WARNING("ListItemElementSelectionState failed");
+ return error.StealNSResult();
+ }
+
+ if (state.IsNotOneTypeDefinitionListItemElementSelected()) {
+ aParams.SetBool(STATE_ALL, false);
+ aParams.SetBool(STATE_MIXED, true);
+ return NS_OK;
+ }
+
+ nsStaticAtom* selectedListItemTagName = nullptr;
+ if (state.IsLIElementSelected()) {
+ selectedListItemTagName = nsGkAtoms::li;
+ } else if (state.IsDTElementSelected()) {
+ selectedListItemTagName = nsGkAtoms::dt;
+ } else if (state.IsDDElementSelected()) {
+ selectedListItemTagName = nsGkAtoms::dd;
+ }
+ aParams.SetBool(STATE_ALL, &aTagName == selectedListItemTagName);
+ aParams.SetBool(STATE_MIXED, false);
+ return NS_OK;
+}
+
+nsresult ListItemCommand::ToggleState(nsStaticAtom& aTagName,
+ HTMLEditor& aHTMLEditor,
+ nsIPrincipal* aPrincipal) const {
+ // Need to use aTagName????
+ RefPtr<nsCommandParams> params = new nsCommandParams();
+ GetCurrentState(aTagName, aHTMLEditor, *params);
+ ErrorResult error;
+ bool inList = params->GetBool(STATE_ALL, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+
+ if (inList) {
+ // To remove a list, first get what kind of list we're in
+ bool bMixed;
+ nsAutoString localName;
+ nsresult rv = GetListState(&aHTMLEditor, &bMixed, localName);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("GetListState() failed");
+ return rv;
+ }
+ if (localName.IsEmpty() || bMixed) {
+ return NS_OK;
+ }
+ rv = aHTMLEditor.RemoveListAsAction(localName, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveListAsAction() failed");
+ return rv;
+ }
+
+ // Set to the requested paragraph type
+ // XXX Note: This actually doesn't work for "LI",
+ // but we currently don't use this for non DL lists anyway.
+ // Problem: won't this replace any current block paragraph style?
+ nsresult rv = aHTMLEditor.SetParagraphStateAsAction(
+ nsDependentAtomString(&aTagName), aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetParagraphFormatAsAction() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::RemoveListCommand
+ *****************************************************************************/
+
+StaticRefPtr<RemoveListCommand> RemoveListCommand::sInstance;
+
+bool RemoveListCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+
+ if (!htmlEditor->IsSelectionEditable()) {
+ return false;
+ }
+
+ // It is enabled if we are in any list type
+ bool bMixed;
+ nsAutoString localName;
+ nsresult rv = GetListState(MOZ_KnownLive(htmlEditor), &bMixed, localName);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "GetListState() failed");
+ return NS_SUCCEEDED(rv) && (bMixed || !localName.IsEmpty());
+}
+
+nsresult RemoveListCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_OK;
+ }
+ // This removes any list type
+ nsresult rv =
+ MOZ_KnownLive(htmlEditor)->RemoveListAsAction(u""_ns, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveListAsAction() failed");
+ return rv;
+}
+
+nsresult RemoveListCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::IndentCommand
+ *****************************************************************************/
+
+StaticRefPtr<IndentCommand> IndentCommand::sInstance;
+
+bool IndentCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ return htmlEditor->IsSelectionEditable();
+}
+
+nsresult IndentCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_OK;
+ }
+ nsresult rv = MOZ_KnownLive(htmlEditor)->IndentAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::IndentAsAction() failed");
+ return rv;
+}
+
+nsresult IndentCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::OutdentCommand
+ *****************************************************************************/
+
+StaticRefPtr<OutdentCommand> OutdentCommand::sInstance;
+
+bool OutdentCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ return htmlEditor->IsSelectionEditable();
+}
+
+nsresult OutdentCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_OK;
+ }
+ nsresult rv = MOZ_KnownLive(htmlEditor)->OutdentAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::OutdentAsAction() failed");
+ return rv;
+}
+
+nsresult OutdentCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::MultiStateCommandBase
+ *****************************************************************************/
+
+bool MultiStateCommandBase::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ // should be disabled sometimes, like if the current selection is an image
+ return htmlEditor->IsSelectionEditable();
+}
+
+nsresult MultiStateCommandBase::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ NS_WARNING(
+ "who is calling MultiStateCommandBase::DoCommand (no implementation)?");
+ return NS_OK;
+}
+
+nsresult MultiStateCommandBase::DoCommandParam(Command aCommand,
+ const nsAString& aStringParam,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ NS_WARNING_ASSERTION(aCommand != Command::FormatJustify,
+ "Command::FormatJustify should be used only for "
+ "IsCommandEnabled() and GetCommandStateParams()");
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = SetState(MOZ_KnownLive(htmlEditor), aStringParam, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "MultiStateCommandBase::SetState() failed");
+ return rv;
+}
+
+nsresult MultiStateCommandBase::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ if (!aEditorBase) {
+ return NS_OK;
+ }
+ HTMLEditor* htmlEditor = aEditorBase->GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = GetCurrentState(MOZ_KnownLive(htmlEditor), aParams);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "MultiStateCommandBase::GetCurrentState() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::FormatBlockStateCommand
+ *****************************************************************************/
+
+StaticRefPtr<FormatBlockStateCommand> FormatBlockStateCommand::sInstance;
+
+nsresult FormatBlockStateCommand::GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ErrorResult error;
+ ParagraphStateAtSelection state(
+ *aHTMLEditor,
+ ParagraphStateAtSelection::FormatBlockMode::HTMLFormatBlockCommand,
+ error);
+ if (error.Failed()) {
+ NS_WARNING("ParagraphStateAtSelection failed");
+ return error.StealNSResult();
+ }
+ aParams.SetBool(STATE_MIXED, state.IsMixed());
+ if (NS_WARN_IF(!state.GetFirstParagraphStateAtSelection())) {
+ aParams.SetCString(STATE_ATTRIBUTE, ""_ns);
+ } else {
+ nsCString paragraphState; // Don't use `nsAutoCString` for avoiding copy.
+ state.GetFirstParagraphStateAtSelection()->ToUTF8String(paragraphState);
+ aParams.SetCString(STATE_ATTRIBUTE, paragraphState);
+ }
+ return NS_OK;
+}
+
+nsresult FormatBlockStateCommand::SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(!aHTMLEditor) || NS_WARN_IF(aNewState.IsEmpty())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv = aHTMLEditor->FormatBlockAsAction(aNewState, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::FormatBlockAsAction() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::ParagraphStateCommand
+ *****************************************************************************/
+
+StaticRefPtr<ParagraphStateCommand> ParagraphStateCommand::sInstance;
+
+nsresult ParagraphStateCommand::GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ErrorResult error;
+ ParagraphStateAtSelection state(
+ *aHTMLEditor,
+ ParagraphStateAtSelection::FormatBlockMode::XULParagraphStateCommand,
+ error);
+ if (error.Failed()) {
+ NS_WARNING("ParagraphStateAtSelection failed");
+ return error.StealNSResult();
+ }
+ aParams.SetBool(STATE_MIXED, state.IsMixed());
+ if (NS_WARN_IF(!state.GetFirstParagraphStateAtSelection())) {
+ // XXX This is odd behavior, we should fix this later.
+ aParams.SetCString(STATE_ATTRIBUTE, "x"_ns);
+ } else {
+ nsCString paragraphState; // Don't use `nsAutoCString` for avoiding copy.
+ state.GetFirstParagraphStateAtSelection()->ToUTF8String(paragraphState);
+ aParams.SetCString(STATE_ATTRIBUTE, paragraphState);
+ }
+ return NS_OK;
+}
+
+nsresult ParagraphStateCommand::SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv = aHTMLEditor->SetParagraphStateAsAction(aNewState, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetParagraphStateAsAction() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::FontFaceStateCommand
+ *****************************************************************************/
+
+StaticRefPtr<FontFaceStateCommand> FontFaceStateCommand::sInstance;
+
+nsresult FontFaceStateCommand::GetCurrentState(HTMLEditor* aHTMLEditor,
+ nsCommandParams& aParams) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoString outStateString;
+ bool outMixed;
+ nsresult rv = aHTMLEditor->GetFontFaceState(&outMixed, outStateString);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetFontFaceState() failed");
+ return rv;
+ }
+ aParams.SetBool(STATE_MIXED, outMixed);
+ aParams.SetCString(STATE_ATTRIBUTE, NS_ConvertUTF16toUTF8(outStateString));
+ return NS_OK;
+}
+
+nsresult FontFaceStateCommand::SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (aNewState.IsEmpty() || aNewState.EqualsLiteral("normal")) {
+ nsresult rv = aHTMLEditor->RemoveInlinePropertyAsAction(
+ *nsGkAtoms::font, nsGkAtoms::face, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveInlinePropertyAsAction(nsGkAtoms::"
+ "font, nsGkAtoms::face) failed");
+ return rv;
+ }
+
+ nsresult rv = aHTMLEditor->SetInlinePropertyAsAction(
+ *nsGkAtoms::font, nsGkAtoms::face, aNewState, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetInlinePropertyAsAction(nsGkAtoms::font, "
+ "nsGkAtoms::face) failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::FontSizeStateCommand
+ *****************************************************************************/
+
+StaticRefPtr<FontSizeStateCommand> FontSizeStateCommand::sInstance;
+
+nsresult FontSizeStateCommand::GetCurrentState(HTMLEditor* aHTMLEditor,
+ nsCommandParams& aParams) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoString outStateString;
+ bool firstHas, anyHas, allHas;
+ nsresult rv = aHTMLEditor->GetInlinePropertyWithAttrValue(
+ *nsGkAtoms::font, nsGkAtoms::size, u""_ns, &firstHas, &anyHas, &allHas,
+ outStateString);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::GetInlinePropertyWithAttrValue(nsGkAtoms::font, "
+ "nsGkAtoms::size) failed");
+ return rv;
+ }
+
+ nsAutoCString tOutStateString;
+ LossyCopyUTF16toASCII(outStateString, tOutStateString);
+ aParams.SetBool(STATE_MIXED, anyHas && !allHas);
+ aParams.SetCString(STATE_ATTRIBUTE, tOutStateString);
+ aParams.SetBool(STATE_ENABLED, true);
+
+ return NS_OK;
+}
+
+// acceptable values for "aNewState" are:
+// -2
+// -1
+// 0
+// +1
+// +2
+// +3
+// medium
+// normal
+nsresult FontSizeStateCommand::SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aNewState.IsEmpty() && !aNewState.EqualsLiteral("normal") &&
+ !aNewState.EqualsLiteral("medium")) {
+ nsresult rv = aHTMLEditor->SetInlinePropertyAsAction(
+ *nsGkAtoms::font, nsGkAtoms::size, aNewState, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetInlinePropertyAsAction(nsGkAtoms::"
+ "font, nsGkAtoms::size) failed");
+ return rv;
+ }
+
+ // remove any existing font size, big or small
+ nsresult rv = aHTMLEditor->RemoveInlinePropertyAsAction(
+ *nsGkAtoms::font, nsGkAtoms::size, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveInlinePropertyAsAction(nsGkAtoms::"
+ "font, nsGkAtoms::size) failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::FontColorStateCommand
+ *****************************************************************************/
+
+StaticRefPtr<FontColorStateCommand> FontColorStateCommand::sInstance;
+
+nsresult FontColorStateCommand::GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool outMixed;
+ nsAutoString outStateString;
+ nsresult rv = aHTMLEditor->GetFontColorState(&outMixed, outStateString);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetFontColorState() failed");
+ return rv;
+ }
+
+ nsAutoCString tOutStateString;
+ LossyCopyUTF16toASCII(outStateString, tOutStateString);
+ aParams.SetBool(STATE_MIXED, outMixed);
+ aParams.SetCString(STATE_ATTRIBUTE, tOutStateString);
+ return NS_OK;
+}
+
+nsresult FontColorStateCommand::SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (aNewState.IsEmpty() || aNewState.EqualsLiteral("normal")) {
+ nsresult rv = aHTMLEditor->RemoveInlinePropertyAsAction(
+ *nsGkAtoms::font, nsGkAtoms::color, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveInlinePropertyAsAction(nsGkAtoms::"
+ "font, nsGkAtoms::color) failed");
+ return rv;
+ }
+
+ nsresult rv = aHTMLEditor->SetInlinePropertyAsAction(
+ *nsGkAtoms::font, nsGkAtoms::color, aNewState, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetInlinePropertyAsAction(nsGkAtoms::font, "
+ "nsGkAtoms::color) failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::HighlightColorStateCommand
+ *****************************************************************************/
+
+StaticRefPtr<HighlightColorStateCommand> HighlightColorStateCommand::sInstance;
+
+nsresult HighlightColorStateCommand::GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool outMixed;
+ nsAutoString outStateString;
+ nsresult rv = aHTMLEditor->GetHighlightColorState(&outMixed, outStateString);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetHighlightColorState() failed");
+ return rv;
+ }
+
+ nsAutoCString tOutStateString;
+ LossyCopyUTF16toASCII(outStateString, tOutStateString);
+ aParams.SetBool(STATE_MIXED, outMixed);
+ aParams.SetCString(STATE_ATTRIBUTE, tOutStateString);
+ return NS_OK;
+}
+
+nsresult HighlightColorStateCommand::SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (aNewState.IsEmpty() || aNewState.EqualsLiteral("normal")) {
+ nsresult rv = aHTMLEditor->RemoveInlinePropertyAsAction(
+ *nsGkAtoms::font, nsGkAtoms::bgcolor, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveInlinePropertyAsAction(nsGkAtoms::"
+ "font, nsGkAtoms::bgcolor) failed");
+ return rv;
+ }
+
+ nsresult rv = aHTMLEditor->SetInlinePropertyAsAction(
+ *nsGkAtoms::font, nsGkAtoms::bgcolor, aNewState, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetInlinePropertyAsAction(nsGkAtoms::font, "
+ "nsGkAtoms::bgcolor) failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::BackgroundColorStateCommand
+ *****************************************************************************/
+
+StaticRefPtr<BackgroundColorStateCommand>
+ BackgroundColorStateCommand::sInstance;
+
+nsresult BackgroundColorStateCommand::GetCurrentState(
+ HTMLEditor* aHTMLEditor, nsCommandParams& aParams) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool outMixed;
+ nsAutoString outStateString;
+ nsresult rv = aHTMLEditor->GetBackgroundColorState(&outMixed, outStateString);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetBackgroundColorState() failed");
+ return rv;
+ }
+
+ nsAutoCString tOutStateString;
+ LossyCopyUTF16toASCII(outStateString, tOutStateString);
+ aParams.SetBool(STATE_MIXED, outMixed);
+ aParams.SetCString(STATE_ATTRIBUTE, tOutStateString);
+ return NS_OK;
+}
+
+nsresult BackgroundColorStateCommand::SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv = aHTMLEditor->SetBackgroundColorAsAction(aNewState, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetBackgroundColorAsAction() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::AlignCommand
+ *****************************************************************************/
+
+StaticRefPtr<AlignCommand> AlignCommand::sInstance;
+
+nsresult AlignCommand::GetCurrentState(HTMLEditor* aHTMLEditor,
+ nsCommandParams& aParams) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ErrorResult error;
+ AlignStateAtSelection state(*aHTMLEditor, error);
+ if (error.Failed()) {
+ if (!state.IsSelectionRangesFound()) {
+ // If there was no selection ranges, we shouldn't throw exception for
+ // compatibility with the other browsers, but I have no better idea
+ // than returning empty string in this case. Oddly, Blink/WebKit returns
+ // "true" or "false", but it's different from us and the value does not
+ // make sense. Additionally, WPT loves our behavior.
+ error.SuppressException();
+ aParams.SetBool(STATE_MIXED, false);
+ aParams.SetCString(STATE_ATTRIBUTE, ""_ns);
+ return NS_OK;
+ }
+ NS_WARNING("AlignStateAtSelection failed");
+ return error.StealNSResult();
+ }
+ nsCString alignment; // Don't use `nsAutoCString` to avoid copying string.
+ switch (state.AlignmentAtSelectionStart()) {
+ default:
+ case nsIHTMLEditor::eLeft:
+ alignment.AssignLiteral("left");
+ break;
+ case nsIHTMLEditor::eCenter:
+ alignment.AssignLiteral("center");
+ break;
+ case nsIHTMLEditor::eRight:
+ alignment.AssignLiteral("right");
+ break;
+ case nsIHTMLEditor::eJustify:
+ alignment.AssignLiteral("justify");
+ break;
+ }
+ aParams.SetBool(STATE_MIXED, false);
+ aParams.SetCString(STATE_ATTRIBUTE, alignment);
+ return NS_OK;
+}
+
+nsresult AlignCommand::SetState(HTMLEditor* aHTMLEditor,
+ const nsAString& aNewState,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(!aHTMLEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv = aHTMLEditor->AlignAsAction(aNewState, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::AlignAsAction() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::AbsolutePositioningCommand
+ *****************************************************************************/
+
+StaticRefPtr<AbsolutePositioningCommand> AbsolutePositioningCommand::sInstance;
+
+nsresult AbsolutePositioningCommand::GetCurrentState(
+ nsStaticAtom& aTagName, HTMLEditor& aHTMLEditor,
+ nsCommandParams& aParams) const {
+ if (!aHTMLEditor.IsAbsolutePositionEditorEnabled()) {
+ aParams.SetBool(STATE_MIXED, false);
+ aParams.SetCString(STATE_ATTRIBUTE, ""_ns);
+ return NS_OK;
+ }
+
+ RefPtr<Element> container =
+ aHTMLEditor.GetAbsolutelyPositionedSelectionContainer();
+ aParams.SetBool(STATE_MIXED, false);
+ aParams.SetCString(STATE_ATTRIBUTE, container ? "absolute"_ns : ""_ns);
+ return NS_OK;
+}
+
+nsresult AbsolutePositioningCommand::ToggleState(
+ nsStaticAtom& aTagName, HTMLEditor& aHTMLEditor,
+ nsIPrincipal* aPrincipal) const {
+ RefPtr<Element> container =
+ aHTMLEditor.GetAbsolutelyPositionedSelectionContainer();
+ nsresult rv = aHTMLEditor.SetSelectionToAbsoluteOrStaticAsAction(!container,
+ aPrincipal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::SetSelectionToAbsoluteOrStaticAsAction() failed");
+ return rv;
+}
+
+/*****************************************************************************
+ * mozilla::DecreaseZIndexCommand
+ *****************************************************************************/
+
+StaticRefPtr<DecreaseZIndexCommand> DecreaseZIndexCommand::sInstance;
+
+bool DecreaseZIndexCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ RefPtr<HTMLEditor> htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ if (!htmlEditor->IsAbsolutePositionEditorEnabled()) {
+ return false;
+ }
+ RefPtr<Element> positionedElement = htmlEditor->GetPositionedElement();
+ if (!positionedElement) {
+ return false;
+ }
+ return htmlEditor->GetZIndex(*positionedElement) > 0;
+}
+
+nsresult DecreaseZIndexCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = MOZ_KnownLive(htmlEditor)->AddZIndexAsAction(-1, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::AddZIndexAsAction(-1) failed");
+ return rv;
+}
+
+nsresult DecreaseZIndexCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::IncreaseZIndexCommand
+ *****************************************************************************/
+
+StaticRefPtr<IncreaseZIndexCommand> IncreaseZIndexCommand::sInstance;
+
+bool IncreaseZIndexCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ if (!htmlEditor->IsAbsolutePositionEditorEnabled()) {
+ return false;
+ }
+ return !!htmlEditor->GetPositionedElement();
+}
+
+nsresult IncreaseZIndexCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = MOZ_KnownLive(htmlEditor)->AddZIndexAsAction(1, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::AddZIndexAsAction(1) failed");
+ return rv;
+}
+
+nsresult IncreaseZIndexCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::RemoveStylesCommand
+ *****************************************************************************/
+
+StaticRefPtr<RemoveStylesCommand> RemoveStylesCommand::sInstance;
+
+bool RemoveStylesCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ // test if we have any styles?
+ return htmlEditor->IsSelectionEditable();
+}
+
+nsresult RemoveStylesCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_OK;
+ }
+ nsresult rv =
+ MOZ_KnownLive(htmlEditor)->RemoveAllInlinePropertiesAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveAllInlinePropertiesAsAction() failed");
+ return rv;
+}
+
+nsresult RemoveStylesCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::IncreaseFontSizeCommand
+ *****************************************************************************/
+
+StaticRefPtr<IncreaseFontSizeCommand> IncreaseFontSizeCommand::sInstance;
+
+bool IncreaseFontSizeCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ // test if we are at max size?
+ return htmlEditor->IsSelectionEditable();
+}
+
+nsresult IncreaseFontSizeCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_OK;
+ }
+ nsresult rv = MOZ_KnownLive(htmlEditor)->IncreaseFontSizeAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::IncreaseFontSizeAsAction() failed");
+ return rv;
+}
+
+nsresult IncreaseFontSizeCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::DecreaseFontSizeCommand
+ *****************************************************************************/
+
+StaticRefPtr<DecreaseFontSizeCommand> DecreaseFontSizeCommand::sInstance;
+
+bool DecreaseFontSizeCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ // test if we are at min size?
+ return htmlEditor->IsSelectionEditable();
+}
+
+nsresult DecreaseFontSizeCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_OK;
+ }
+ nsresult rv = MOZ_KnownLive(htmlEditor)->DecreaseFontSizeAsAction(aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::DecreaseFontSizeAsAction() failed");
+ return rv;
+}
+
+nsresult DecreaseFontSizeCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::InsertHTMLCommand
+ *****************************************************************************/
+
+StaticRefPtr<InsertHTMLCommand> InsertHTMLCommand::sInstance;
+
+bool InsertHTMLCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ return htmlEditor->IsSelectionEditable();
+}
+
+nsresult InsertHTMLCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ // If InsertHTMLCommand is called with no parameters, it was probably called
+ // with an empty string parameter ''. In this case, it should act the same as
+ // the delete command
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv =
+ MOZ_KnownLive(htmlEditor)->InsertHTMLAsAction(u""_ns, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLAsAction() failed");
+ return rv;
+}
+
+nsresult InsertHTMLCommand::DoCommandParam(Command aCommand,
+ const nsAString& aStringParam,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(aStringParam.IsVoid())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv =
+ MOZ_KnownLive(htmlEditor)->InsertHTMLAsAction(aStringParam, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLAsAction() failed");
+ return rv;
+}
+
+nsresult InsertHTMLCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * mozilla::InsertTagCommand
+ *****************************************************************************/
+
+StaticRefPtr<InsertTagCommand> InsertTagCommand::sInstance;
+
+bool InsertTagCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (!htmlEditor) {
+ return false;
+ }
+ return htmlEditor->IsSelectionEditable();
+}
+
+// corresponding STATE_ATTRIBUTE is: src (img) and href (a)
+nsresult InsertTagCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ nsAtom* tagName = GetTagName(aCommand);
+ if (NS_WARN_IF(tagName != nsGkAtoms::hr)) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Element> newElement =
+ MOZ_KnownLive(htmlEditor)
+ ->CreateElementWithDefaults(MOZ_KnownLive(*tagName));
+ if (NS_WARN_IF(!newElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv =
+ MOZ_KnownLive(htmlEditor)
+ ->InsertElementAtSelectionAsAction(newElement, true, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertElementAtSelectionAsAction() failed");
+ return rv;
+}
+
+nsresult InsertTagCommand::DoCommandParam(Command aCommand,
+ const nsAString& aStringParam,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT(aCommand != Command::InsertHorizontalRule);
+
+ if (NS_WARN_IF(aStringParam.IsEmpty())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsAtom* tagName = GetTagName(aCommand);
+ if (NS_WARN_IF(!tagName)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // filter out tags we don't know how to insert
+ nsAtom* attribute = nullptr;
+ if (tagName == nsGkAtoms::a) {
+ attribute = nsGkAtoms::href;
+ } else if (tagName == nsGkAtoms::img) {
+ attribute = nsGkAtoms::src;
+ } else {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ RefPtr<Element> newElement =
+ MOZ_KnownLive(htmlEditor)
+ ->CreateElementWithDefaults(MOZ_KnownLive(*tagName));
+ if (!newElement) {
+ NS_WARNING("HTMLEditor::CreateElementWithDefaults() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult error;
+ newElement->SetAttr(attribute, aStringParam, error);
+ if (error.Failed()) {
+ NS_WARNING("Element::SetAttr() failed");
+ return error.StealNSResult();
+ }
+
+ // do actual insertion
+ if (tagName == nsGkAtoms::a) {
+ nsresult rv =
+ MOZ_KnownLive(htmlEditor)
+ ->InsertLinkAroundSelectionAsAction(newElement, aPrincipal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertLinkAroundSelectionAsAction() failed");
+ return rv;
+ }
+
+ nsresult rv =
+ MOZ_KnownLive(htmlEditor)
+ ->InsertElementAtSelectionAsAction(newElement, true, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertElementAtSelectionAsAction() failed");
+ return rv;
+}
+
+nsresult InsertTagCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ return aParams.SetBool(STATE_ENABLED,
+ IsCommandEnabled(aCommand, aEditorBase));
+}
+
+/*****************************************************************************
+ * Helper methods
+ *****************************************************************************/
+
+static nsresult GetListState(HTMLEditor* aHTMLEditor, bool* aMixed,
+ nsAString& aLocalName) {
+ MOZ_ASSERT(aHTMLEditor);
+ MOZ_ASSERT(aMixed);
+
+ *aMixed = false;
+ aLocalName.Truncate();
+
+ ErrorResult error;
+ ListElementSelectionState state(*aHTMLEditor, error);
+ if (error.Failed()) {
+ NS_WARNING("ListElementSelectionState failed");
+ return error.StealNSResult();
+ }
+ if (state.IsNotOneTypeListElementSelected()) {
+ *aMixed = true;
+ return NS_OK;
+ }
+
+ if (state.IsOLElementSelected()) {
+ aLocalName.AssignLiteral("ol");
+ } else if (state.IsULElementSelected()) {
+ aLocalName.AssignLiteral("ul");
+ } else if (state.IsDLElementSelected()) {
+ aLocalName.AssignLiteral("dl");
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorController.cpp b/editor/libeditor/HTMLEditorController.cpp
new file mode 100644
index 0000000000..ea795b920b
--- /dev/null
+++ b/editor/libeditor/HTMLEditorController.cpp
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/HTMLEditorController.h"
+
+#include "mozilla/EditorCommands.h" // for StyleUpdatingCommand, etc
+#include "mozilla/mozalloc.h" // for operator new
+#include "nsControllerCommandTable.h" // for nsControllerCommandTable
+#include "nsError.h" // for NS_OK
+
+namespace mozilla {
+
+#define NS_REGISTER_COMMAND(_cmdClass, _cmdName) \
+ { \
+ aCommandTable->RegisterCommand( \
+ _cmdName, \
+ static_cast<nsIControllerCommand*>(_cmdClass::GetInstance())); \
+ }
+
+// static
+nsresult HTMLEditorController::RegisterEditorDocStateCommands(
+ nsControllerCommandTable* aCommandTable) {
+ // observer commands for document state
+ NS_REGISTER_COMMAND(DocumentStateCommand, "obs_documentCreated")
+ NS_REGISTER_COMMAND(DocumentStateCommand, "obs_documentWillBeDestroyed")
+ NS_REGISTER_COMMAND(DocumentStateCommand, "obs_documentLocationChanged")
+
+ // commands that may get or change state
+ NS_REGISTER_COMMAND(SetDocumentStateCommand, "cmd_setDocumentModified")
+ NS_REGISTER_COMMAND(SetDocumentStateCommand, "cmd_setDocumentUseCSS")
+ NS_REGISTER_COMMAND(SetDocumentStateCommand, "cmd_setDocumentReadOnly")
+ NS_REGISTER_COMMAND(SetDocumentStateCommand, "cmd_insertBrOnReturn")
+ NS_REGISTER_COMMAND(SetDocumentStateCommand, "cmd_defaultParagraphSeparator")
+ NS_REGISTER_COMMAND(SetDocumentStateCommand, "cmd_enableObjectResizing")
+ NS_REGISTER_COMMAND(SetDocumentStateCommand, "cmd_enableInlineTableEditing")
+ NS_REGISTER_COMMAND(SetDocumentStateCommand,
+ "cmd_enableAbsolutePositionEditing")
+ NS_REGISTER_COMMAND(SetDocumentStateCommand,
+ "cmd_enableCompatibleJoinSplitNodeDirection")
+
+ return NS_OK;
+}
+
+// static
+nsresult HTMLEditorController::RegisterHTMLEditorCommands(
+ nsControllerCommandTable* aCommandTable) {
+ // Edit menu
+ NS_REGISTER_COMMAND(PasteNoFormattingCommand, "cmd_pasteNoFormatting");
+
+ // indent/outdent
+ NS_REGISTER_COMMAND(IndentCommand, "cmd_indent");
+ NS_REGISTER_COMMAND(OutdentCommand, "cmd_outdent");
+
+ // Styles
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_bold");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_italic");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_underline");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_tt");
+
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_strikethrough");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_superscript");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_subscript");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_nobreak");
+
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_em");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_strong");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_cite");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_abbr");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_acronym");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_code");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_samp");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_var");
+ NS_REGISTER_COMMAND(StyleUpdatingCommand, "cmd_removeLinks");
+
+ // lists
+ NS_REGISTER_COMMAND(ListCommand, "cmd_ol");
+ NS_REGISTER_COMMAND(ListCommand, "cmd_ul");
+ NS_REGISTER_COMMAND(ListItemCommand, "cmd_dt");
+ NS_REGISTER_COMMAND(ListItemCommand, "cmd_dd");
+ NS_REGISTER_COMMAND(RemoveListCommand, "cmd_removeList");
+
+ // format stuff
+ NS_REGISTER_COMMAND(FormatBlockStateCommand, "cmd_formatBlock");
+ NS_REGISTER_COMMAND(ParagraphStateCommand, "cmd_paragraphState");
+ NS_REGISTER_COMMAND(FontFaceStateCommand, "cmd_fontFace");
+ NS_REGISTER_COMMAND(FontSizeStateCommand, "cmd_fontSize");
+ NS_REGISTER_COMMAND(FontColorStateCommand, "cmd_fontColor");
+ NS_REGISTER_COMMAND(BackgroundColorStateCommand, "cmd_backgroundColor");
+ NS_REGISTER_COMMAND(HighlightColorStateCommand, "cmd_highlight");
+
+ NS_REGISTER_COMMAND(AlignCommand, "cmd_align");
+ NS_REGISTER_COMMAND(RemoveStylesCommand, "cmd_removeStyles");
+
+ NS_REGISTER_COMMAND(IncreaseFontSizeCommand, "cmd_increaseFont");
+ NS_REGISTER_COMMAND(DecreaseFontSizeCommand, "cmd_decreaseFont");
+
+ // Insert content
+ NS_REGISTER_COMMAND(InsertHTMLCommand, "cmd_insertHTML");
+ NS_REGISTER_COMMAND(InsertTagCommand, "cmd_insertLinkNoUI");
+ NS_REGISTER_COMMAND(InsertTagCommand, "cmd_insertImageNoUI");
+ NS_REGISTER_COMMAND(InsertTagCommand, "cmd_insertHR");
+
+ NS_REGISTER_COMMAND(AbsolutePositioningCommand, "cmd_absPos");
+ NS_REGISTER_COMMAND(DecreaseZIndexCommand, "cmd_decreaseZIndex");
+ NS_REGISTER_COMMAND(IncreaseZIndexCommand, "cmd_increaseZIndex");
+
+ return NS_OK;
+}
+
+// static
+void HTMLEditorController::Shutdown() {
+ // EditorDocStateCommands
+ DocumentStateCommand::Shutdown();
+ SetDocumentStateCommand::Shutdown();
+
+ // HTMLEditorCommands
+ PasteNoFormattingCommand::Shutdown();
+ IndentCommand::Shutdown();
+ OutdentCommand::Shutdown();
+ StyleUpdatingCommand::Shutdown();
+ ListCommand::Shutdown();
+ ListItemCommand::Shutdown();
+ RemoveListCommand::Shutdown();
+ FormatBlockStateCommand::Shutdown();
+ ParagraphStateCommand::Shutdown();
+ FontFaceStateCommand::Shutdown();
+ FontSizeStateCommand::Shutdown();
+ FontColorStateCommand::Shutdown();
+ BackgroundColorStateCommand::Shutdown();
+ HighlightColorStateCommand::Shutdown();
+ AlignCommand::Shutdown();
+ RemoveStylesCommand::Shutdown();
+ IncreaseFontSizeCommand::Shutdown();
+ DecreaseFontSizeCommand::Shutdown();
+ InsertHTMLCommand::Shutdown();
+ InsertTagCommand::Shutdown();
+ AbsolutePositioningCommand::Shutdown();
+ DecreaseZIndexCommand::Shutdown();
+ IncreaseZIndexCommand::Shutdown();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorController.h b/editor/libeditor/HTMLEditorController.h
new file mode 100644
index 0000000000..cf296e57c3
--- /dev/null
+++ b/editor/libeditor/HTMLEditorController.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_HTMLEditorController_h__
+#define mozilla_HTMLEditorController_h__
+
+#include "nscore.h" // for nsresult
+
+class nsControllerCommandTable;
+
+namespace mozilla {
+
+class HTMLEditorController final {
+ public:
+ static nsresult RegisterEditorDocStateCommands(
+ nsControllerCommandTable* aCommandTable);
+ static nsresult RegisterHTMLEditorCommands(
+ nsControllerCommandTable* aCommandTable);
+ static void Shutdown();
+};
+
+} // namespace mozilla
+
+#endif /* mozllla_HTMLEditorController_h__ */
diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp
new file mode 100644
index 0000000000..72aeb7eee8
--- /dev/null
+++ b/editor/libeditor/HTMLEditorDataTransfer.cpp
@@ -0,0 +1,4353 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditor.h"
+
+#include <string.h>
+
+#include "EditAction.h"
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditUtils.h"
+#include "InternetCiter.h"
+#include "PendingStyles.h"
+#include "SelectionState.h"
+#include "WSRunObject.h"
+
+#include "ErrorList.h"
+#include "mozilla/dom/Comment.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/DOMStringList.h"
+#include "mozilla/dom/DOMStringList.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/FileBlobImpl.h"
+#include "mozilla/dom/FileReader.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Base64.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Result.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsCRTGlue.h" // for CRLF
+#include "nsComponentManagerUtils.h"
+#include "nsIScriptError.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsFocusManager.h"
+#include "nsGkAtoms.h"
+#include "nsIClipboard.h"
+#include "nsIContent.h"
+#include "nsIDocumentEncoder.h"
+#include "nsIFile.h"
+#include "nsIInputStream.h"
+#include "nsIMIMEService.h"
+#include "nsINode.h"
+#include "nsIParserUtils.h"
+#include "nsIPrincipal.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsPrimitives.h"
+#include "nsISupportsUtils.h"
+#include "nsITransferable.h"
+#include "nsIVariant.h"
+#include "nsLinebreakConverter.h"
+#include "nsLiteralString.h"
+#include "nsNameSpaceManager.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+#include "nsRange.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStringIterator.h"
+#include "nsTreeSanitizer.h"
+#include "nsXPCOM.h"
+#include "nscore.h"
+#include "nsContentUtils.h"
+#include "nsQueryObject.h"
+
+class nsAtom;
+class nsILoadContext;
+
+namespace mozilla {
+
+using namespace dom;
+using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+
+#define kInsertCookie "_moz_Insert Here_moz_"
+
+// some little helpers
+static bool FindIntegerAfterString(const char* aLeadingString,
+ const nsCString& aCStr,
+ int32_t& foundNumber);
+static void RemoveFragComments(nsCString& aStr);
+
+nsresult HTMLEditor::InsertDroppedDataTransferAsAction(
+ AutoEditActionDataSetter& aEditActionData, DataTransfer& aDataTransfer,
+ const EditorDOMPoint& aDroppedAt, nsIPrincipal* aSourcePrincipal) {
+ MOZ_ASSERT(aEditActionData.GetEditAction() == EditAction::eDrop);
+ MOZ_ASSERT(GetEditAction() == EditAction::eDrop);
+ MOZ_ASSERT(aDroppedAt.IsSet());
+ MOZ_ASSERT(aDataTransfer.MozItemCount() > 0);
+
+ if (IsReadonly()) {
+ return NS_OK;
+ }
+
+ aEditActionData.InitializeDataTransfer(&aDataTransfer);
+ RefPtr<StaticRange> targetRange = StaticRange::Create(
+ aDroppedAt.GetContainer(), aDroppedAt.Offset(), aDroppedAt.GetContainer(),
+ aDroppedAt.Offset(), IgnoreErrors());
+ NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(),
+ "Why did we fail to create collapsed static range at "
+ "dropped position?");
+ if (targetRange && targetRange->IsPositioned()) {
+ aEditActionData.AppendTargetRange(*targetRange);
+ }
+ nsresult rv = aEditActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return rv;
+ }
+ uint32_t numItems = aDataTransfer.MozItemCount();
+ for (uint32_t i = 0; i < numItems; ++i) {
+ DebugOnly<nsresult> rvIgnored =
+ InsertFromDataTransfer(&aDataTransfer, i, aSourcePrincipal, aDroppedAt,
+ DeleteSelectedContent::No);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_OK;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::InsertFromDataTransfer("
+ "DeleteSelectedContent::No) failed, but ignored");
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::LoadHTML(const nsAString& aInputString) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // force IME commit; set up rules sniffing and batching
+ DebugOnly<nsresult> rvIgnored = CommitComposition();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CommitComposition() failed, but ignored");
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertHTMLSource, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::EnsureNoPaddingBRElementForEmptyEditor() failed");
+ return rv;
+ }
+
+ // Delete Selection, but only if it isn't collapsed, see bug #106269
+ if (!SelectionRef().IsCollapsed()) {
+ nsresult rv = DeleteSelectionAsSubAction(eNone, eStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed");
+ return rv;
+ }
+ }
+
+ // Get the first range in the selection, for context:
+ RefPtr<const nsRange> range = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create fragment for pasted HTML.
+ ErrorResult error;
+ RefPtr<DocumentFragment> documentFragment =
+ range->CreateContextualFragment(aInputString, error);
+ if (error.Failed()) {
+ NS_WARNING("nsRange::CreateContextualFragment() failed");
+ return error.StealNSResult();
+ }
+
+ // Put the fragment into the document at start of selection.
+ EditorDOMPoint pointToInsert(range->StartRef());
+ // XXX We need to make pointToInsert store offset for keeping traditional
+ // behavior since using only child node to pointing insertion point
+ // changes the behavior when inserted child is moved by mutation
+ // observer. We need to investigate what we should do here.
+ Unused << pointToInsert.Offset();
+ EditorDOMPoint pointToPutCaret;
+ for (nsCOMPtr<nsIContent> contentToInsert = documentFragment->GetFirstChild();
+ contentToInsert; contentToInsert = documentFragment->GetFirstChild()) {
+ Result<CreateContentResult, nsresult> insertChildContentNodeResult =
+ InsertNodeWithTransaction(*contentToInsert, pointToInsert);
+ if (MOZ_UNLIKELY(insertChildContentNodeResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertChildContentNodeResult.unwrapErr();
+ }
+ CreateContentResult unwrappedInsertChildContentNodeResult =
+ insertChildContentNodeResult.unwrap();
+ unwrappedInsertChildContentNodeResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ // XXX If the inserted node has been moved by mutation observer,
+ // incrementing offset will cause odd result. Next new node
+ // will be inserted after existing node and the offset will be
+ // overflown from the container node.
+ pointToInsert.Set(pointToInsert.GetContainer(), pointToInsert.Offset() + 1);
+ if (NS_WARN_IF(!pointToInsert.Offset())) {
+ // Append the remaining children to the container if offset is
+ // overflown.
+ pointToInsert.SetToEndOf(pointToInsert.GetContainer());
+ }
+ }
+
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::InsertHTML(const nsAString& aInString) {
+ nsresult rv = InsertHTMLAsAction(aInString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLAsAction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::InsertHTMLAsAction(const nsAString& aInString,
+ nsIPrincipal* aPrincipal) {
+ // FIXME: This should keep handling inserting HTML if the caller is
+ // nsIHTMLEditor::InsertHTML.
+ if (IsReadonly()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertHTML,
+ aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertHTMLWithContextAsSubAction(aInString, u""_ns, u""_ns, u""_ns,
+ SafeToInsertData::Yes, EditorDOMPoint(),
+ DeleteSelectedContent::Yes,
+ InlineStylesAtInsertionPoint::Clear);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "SafeToInsertData::Yes, DeleteSelectedContent::Yes, "
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+class MOZ_STACK_CLASS HTMLEditor::HTMLWithContextInserter final {
+ public:
+ MOZ_CAN_RUN_SCRIPT explicit HTMLWithContextInserter(HTMLEditor& aHTMLEditor)
+ : mHTMLEditor(aHTMLEditor) {}
+
+ HTMLWithContextInserter() = delete;
+ HTMLWithContextInserter(const HTMLWithContextInserter&) = delete;
+ HTMLWithContextInserter(HTMLWithContextInserter&&) = delete;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
+ const nsAString& aInputString, const nsAString& aContextStr,
+ const nsAString& aInfoStr, SafeToInsertData aSafeToInsertData,
+ InlineStylesAtInsertionPoint aInlineStylesAtInsertionPoint);
+
+ private:
+ class FragmentFromPasteCreator;
+ class FragmentParser;
+ /**
+ * CollectTopMostChildContentsCompletelyInRange() collects topmost child
+ * contents which are completely in the given range.
+ * For example, if the range points a node with its container node, the
+ * result is only the node (meaning does not include its descendants).
+ * If the range starts start of a node and ends end of it, and if the node
+ * does not have children, returns no nodes, otherwise, if the node has
+ * some children, the result includes its all children (not including their
+ * descendants).
+ *
+ * @param aStartPoint Start point of the range.
+ * @param aEndPoint End point of the range.
+ * @param aOutArrayOfContents [Out] Topmost children which are completely in
+ * the range.
+ */
+ static void CollectTopMostChildContentsCompletelyInRange(
+ const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents);
+
+ /**
+ * @return nullptr, if there's no invisible `<br>`.
+ */
+ HTMLBRElement* GetInvisibleBRElementAtPoint(
+ const EditorDOMPoint& aPointToInsert) const;
+
+ EditorDOMPoint GetNewCaretPointAfterInsertingHTML(
+ const EditorDOMPoint& aLastInsertedPoint) const;
+
+ /**
+ * @return error result or the last inserted point. The latter is only set, if
+ * content was inserted.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ InsertContents(
+ const EditorDOMPoint& aPointToInsert,
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents,
+ const nsINode* aFragmentAsNode);
+
+ /**
+ * @param aContextStr as indicated by nsITransferable's kHTMLContext.
+ * @param aInfoStr as indicated by nsITransferable's kHTMLInfo.
+ */
+ nsresult CreateDOMFragmentFromPaste(
+ const nsAString& aInputString, const nsAString& aContextStr,
+ const nsAString& aInfoStr, nsCOMPtr<nsINode>* aOutFragNode,
+ nsCOMPtr<nsINode>* aOutStartNode, nsCOMPtr<nsINode>* aOutEndNode,
+ uint32_t* aOutStartOffset, uint32_t* aOutEndOffset,
+ SafeToInsertData aSafeToInsertData) const;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MoveCaretOutsideOfLink(
+ Element& aLinkElement, const EditorDOMPoint& aPointToPutCaret);
+
+ // MOZ_KNOWN_LIVE because this is set only by the constructor which is
+ // marked as MOZ_CAN_RUN_SCRIPT and this is allocated only in the stack.
+ MOZ_KNOWN_LIVE HTMLEditor& mHTMLEditor;
+};
+
+class MOZ_STACK_CLASS
+ HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator final {
+ public:
+ nsresult Run(const Document& aDocument, const nsAString& aInputString,
+ const nsAString& aContextStr, const nsAString& aInfoStr,
+ nsCOMPtr<nsINode>* aOutFragNode,
+ nsCOMPtr<nsINode>* aOutStartNode, nsCOMPtr<nsINode>* aOutEndNode,
+ SafeToInsertData aSafeToInsertData) const;
+
+ private:
+ nsresult CreateDocumentFragmentAndGetParentOfPastedHTMLInContext(
+ const Document& aDocument, const nsAString& aInputString,
+ const nsAString& aContextStr, SafeToInsertData aSafeToInsertData,
+ nsCOMPtr<nsINode>& aParentNodeOfPastedHTMLInContext,
+ RefPtr<DocumentFragment>& aDocumentFragmentToInsert) const;
+
+ static nsAtom* DetermineContextLocalNameForParsingPastedHTML(
+ const nsIContent* aParentContentOfPastedHTMLInContext);
+
+ static bool FindTargetNodeOfContextForPastedHTMLAndRemoveInsertionCookie(
+ nsINode& aStart, nsCOMPtr<nsINode>& aResult);
+
+ static bool IsInsertionCookie(const nsIContent& aContent);
+
+ /**
+ * @param aDocumentFragmentForContext contains the merged result.
+ */
+ static nsresult MergeAndPostProcessFragmentsForPastedHTMLAndContext(
+ DocumentFragment& aDocumentFragmentForPastedHTML,
+ DocumentFragment& aDocumentFragmentForContext,
+ nsIContent& aTargetContentOfContextForPastedHTML);
+
+ /**
+ * @param aInfoStr as indicated by nsITransferable's kHTMLInfo.
+ */
+ [[nodiscard]] static nsresult MoveStartAndEndAccordingToHTMLInfo(
+ const nsAString& aInfoStr, nsCOMPtr<nsINode>* aOutStartNode,
+ nsCOMPtr<nsINode>* aOutEndNode);
+
+ static nsresult PostProcessFragmentForPastedHTMLWithoutContext(
+ DocumentFragment& aDocumentFragmentForPastedHTML);
+
+ static nsresult PreProcessContextDocumentFragmentForMerging(
+ DocumentFragment& aDocumentFragmentForContext);
+
+ static void RemoveHeadChildAndStealBodyChildsChildren(nsINode& aNode);
+
+ /**
+ * This is designed for a helper class to remove disturbing nodes at inserting
+ * the HTML fragment into the DOM tree. This walks the children and if some
+ * elements do not have enough children, e.g., list elements not having
+ * another visible list elements nor list item elements,
+ * will be removed.
+ *
+ * @param aNode Should not be a node whose mutation may be observed by
+ * JS.
+ */
+ static void RemoveIncompleteDescendantsFromInsertingFragment(nsINode& aNode);
+
+ enum class NodesToRemove {
+ eAll,
+ eOnlyListItems /*!< List items are always block-level elements, hence such
+ whitespace-only nodes are always invisible. */
+ };
+ static nsresult
+ RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces(
+ nsIContent& aNode, NodesToRemove aNodesToRemove);
+};
+
+HTMLBRElement*
+HTMLEditor::HTMLWithContextInserter::GetInvisibleBRElementAtPoint(
+ const EditorDOMPoint& aPointToInsert) const {
+ WSRunScanner wsRunScannerAtInsertionPoint(
+ mHTMLEditor.ComputeEditingHost(), aPointToInsert,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (wsRunScannerAtInsertionPoint.EndsByInvisibleBRElement()) {
+ return wsRunScannerAtInsertionPoint.EndReasonBRElementPtr();
+ }
+ return nullptr;
+}
+
+EditorDOMPoint
+HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML(
+ const EditorDOMPoint& aLastInsertedPoint) const {
+ EditorDOMPoint pointToPutCaret;
+
+ // but don't cross tables
+ nsIContent* containerContent = nullptr;
+ if (!HTMLEditUtils::IsTable(aLastInsertedPoint.GetChild())) {
+ containerContent = HTMLEditUtils::GetLastLeafContent(
+ *aLastInsertedPoint.GetChild(), {LeafNodeType::OnlyEditableLeafNode},
+ BlockInlineCheck::Unused,
+ aLastInsertedPoint.GetChild()->GetAsElementOrParentElement());
+ if (containerContent) {
+ Element* mostDistantInclusiveAncestorTableElement = nullptr;
+ for (Element* maybeTableElement =
+ containerContent->GetAsElementOrParentElement();
+ maybeTableElement &&
+ maybeTableElement != aLastInsertedPoint.GetChild();
+ maybeTableElement = maybeTableElement->GetParentElement()) {
+ if (HTMLEditUtils::IsTable(maybeTableElement)) {
+ mostDistantInclusiveAncestorTableElement = maybeTableElement;
+ }
+ }
+ // If we're in table elements, we should put caret into the most ancestor
+ // table element.
+ if (mostDistantInclusiveAncestorTableElement) {
+ containerContent = mostDistantInclusiveAncestorTableElement;
+ }
+ }
+ }
+ // If we are not in table elements, we should put caret in the last inserted
+ // node.
+ if (!containerContent) {
+ containerContent = aLastInsertedPoint.GetChild();
+ }
+
+ // If the container is a text node or a container element except `<table>`
+ // element, put caret a end of it.
+ if (containerContent->IsText() ||
+ (HTMLEditUtils::IsContainerNode(*containerContent) &&
+ !HTMLEditUtils::IsTable(containerContent))) {
+ pointToPutCaret.SetToEndOf(containerContent);
+ }
+ // Otherwise, i.e., it's an atomic element, `<table>` element or data node,
+ // put caret after it.
+ else {
+ pointToPutCaret.Set(containerContent);
+ DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset();
+ NS_WARNING_ASSERTION(advanced, "Failed to advance offset from found node");
+ }
+
+ // Make sure we don't end up with selection collapsed after an invisible
+ // `<br>` element.
+ Element* editingHost = mHTMLEditor.ComputeEditingHost();
+ WSRunScanner wsRunScannerAtCaret(editingHost, pointToPutCaret,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (wsRunScannerAtCaret
+ .ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToPutCaret)
+ .ReachedInvisibleBRElement()) {
+ WSRunScanner wsRunScannerAtStartReason(
+ editingHost,
+ EditorDOMPoint(wsRunScannerAtCaret.GetStartReasonContent()),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ WSScanResult backwardScanFromPointToCaretResult =
+ wsRunScannerAtStartReason.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ pointToPutCaret);
+ if (backwardScanFromPointToCaretResult.InVisibleOrCollapsibleCharacters()) {
+ pointToPutCaret =
+ backwardScanFromPointToCaretResult.Point<EditorDOMPoint>();
+ } else if (backwardScanFromPointToCaretResult.ReachedSpecialContent()) {
+ // XXX In my understanding, this is odd. The end reason may not be
+ // same as the reached special content because the equality is
+ // guaranteed only when ReachedCurrentBlockBoundary() returns true.
+ // However, looks like that this code assumes that
+ // GetStartReasonContent() returns the content.
+ NS_ASSERTION(wsRunScannerAtStartReason.GetStartReasonContent() ==
+ backwardScanFromPointToCaretResult.GetContent(),
+ "Start reason is not the reached special content");
+ pointToPutCaret.SetAfter(
+ wsRunScannerAtStartReason.GetStartReasonContent());
+ }
+ }
+
+ return pointToPutCaret;
+}
+
+nsresult HTMLEditor::InsertHTMLWithContextAsSubAction(
+ const nsAString& aInputString, const nsAString& aContextStr,
+ const nsAString& aInfoStr, const nsAString& aFlavor,
+ SafeToInsertData aSafeToInsertData, const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint aInlineStylesAtInsertionPoint) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ CommitComposition();
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::ePasteHTMLContent, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+ ignoredError.SuppressException();
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ // If we have a destination / target node, we want to insert there rather than
+ // in place of the selection. Ignore aDeleteSelectedContent here if
+ // aPointToInsert is not set since deletion will also occur later in
+ // HTMLWithContextInserter and will be collapsed around there; this block
+ // is intended to cover the various scenarios where we are dropping in an
+ // editor (and may want to delete the selection before collapsing the
+ // selection in the new destination)
+ if (aPointToInsert.IsSet()) {
+ nsresult rv =
+ PrepareToInsertContent(aPointToInsert, aDeleteSelectedContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::PrepareToInsertContent() failed");
+ return rv;
+ }
+ aDeleteSelectedContent = DeleteSelectedContent::No;
+ }
+
+ HTMLWithContextInserter htmlWithContextInserter(*this);
+
+ Result<EditActionResult, nsresult> result = htmlWithContextInserter.Run(
+ aInputString, aContextStr, aInfoStr, aSafeToInsertData,
+ aInlineStylesAtInsertionPoint);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ return result.unwrapErr();
+ }
+
+ // If nothing is inserted and delete selection is required, we need to
+ // delete selection right now.
+ if (result.inspect().Ignored() &&
+ aDeleteSelectedContent == DeleteSelectedContent::Yes) {
+ nsresult rv = DeleteSelectionAsSubAction(eNone, eStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::HTMLWithContextInserter::Run(
+ const nsAString& aInputString, const nsAString& aContextStr,
+ const nsAString& aInfoStr, SafeToInsertData aSafeToInsertData,
+ InlineStylesAtInsertionPoint aInlineStylesAtInsertionPoint) {
+ MOZ_ASSERT(mHTMLEditor.IsEditActionDataAvailable());
+
+ // create a dom document fragment that represents the structure to paste
+ nsCOMPtr<nsINode> fragmentAsNode, streamStartParent, streamEndParent;
+ uint32_t streamStartOffset = 0, streamEndOffset = 0;
+
+ nsresult rv = CreateDOMFragmentFromPaste(
+ aInputString, aContextStr, aInfoStr, address_of(fragmentAsNode),
+ address_of(streamStartParent), address_of(streamEndParent),
+ &streamStartOffset, &streamEndOffset, aSafeToInsertData);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::CreateDOMFragmentFromPaste() "
+ "failed");
+ return Err(rv);
+ }
+
+ // we need to recalculate various things based on potentially new offsets
+ // this is work to be completed at a later date (probably by jfrancis)
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfTopMostChildContents;
+ // If we have stream start point information, lets use it and end point.
+ // Otherwise, we should make a range all over the document fragment.
+ EditorRawDOMPoint streamStartPoint =
+ streamStartParent
+ ? EditorRawDOMPoint(streamStartParent,
+ AssertedCast<uint32_t>(streamStartOffset))
+ : EditorRawDOMPoint(fragmentAsNode, 0);
+ EditorRawDOMPoint streamEndPoint =
+ streamStartParent ? EditorRawDOMPoint(streamEndParent, streamEndOffset)
+ : EditorRawDOMPoint::AtEndOf(fragmentAsNode);
+
+ Unused << streamStartPoint;
+ Unused << streamEndPoint;
+
+ HTMLWithContextInserter::CollectTopMostChildContentsCompletelyInRange(
+ EditorRawDOMPoint(streamStartParent,
+ AssertedCast<uint32_t>(streamStartOffset)),
+ EditorRawDOMPoint(streamEndParent,
+ AssertedCast<uint32_t>(streamEndOffset)),
+ arrayOfTopMostChildContents);
+
+ if (arrayOfTopMostChildContents.IsEmpty()) {
+ return EditActionResult::IgnoredResult(); // Nothing to insert.
+ }
+
+ // Are there any table elements in the list?
+ // check for table cell selection mode
+ bool cellSelectionMode =
+ HTMLEditUtils::IsInTableCellSelectionMode(mHTMLEditor.SelectionRef());
+
+ if (cellSelectionMode) {
+ // do we have table content to paste? If so, we want to delete
+ // the selected table cells and replace with new table elements;
+ // but if not we want to delete _contents_ of cells and replace
+ // with non-table elements. Use cellSelectionMode bool to
+ // indicate results.
+ if (!HTMLEditUtils::IsAnyTableElement(arrayOfTopMostChildContents[0])) {
+ cellSelectionMode = false;
+ }
+ }
+
+ if (!cellSelectionMode) {
+ rv = mHTMLEditor.DeleteSelectionAndPrepareToCreateNode();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteSelectionAndPrepareToCreateNode() failed");
+ return Err(rv);
+ }
+
+ if (aInlineStylesAtInsertionPoint == InlineStylesAtInsertionPoint::Clear) {
+ // pasting does not inherit local inline styles
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ mHTMLEditor.ClearStyleAt(
+ EditorDOMPoint(mHTMLEditor.SelectionRef().AnchorRef()),
+ EditorInlineStyle::RemoveAllStyles(), SpecifiedStyle::Preserve);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::ClearStyleAt() failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSetAndValid()) {
+ nsresult rv =
+ mHTMLEditor.CollapseSelectionTo(pointToPutCaretOrError.unwrap());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ }
+ } else {
+ // Delete whole cells: we will replace with new table content.
+
+ // Braces for artificial block to scope AutoSelectionRestorer.
+ // Save current selection since DeleteTableCellWithTransaction() perturbs
+ // it.
+ {
+ AutoSelectionRestorer restoreSelectionLater(mHTMLEditor);
+ rv = mHTMLEditor.DeleteTableCellWithTransaction(1);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteTableCellWithTransaction(1) failed");
+ return Err(rv);
+ }
+ }
+ // collapse selection to beginning of deleted table content
+ IgnoredErrorResult ignoredError;
+ mHTMLEditor.SelectionRef().CollapseToStart(ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "Selection::Collapse() failed, but ignored");
+ }
+
+ {
+ Result<EditActionResult, nsresult> result =
+ mHTMLEditor.CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ mHTMLEditor.UndefineCaretBidiLevel();
+
+ rv = mHTMLEditor.EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && mHTMLEditor.SelectionRef().IsCollapsed()) {
+ nsresult rv = mHTMLEditor.EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = mHTMLEditor.PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ Element* editingHost =
+ mHTMLEditor.ComputeEditingHost(HTMLEditor::LimitInBodyElement::No);
+ if (NS_WARN_IF(!editingHost)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Adjust position based on the first node we are going to insert.
+ EditorDOMPoint pointToInsert =
+ HTMLEditUtils::GetBetterInsertionPointFor<EditorDOMPoint>(
+ arrayOfTopMostChildContents[0],
+ mHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>(),
+ *editingHost);
+ if (!pointToInsert.IsSet()) {
+ NS_WARNING("HTMLEditor::GetBetterInsertionPointFor() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Remove invisible `<br>` element at the point because if there is a `<br>`
+ // element at end of what we paste, it will make the existing invisible
+ // `<br>` element visible.
+ if (HTMLBRElement* invisibleBRElement =
+ GetInvisibleBRElementAtPoint(pointToInsert)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
+ nsresult rv = mHTMLEditor.DeleteNodeWithTransaction(
+ MOZ_KnownLive(*invisibleBRElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed.");
+ return Err(rv);
+ }
+ }
+
+ const bool insertionPointWasInLink =
+ !!HTMLEditor::GetLinkElement(pointToInsert.GetContainer());
+
+ if (pointToInsert.IsInTextNode()) {
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ mHTMLEditor.SplitNodeDeepWithTransaction(
+ MOZ_KnownLive(*pointToInsert.ContainerAs<nsIContent>()),
+ pointToInsert, SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitNodeResult.propagateErr();
+ }
+ nsresult rv = splitNodeResult.inspect().SuggestCaretPointTo(
+ mHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ pointToInsert = splitNodeResult.inspect().AtSplitPoint<EditorDOMPoint>();
+ if (MOZ_UNLIKELY(!pointToInsert.IsSet())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction() didn't return split "
+ "point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ { // Block only for AutoHTMLFragmentBoundariesFixer to hide it from the
+ // following code. Note that it may modify arrayOfTopMostChildContents.
+ AutoHTMLFragmentBoundariesFixer fixPiecesOfTablesAndLists(
+ arrayOfTopMostChildContents);
+ }
+
+ MOZ_ASSERT(pointToInsert.GetContainer()->GetChildAt_Deprecated(
+ pointToInsert.Offset()) == pointToInsert.GetChild());
+
+ Result<EditorDOMPoint, nsresult> lastInsertedPoint = InsertContents(
+ pointToInsert, arrayOfTopMostChildContents, fragmentAsNode);
+ if (lastInsertedPoint.isErr()) {
+ NS_WARNING("HTMLWithContextInserter::InsertContents() failed.");
+ return lastInsertedPoint.propagateErr();
+ }
+
+ mHTMLEditor.TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements =
+ false;
+
+ if (MOZ_UNLIKELY(!lastInsertedPoint.inspect().IsInComposedDoc())) {
+ return EditActionResult::HandledResult();
+ }
+
+ const EditorDOMPoint pointToPutCaret =
+ GetNewCaretPointAfterInsertingHTML(lastInsertedPoint.inspect());
+ // Now collapse the selection to the end of what we just inserted.
+ rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+
+ // If we didn't start from an `<a href>` element, we should not keep
+ // caret in the link to make users type something outside the link.
+ if (insertionPointWasInLink) {
+ return EditActionResult::HandledResult();
+ }
+ RefPtr<Element> linkElement = GetLinkElement(pointToPutCaret.GetContainer());
+
+ if (!linkElement) {
+ return EditActionResult::HandledResult();
+ }
+
+ rv = MoveCaretOutsideOfLink(*linkElement, pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::MoveCaretOutsideOfLink "
+ "failed.");
+ return Err(rv);
+ }
+
+ return EditActionResult::HandledResult();
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::HTMLWithContextInserter::InsertContents(
+ const EditorDOMPoint& aPointToInsert,
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents,
+ const nsINode* aFragmentAsNode) {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValidInComposedDoc());
+
+ EditorDOMPoint pointToInsert{aPointToInsert};
+
+ // Loop over the node list and paste the nodes:
+ const RefPtr<const Element> maybeNonEditableBlockElement =
+ pointToInsert.IsInContentNode()
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *pointToInsert.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)
+ : nullptr;
+
+ EditorDOMPoint lastInsertedPoint;
+ nsCOMPtr<nsIContent> insertedContextParentContent;
+ for (OwningNonNull<nsIContent>& content : aArrayOfTopMostChildContents) {
+ if (NS_WARN_IF(content == aFragmentAsNode) ||
+ NS_WARN_IF(content->IsHTMLElement(nsGkAtoms::body))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (insertedContextParentContent) {
+ // If we had to insert something higher up in the paste hierarchy,
+ // we want to skip any further paste nodes that descend from that.
+ // Else we will paste twice.
+ // XXX This check may be really expensive. Cannot we check whether
+ // the node's `ownerDocument` is the `aFragmentAsNode` or not?
+ // XXX If content was moved to outside of insertedContextParentContent
+ // by mutation event listeners, we will anyway duplicate it.
+ if (EditorUtils::IsDescendantOf(*content,
+ *insertedContextParentContent)) {
+ continue;
+ }
+ }
+
+ // If a `<table>` or `<tr>` element on the clipboard, and pasting it into
+ // a `<table>` or `<tr>` element, insert only the appropriate children
+ // instead.
+ bool inserted = false;
+ if (HTMLEditUtils::IsTableRow(content) &&
+ HTMLEditUtils::IsTableRow(pointToInsert.GetContainer()) &&
+ (HTMLEditUtils::IsTable(content) ||
+ HTMLEditUtils::IsTable(pointToInsert.GetContainer()))) {
+ // Move children of current node to the insertion point.
+ AutoTArray<OwningNonNull<nsIContent>, 24> children;
+ HTMLEditUtils::CollectAllChildren(*content, children);
+ EditorDOMPoint pointToPutCaret;
+ for (const OwningNonNull<nsIContent>& child : children) {
+ // MOZ_KnownLive(child) because of bug 1622253
+ Result<CreateContentResult, nsresult> moveChildResult =
+ mHTMLEditor.InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ MOZ_KnownLive(child), pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(moveChildResult.isErr())) {
+ // If moving node is moved to different place, we should ignore
+ // this result and keep trying to insert next content node to same
+ // position.
+ if (moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ inserted = true;
+ continue; // the inner `for` loop
+ }
+ NS_WARNING(
+ "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, maybe "
+ "ignored");
+ break; // from the inner `for` loop
+ }
+ if (MOZ_UNLIKELY(!moveChildResult.inspect().Handled())) {
+ continue;
+ }
+ inserted = true;
+ lastInsertedPoint.Set(child);
+ pointToInsert = lastInsertedPoint.NextPoint();
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ CreateContentResult unwrappedMoveChildResult = moveChildResult.unwrap();
+ unwrappedMoveChildResult.MoveCaretPointTo(
+ pointToPutCaret, mHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ } // end of the inner `for` loop
+
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+ // If a list element on the clipboard, and pasting it into a list or
+ // list item element, insert the appropriate children instead. I.e.,
+ // merge the list elements instead of pasting as a sublist.
+ else if (HTMLEditUtils::IsAnyListElement(content) &&
+ (HTMLEditUtils::IsAnyListElement(pointToInsert.GetContainer()) ||
+ HTMLEditUtils::IsListItem(pointToInsert.GetContainer()))) {
+ AutoTArray<OwningNonNull<nsIContent>, 24> children;
+ HTMLEditUtils::CollectAllChildren(*content, children);
+ EditorDOMPoint pointToPutCaret;
+ for (const OwningNonNull<nsIContent>& child : children) {
+ if (HTMLEditUtils::IsListItem(child) ||
+ HTMLEditUtils::IsAnyListElement(child)) {
+ // If we're pasting into empty list item, we should remove it
+ // and past current node into the parent list directly.
+ // XXX This creates invalid structure if current list item element
+ // is not proper child of the parent element, or current node
+ // is a list element.
+ if (HTMLEditUtils::IsListItem(pointToInsert.GetContainer()) &&
+ HTMLEditUtils::IsEmptyNode(
+ *pointToInsert.GetContainer(),
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ NS_WARNING_ASSERTION(pointToInsert.GetContainerParent(),
+ "Insertion point is out of the DOM tree");
+ if (pointToInsert.GetContainerParent()) {
+ pointToInsert.Set(pointToInsert.GetContainer());
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
+ nsresult rv = mHTMLEditor.DeleteNodeWithTransaction(
+ MOZ_KnownLive(*pointToInsert.GetChild()));
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() "
+ "failed, but ignored");
+ }
+ }
+ // MOZ_KnownLive(child) because of bug 1622253
+ Result<CreateContentResult, nsresult> moveChildResult =
+ mHTMLEditor
+ .InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ MOZ_KnownLive(child), pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(moveChildResult.isErr())) {
+ // If moving node is moved to different place, we should ignore
+ // this result and keep trying to insert next content node to
+ // same position.
+ if (moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ inserted = true;
+ continue; // the inner `for` loop
+ }
+ if (NS_WARN_IF(moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but maybe "
+ "ignored");
+ break; // from the inner `for` loop
+ }
+ if (MOZ_UNLIKELY(!moveChildResult.inspect().Handled())) {
+ continue;
+ }
+ inserted = true;
+ lastInsertedPoint.Set(child);
+ pointToInsert = lastInsertedPoint.NextPoint();
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ CreateContentResult unwrappedMoveChildResult =
+ moveChildResult.unwrap();
+ unwrappedMoveChildResult.MoveCaretPointTo(
+ pointToPutCaret, mHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+ // If the child of current node is not list item nor list element,
+ // we should remove it from the DOM tree.
+ else if (HTMLEditUtils::IsRemovableNode(child)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
+ IgnoredErrorResult ignoredError;
+ content->RemoveChild(child, ignoredError);
+ if (MOZ_UNLIKELY(mHTMLEditor.Destroyed())) {
+ NS_WARNING(
+ "nsIContent::RemoveChild() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "nsINode::RemoveChild() failed, but ignored");
+ } else {
+ NS_WARNING(
+ "Failed to delete the first child of a list element because the "
+ "list element non-editable");
+ break; // from the inner `for` loop
+ }
+ } // end of the inner `for` loop
+
+ if (MOZ_UNLIKELY(mHTMLEditor.Destroyed())) {
+ NS_WARNING("The editor has been destroyed");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+ // If pasting into a `<pre>` element and current node is a `<pre>` element,
+ // move only its children.
+ else if (HTMLEditUtils::IsPre(maybeNonEditableBlockElement) &&
+ HTMLEditUtils::IsPre(content)) {
+ // Check for pre's going into pre's.
+ AutoTArray<OwningNonNull<nsIContent>, 24> children;
+ HTMLEditUtils::CollectAllChildren(*content, children);
+ EditorDOMPoint pointToPutCaret;
+ for (const OwningNonNull<nsIContent>& child : children) {
+ // MOZ_KnownLive(child) because of bug 1622253
+ Result<CreateContentResult, nsresult> moveChildResult =
+ mHTMLEditor.InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ MOZ_KnownLive(child), pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(moveChildResult.isErr())) {
+ // If moving node is moved to different place, we should ignore
+ // this result and keep trying to insert next content node there.
+ if (moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ inserted = true;
+ continue; // the inner `for` loop
+ }
+ if (NS_WARN_IF(moveChildResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return moveChildResult.propagateErr();
+ }
+ NS_WARNING(
+ "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but maybe "
+ "ignored");
+ break; // from the inner `for` loop
+ }
+ if (MOZ_UNLIKELY(!moveChildResult.inspect().Handled())) {
+ continue;
+ }
+ CreateContentResult unwrappedMoveChildResult = moveChildResult.unwrap();
+ inserted = true;
+ lastInsertedPoint.Set(child);
+ pointToInsert = lastInsertedPoint.NextPoint();
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ unwrappedMoveChildResult.MoveCaretPointTo(
+ pointToPutCaret, mHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ } // end of the inner `for` loop
+
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+
+ // TODO: For making the above code clearer, we should move this fallback
+ // path into a lambda and call it in each if/else-if block.
+ // If we haven't inserted current node nor its children, move current node
+ // to the insertion point.
+ if (!inserted) {
+ // MOZ_KnownLive(content) because 'aArrayOfTopMostChildContents' is
+ // guaranteed to keep it alive.
+ Result<CreateContentResult, nsresult> moveContentResult =
+ mHTMLEditor.InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ MOZ_KnownLive(content), pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_LIKELY(moveContentResult.isOk())) {
+ if (MOZ_UNLIKELY(!moveContentResult.inspect().Handled())) {
+ continue;
+ }
+ lastInsertedPoint.Set(content);
+ pointToInsert = lastInsertedPoint;
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ nsresult rv = moveContentResult.inspect().SuggestCaretPointTo(
+ mHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateContentResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateContentResult::SuggestCaretPointTo() failed, but ignored");
+ } else if (moveContentResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ // Moving node is moved to different place, we should keep trying to
+ // insert the next content to same position.
+ } else {
+ NS_WARNING(
+ "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but ignored");
+ // Assume failure means no legal parent in the document hierarchy,
+ // try again with the parent of content in the paste hierarchy.
+ // FYI: We cannot use `InclusiveAncestorOfType` here because of
+ // calling `InsertNodeIntoProperAncestorWithTransaction()`.
+ for (nsCOMPtr<nsIContent> childContent = content; childContent;
+ childContent = childContent->GetParent()) {
+ if (NS_WARN_IF(!childContent->GetParent()) ||
+ NS_WARN_IF(
+ childContent->GetParent()->IsHTMLElement(nsGkAtoms::body))) {
+ break; // for the inner `for` loop
+ }
+ const OwningNonNull<nsIContent> oldParentContent =
+ *childContent->GetParent();
+ Result<CreateContentResult, nsresult> moveParentResult =
+ mHTMLEditor
+ .InsertNodeIntoProperAncestorWithTransaction<nsIContent>(
+ oldParentContent, pointToInsert,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(moveParentResult.isErr())) {
+ // Moving node is moved to different place, we should keep trying to
+ // insert the next content to same position.
+ if (moveParentResult.inspectErr() ==
+ NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE) {
+ break; // from the inner `for` loop
+ }
+ if (NS_WARN_IF(moveParentResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::InsertNodeInToProperAncestorWithTransaction("
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed, but "
+ "ignored");
+ continue; // the inner `for` loop
+ }
+ if (MOZ_UNLIKELY(!moveParentResult.inspect().Handled())) {
+ continue;
+ }
+ insertedContextParentContent = oldParentContent;
+ pointToInsert.Set(oldParentContent);
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ nsresult rv = moveParentResult.inspect().SuggestCaretPointTo(
+ mHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateContentResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateContentResult::SuggestCaretPointTo() failed, but ignored");
+ break; // from the inner `for` loop
+ } // end of the inner `for` loop
+ }
+ }
+ if (lastInsertedPoint.IsSet()) {
+ if (MOZ_UNLIKELY(lastInsertedPoint.GetContainer() !=
+ lastInsertedPoint.GetChild()->GetParentNode())) {
+ NS_WARNING(
+ "HTMLEditor::InsertHTMLWithContextAsSubAction() got lost insertion "
+ "point");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ pointToInsert = lastInsertedPoint.NextPoint();
+ MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc());
+ }
+ } // end of the `for` loop
+
+ return lastInsertedPoint;
+}
+
+nsresult HTMLEditor::HTMLWithContextInserter::MoveCaretOutsideOfLink(
+ Element& aLinkElement, const EditorDOMPoint& aPointToPutCaret) {
+ MOZ_ASSERT(HTMLEditUtils::IsLink(&aLinkElement));
+
+ // The reason why do that instead of just moving caret after it is, the
+ // link might have ended in an invisible `<br>` element. If so, the code
+ // above just placed selection inside that. So we need to split it instead.
+ // XXX Sounds like that it's not really expensive comparing with the reason
+ // to use SplitNodeDeepWithTransaction() here.
+ Result<SplitNodeResult, nsresult> splitLinkResult =
+ mHTMLEditor.SplitNodeDeepWithTransaction(
+ aLinkElement, aPointToPutCaret,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitLinkResult.isErr())) {
+ if (splitLinkResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction() failed, but ignored");
+ }
+
+ if (nsIContent* previousContentOfSplitPoint =
+ splitLinkResult.inspect().GetPreviousContent()) {
+ splitLinkResult.inspect().IgnoreCaretPointSuggestion();
+ nsresult rv = mHTMLEditor.CollapseSelectionTo(
+ EditorRawDOMPoint::After(*previousContentOfSplitPoint));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return NS_OK;
+ }
+
+ nsresult rv = splitLinkResult.inspect().SuggestCaretPointTo(
+ mHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "SplitNodeResult::SuggestCaretPointTo() failed");
+ return rv;
+}
+
+// static
+Element* HTMLEditor::GetLinkElement(nsINode* aNode) {
+ if (NS_WARN_IF(!aNode)) {
+ return nullptr;
+ }
+ nsINode* node = aNode;
+ while (node) {
+ if (HTMLEditUtils::IsLink(node)) {
+ return node->AsElement();
+ }
+ node = node->GetParentNode();
+ }
+ return nullptr;
+}
+
+// static
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces(
+ nsIContent& aNode, NodesToRemove aNodesToRemove) {
+ if (aNode.TextIsOnlyWhitespace()) {
+ nsCOMPtr<nsINode> parent = aNode.GetParentNode();
+ // TODO: presumably, if the parent is a `<pre>` element, the node
+ // shouldn't be removed.
+ if (parent) {
+ if (aNodesToRemove == NodesToRemove::eAll ||
+ HTMLEditUtils::IsAnyListElement(parent)) {
+ ErrorResult error;
+ parent->RemoveChild(aNode, error);
+ NS_WARNING_ASSERTION(!error.Failed(), "nsINode::RemoveChild() failed");
+ return error.StealNSResult();
+ }
+ return NS_OK;
+ }
+ }
+
+ if (!aNode.IsHTMLElement(nsGkAtoms::pre)) {
+ nsCOMPtr<nsIContent> child = aNode.GetLastChild();
+ while (child) {
+ nsCOMPtr<nsIContent> previous = child->GetPreviousSibling();
+ nsresult rv = FragmentFromPasteCreator::
+ RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces(
+ *child, aNodesToRemove);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces"
+ "() "
+ "failed");
+ return rv;
+ }
+ child = std::move(previous);
+ }
+ }
+ return NS_OK;
+}
+
+class MOZ_STACK_CLASS HTMLEditor::HTMLTransferablePreparer {
+ public:
+ HTMLTransferablePreparer(const HTMLEditor& aHTMLEditor,
+ nsITransferable** aTransferable);
+
+ nsresult Run();
+
+ private:
+ void AddDataFlavorsInBestOrder(nsITransferable& aTransferable) const;
+
+ const HTMLEditor& mHTMLEditor;
+ nsITransferable** mTransferable;
+};
+
+HTMLEditor::HTMLTransferablePreparer::HTMLTransferablePreparer(
+ const HTMLEditor& aHTMLEditor, nsITransferable** aTransferable)
+ : mHTMLEditor{aHTMLEditor}, mTransferable{aTransferable} {
+ MOZ_ASSERT(mTransferable);
+ MOZ_ASSERT(!*mTransferable);
+}
+
+nsresult HTMLEditor::PrepareHTMLTransferable(
+ nsITransferable** aTransferable) const {
+ HTMLTransferablePreparer htmlTransferablePreparer{*this, aTransferable};
+ return htmlTransferablePreparer.Run();
+}
+
+nsresult HTMLEditor::HTMLTransferablePreparer::Run() {
+ // Create generic Transferable for getting the data
+ nsresult rv;
+ RefPtr<nsITransferable> transferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("do_CreateInstance() failed to create nsITransferable instance");
+ return rv;
+ }
+
+ if (!transferable) {
+ NS_WARNING("do_CreateInstance() returned nullptr, but ignored");
+ return NS_OK;
+ }
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ RefPtr<Document> destdoc = mHTMLEditor.GetDocument();
+ nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr;
+ DebugOnly<nsresult> rvIgnored = transferable->Init(loadContext);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::Init() failed, but ignored");
+
+ // See `HTMLEditor::InsertFromTransferableAtSelection`.
+ AddDataFlavorsInBestOrder(*transferable);
+
+ transferable.forget(mTransferable);
+
+ return NS_OK;
+}
+
+void HTMLEditor::HTMLTransferablePreparer::AddDataFlavorsInBestOrder(
+ nsITransferable& aTransferable) const {
+ // Create the desired DataFlavor for the type of data
+ // we want to get out of the transferable
+ // This should only happen in html editors, not plaintext
+ if (!mHTMLEditor.IsPlaintextMailComposer()) {
+ DebugOnly<nsresult> rvIgnored =
+ aTransferable.AddDataFlavor(kNativeHTMLMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kNativeHTMLMime) failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kHTMLMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kHTMLMime) failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kFileMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kFileMime) failed, but ignored");
+
+ switch (Preferences::GetInt("clipboard.paste_image_type", 1)) {
+ case 0: // prefer JPEG over PNG over GIF encoding
+ rvIgnored = aTransferable.AddDataFlavor(kJPEGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPEGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kPNGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kPNGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kGIFImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kGIFImageMime) "
+ "failed, but ignored");
+ break;
+ case 1: // prefer PNG over JPEG over GIF encoding (default)
+ default:
+ rvIgnored = aTransferable.AddDataFlavor(kPNGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kPNGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPEGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPEGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kGIFImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kGIFImageMime) "
+ "failed, but ignored");
+ break;
+ case 2: // prefer GIF over JPEG over PNG encoding
+ rvIgnored = aTransferable.AddDataFlavor(kGIFImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kGIFImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPEGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPEGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kJPGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kJPGImageMime) "
+ "failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kPNGImageMime);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kPNGImageMime) "
+ "failed, but ignored");
+ break;
+ }
+ }
+ DebugOnly<nsresult> rvIgnored = aTransferable.AddDataFlavor(kTextMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kTextMime) failed, but ignored");
+ rvIgnored = aTransferable.AddDataFlavor(kMozTextInternal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kMozTextInternal) failed, but ignored");
+}
+
+bool FindIntegerAfterString(const char* aLeadingString, const nsCString& aCStr,
+ int32_t& foundNumber) {
+ // first obtain offsets from cfhtml str
+ int32_t numFront = aCStr.Find(aLeadingString);
+ if (numFront == -1) {
+ return false;
+ }
+ numFront += strlen(aLeadingString);
+
+ int32_t numBack = aCStr.FindCharInSet(CRLF, numFront);
+ if (numBack == -1) {
+ return false;
+ }
+
+ nsAutoCString numStr(Substring(aCStr, numFront, numBack - numFront));
+ nsresult errorCode;
+ foundNumber = numStr.ToInteger(&errorCode);
+ return true;
+}
+
+void RemoveFragComments(nsCString& aStr) {
+ // remove the StartFragment/EndFragment comments from the str, if present
+ int32_t startCommentIndx = aStr.Find("<!--StartFragment");
+ if (startCommentIndx >= 0) {
+ int32_t startCommentEnd = aStr.Find("-->", startCommentIndx);
+ if (startCommentEnd > startCommentIndx) {
+ aStr.Cut(startCommentIndx, (startCommentEnd + 3) - startCommentIndx);
+ }
+ }
+ int32_t endCommentIndx = aStr.Find("<!--EndFragment");
+ if (endCommentIndx >= 0) {
+ int32_t endCommentEnd = aStr.Find("-->", endCommentIndx);
+ if (endCommentEnd > endCommentIndx) {
+ aStr.Cut(endCommentIndx, (endCommentEnd + 3) - endCommentIndx);
+ }
+ }
+}
+
+nsresult HTMLEditor::ParseCFHTML(const nsCString& aCfhtml,
+ char16_t** aStuffToPaste,
+ char16_t** aCfcontext) {
+ // First obtain offsets from cfhtml str.
+ int32_t startHTML, endHTML, startFragment, endFragment;
+ if (!FindIntegerAfterString("StartHTML:", aCfhtml, startHTML) ||
+ startHTML < -1) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!FindIntegerAfterString("EndHTML:", aCfhtml, endHTML) || endHTML < -1) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!FindIntegerAfterString("StartFragment:", aCfhtml, startFragment) ||
+ startFragment < 0) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!FindIntegerAfterString("EndFragment:", aCfhtml, endFragment) ||
+ startFragment < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The StartHTML and EndHTML markers are allowed to be -1 to include
+ // everything.
+ // See Reference: MSDN doc entitled "HTML Clipboard Format"
+ // http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx#unknown_854
+ if (startHTML == -1) {
+ startHTML = aCfhtml.Find("<!--StartFragment-->");
+ if (startHTML == -1) {
+ return NS_OK;
+ }
+ }
+ if (endHTML == -1) {
+ const char endFragmentMarker[] = "<!--EndFragment-->";
+ endHTML = aCfhtml.Find(endFragmentMarker);
+ if (endHTML == -1) {
+ return NS_OK;
+ }
+ endHTML += ArrayLength(endFragmentMarker) - 1;
+ }
+
+ // create context string
+ nsAutoCString contextUTF8(
+ Substring(aCfhtml, startHTML, startFragment - startHTML) +
+ "<!--" kInsertCookie "-->"_ns +
+ Substring(aCfhtml, endFragment, endHTML - endFragment));
+
+ // validate startFragment
+ // make sure it's not in the middle of a HTML tag
+ // see bug #228879 for more details
+ int32_t curPos = startFragment;
+ while (curPos > startHTML) {
+ if (aCfhtml[curPos] == '>') {
+ // working backwards, the first thing we see is the end of a tag
+ // so StartFragment is good, so do nothing.
+ break;
+ }
+ if (aCfhtml[curPos] == '<') {
+ // if we are at the start, then we want to see the '<'
+ if (curPos != startFragment) {
+ // working backwards, the first thing we see is the start of a tag
+ // so StartFragment is bad, so we need to update it.
+ NS_ERROR(
+ "StartFragment byte count in the clipboard looks bad, see bug "
+ "#228879");
+ startFragment = curPos - 1;
+ }
+ break;
+ }
+ curPos--;
+ }
+
+ // create fragment string
+ nsAutoCString fragmentUTF8(
+ Substring(aCfhtml, startFragment, endFragment - startFragment));
+
+ // remove the StartFragment/EndFragment comments from the fragment, if present
+ RemoveFragComments(fragmentUTF8);
+
+ // remove the StartFragment/EndFragment comments from the context, if present
+ RemoveFragComments(contextUTF8);
+
+ // convert both strings to usc2
+ const nsString& fragUcs2Str = NS_ConvertUTF8toUTF16(fragmentUTF8);
+ const nsString& cntxtUcs2Str = NS_ConvertUTF8toUTF16(contextUTF8);
+
+ // translate platform linebreaks for fragment
+ int32_t oldLengthInChars =
+ fragUcs2Str.Length() + 1; // +1 to include null terminator
+ int32_t newLengthInChars = 0;
+ *aStuffToPaste = nsLinebreakConverter::ConvertUnicharLineBreaks(
+ fragUcs2Str.get(), nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakContent, oldLengthInChars,
+ &newLengthInChars);
+ if (!*aStuffToPaste) {
+ NS_WARNING("nsLinebreakConverter::ConvertUnicharLineBreaks() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // translate platform linebreaks for context
+ oldLengthInChars =
+ cntxtUcs2Str.Length() + 1; // +1 to include null terminator
+ newLengthInChars = 0;
+ *aCfcontext = nsLinebreakConverter::ConvertUnicharLineBreaks(
+ cntxtUcs2Str.get(), nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakContent, oldLengthInChars,
+ &newLengthInChars);
+ // it's ok for context to be empty. frag might be whole doc and contain all
+ // its context.
+
+ // we're done!
+ return NS_OK;
+}
+
+static nsresult ImgFromData(const nsACString& aType, const nsACString& aData,
+ nsString& aOutput) {
+ aOutput.AssignLiteral("<IMG src=\"data:");
+ AppendUTF8toUTF16(aType, aOutput);
+ aOutput.AppendLiteral(";base64,");
+ nsresult rv = Base64EncodeAppend(aData, aOutput);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Base64Encode() failed");
+ return rv;
+ }
+ aOutput.AppendLiteral("\" alt=\"\" >");
+ return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEditor::BlobReader)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HTMLEditor::BlobReader)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlob)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mHTMLEditor)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPointToInsert)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(HTMLEditor::BlobReader)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlob)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHTMLEditor)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPointToInsert)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+HTMLEditor::BlobReader::BlobReader(BlobImpl* aBlob, HTMLEditor* aHTMLEditor,
+ SafeToInsertData aSafeToInsertData,
+ const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent)
+ : mBlob(aBlob),
+ mHTMLEditor(aHTMLEditor),
+ // "beforeinput" event should've been dispatched before we read blob,
+ // but anyway, we need to clone dataTransfer for "input" event.
+ mDataTransfer(mHTMLEditor->GetInputEventDataTransfer()),
+ mPointToInsert(aPointToInsert),
+ mEditAction(aHTMLEditor->GetEditAction()),
+ mSafeToInsertData(aSafeToInsertData),
+ mDeleteSelectedContent(aDeleteSelectedContent),
+ mNeedsToDispatchBeforeInputEvent(
+ !mHTMLEditor->HasTriedToDispatchBeforeInputEvent()) {
+ MOZ_ASSERT(mBlob);
+ MOZ_ASSERT(mHTMLEditor);
+ MOZ_ASSERT(mHTMLEditor->IsEditActionDataAvailable());
+ MOZ_ASSERT(mDataTransfer);
+
+ // Take only offset here since it's our traditional behavior.
+ if (mPointToInsert.IsSet()) {
+ AutoEditorDOMPointChildInvalidator storeOnlyWithOffset(mPointToInsert);
+ }
+}
+
+nsresult HTMLEditor::BlobReader::OnResult(const nsACString& aResult) {
+ AutoEditActionDataSetter editActionData(*mHTMLEditor, mEditAction);
+ editActionData.InitializeDataTransfer(mDataTransfer);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_FAILURE);
+ }
+
+ if (NS_WARN_IF(mNeedsToDispatchBeforeInputEvent)) {
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ } else {
+ editActionData.MarkAsBeforeInputHasBeenDispatched();
+ }
+
+ nsString blobType;
+ mBlob->GetType(blobType);
+
+ // TODO: This does not work well.
+ // * If the data is not an image file, this inserts <img> element with odd
+ // data URI (bug 1610220).
+ // * If the data is valid image file data, an <img> file is inserted with
+ // data URI, but it's not loaded (bug 1610219).
+ NS_ConvertUTF16toUTF8 type(blobType);
+ nsAutoString stuffToPaste;
+ nsresult rv = ImgFromData(type, aResult, stuffToPaste);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("ImgFormData() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<HTMLEditor> htmlEditor = std::move(mHTMLEditor);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *htmlEditor, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ EditorDOMPoint pointToInsert = std::move(mPointToInsert);
+ rv = htmlEditor->InsertHTMLWithContextAsSubAction(
+ stuffToPaste, u""_ns, u""_ns, NS_LITERAL_STRING_FROM_CSTRING(kFileMime),
+ mSafeToInsertData, pointToInsert, mDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Preserve);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Preserve) failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::BlobReader::OnError(const nsAString& aError) {
+ AutoTArray<nsString, 1> error;
+ error.AppendElement(aError);
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "Editor"_ns, mHTMLEditor->GetDocument(),
+ nsContentUtils::eDOM_PROPERTIES, "EditorFileDropFailed", error);
+ return NS_OK;
+}
+
+class SlurpBlobEventListener final : public nsIDOMEventListener {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(SlurpBlobEventListener)
+
+ explicit SlurpBlobEventListener(HTMLEditor::BlobReader* aListener)
+ : mListener(aListener) {}
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override;
+
+ private:
+ ~SlurpBlobEventListener() = default;
+
+ RefPtr<HTMLEditor::BlobReader> mListener;
+};
+
+NS_IMPL_CYCLE_COLLECTION(SlurpBlobEventListener, mListener)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SlurpBlobEventListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SlurpBlobEventListener)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SlurpBlobEventListener)
+
+NS_IMETHODIMP SlurpBlobEventListener::HandleEvent(Event* aEvent) {
+ EventTarget* target = aEvent->GetTarget();
+ if (!target || !mListener) {
+ return NS_OK;
+ }
+
+ RefPtr<FileReader> reader = do_QueryObject(target);
+ if (!reader) {
+ return NS_OK;
+ }
+
+ EventMessage message = aEvent->WidgetEventPtr()->mMessage;
+
+ RefPtr<HTMLEditor::BlobReader> listener(mListener);
+ if (message == eLoad) {
+ MOZ_ASSERT(reader->DataFormat() == FileReader::FILE_AS_BINARY);
+
+ // The original data has been converted from Latin1 to UTF-16, this just
+ // undoes that conversion.
+ DebugOnly<nsresult> rvIgnored =
+ listener->OnResult(NS_LossyConvertUTF16toASCII(reader->Result()));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::BlobReader::OnResult() failed, but ignored");
+ return NS_OK;
+ }
+
+ if (message == eLoadError) {
+ nsAutoString errorMessage;
+ reader->GetError()->GetErrorMessage(errorMessage);
+ DebugOnly<nsresult> rvIgnored = listener->OnError(errorMessage);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::BlobReader::OnError() failed, but ignored");
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult HTMLEditor::SlurpBlob(Blob* aBlob, nsIGlobalObject* aGlobal,
+ BlobReader* aBlobReader) {
+ MOZ_ASSERT(aBlob);
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(aBlobReader);
+
+ RefPtr<WeakWorkerRef> workerRef;
+ RefPtr<FileReader> reader = new FileReader(aGlobal, workerRef);
+
+ RefPtr<SlurpBlobEventListener> eventListener =
+ new SlurpBlobEventListener(aBlobReader);
+
+ nsresult rv = reader->AddEventListener(u"load"_ns, eventListener, false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("FileReader::AddEventListener(load) failed");
+ return rv;
+ }
+
+ rv = reader->AddEventListener(u"error"_ns, eventListener, false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("FileReader::AddEventListener(error) failed");
+ return rv;
+ }
+
+ ErrorResult error;
+ reader->ReadAsBinaryString(*aBlob, error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "FileReader::ReadAsBinaryString() failed");
+ return error.StealNSResult();
+}
+
+nsresult HTMLEditor::InsertObject(
+ const nsACString& aType, nsISupports* aObject,
+ SafeToInsertData aSafeToInsertData, const EditorDOMPoint& aPointToInsert,
+ DeleteSelectedContent aDeleteSelectedContent) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Check to see if we the file is actually an image.
+ nsAutoCString type(aType);
+ if (type.EqualsLiteral(kFileMime)) {
+ if (nsCOMPtr<nsIFile> file = do_QueryInterface(aObject)) {
+ nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1");
+ if (NS_WARN_IF(!mime)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = mime->GetTypeFromFile(file, type);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIMIMEService::GetTypeFromFile() failed");
+ return rv;
+ }
+ }
+ }
+
+ nsCOMPtr<nsISupports> object = aObject;
+ if (type.EqualsLiteral(kJPEGImageMime) || type.EqualsLiteral(kJPGImageMime) ||
+ type.EqualsLiteral(kPNGImageMime) || type.EqualsLiteral(kGIFImageMime)) {
+ if (nsCOMPtr<nsIFile> file = do_QueryInterface(object)) {
+ object = new FileBlobImpl(file);
+ // Fallthrough to BlobImpl code below.
+ } else if (RefPtr<Blob> blob = do_QueryObject(object)) {
+ object = blob->Impl();
+ // Fallthrough.
+ } else if (nsCOMPtr<nsIInputStream> imageStream =
+ do_QueryInterface(object)) {
+ nsCString imageData;
+ nsresult rv = NS_ConsumeStream(imageStream, UINT32_MAX, imageData);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("NS_ConsumeStream() failed");
+ return rv;
+ }
+
+ rv = imageStream->Close();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIInputStream::Close() failed");
+ return rv;
+ }
+
+ nsAutoString stuffToPaste;
+ rv = ImgFromData(type, imageData, stuffToPaste);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("ImgFromData() failed");
+ return rv;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertHTMLWithContextAsSubAction(
+ stuffToPaste, u""_ns, u""_ns,
+ NS_LITERAL_STRING_FROM_CSTRING(kFileMime), aSafeToInsertData,
+ aPointToInsert, aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Preserve);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Preserve) failed, but ignored");
+ return NS_OK;
+ } else {
+ NS_WARNING("HTMLEditor::InsertObject: Unexpected type for image mime");
+ return NS_OK;
+ }
+ }
+
+ // We always try to insert BlobImpl even without a known image mime.
+ nsCOMPtr<BlobImpl> blob = do_QueryInterface(object);
+ if (!blob) {
+ return NS_OK;
+ }
+
+ RefPtr<BlobReader> br = new BlobReader(
+ blob, this, aSafeToInsertData, aPointToInsert, aDeleteSelectedContent);
+
+ nsCOMPtr<nsPIDOMWindowInner> inner = GetInnerWindow();
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(inner);
+ if (!global) {
+ NS_WARNING("Could not get global");
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Blob> domBlob = Blob::Create(global, blob);
+ if (!domBlob) {
+ NS_WARNING("Blob::Create() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = SlurpBlob(domBlob, global, br);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::::SlurpBlob() failed");
+ return rv;
+}
+
+static bool GetString(nsISupports* aData, nsAString& aText) {
+ if (nsCOMPtr<nsISupportsString> str = do_QueryInterface(aData)) {
+ DebugOnly<nsresult> rvIgnored = str->GetData(aText);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ return !aText.IsEmpty();
+ }
+
+ return false;
+}
+
+static bool GetCString(nsISupports* aData, nsACString& aText) {
+ if (nsCOMPtr<nsISupportsCString> str = do_QueryInterface(aData)) {
+ DebugOnly<nsresult> rvIgnored = str->GetData(aText);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ return !aText.IsEmpty();
+ }
+
+ return false;
+}
+
+nsresult HTMLEditor::InsertFromTransferableAtSelection(
+ nsITransferable* aTransferable, const nsAString& aContextStr,
+ const nsAString& aInfoStr, HavePrivateHTMLFlavor aHavePrivateHTMLFlavor) {
+ nsAutoCString bestFlavor;
+ nsCOMPtr<nsISupports> genericDataObj;
+
+ // See `HTMLTransferablePreparer::AddDataFlavorsInBestOrder`.
+ nsresult rv = aTransferable->GetAnyTransferData(
+ bestFlavor, getter_AddRefs(genericDataObj));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "nsITransferable::GetAnyTransferData() failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+ nsAutoString flavor;
+ CopyASCIItoUTF16(bestFlavor, flavor);
+ const SafeToInsertData safeToInsertData = IsSafeToInsertData(nullptr);
+
+ if (bestFlavor.EqualsLiteral(kFileMime) ||
+ bestFlavor.EqualsLiteral(kJPEGImageMime) ||
+ bestFlavor.EqualsLiteral(kJPGImageMime) ||
+ bestFlavor.EqualsLiteral(kPNGImageMime) ||
+ bestFlavor.EqualsLiteral(kGIFImageMime)) {
+ nsresult rv = InsertObject(bestFlavor, genericDataObj, safeToInsertData,
+ EditorDOMPoint(), DeleteSelectedContent::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::InsertObject() failed");
+ return rv;
+ }
+ } else if (bestFlavor.EqualsLiteral(kNativeHTMLMime)) {
+ // note cf_html uses utf8
+ nsAutoCString cfhtml;
+ if (GetCString(genericDataObj, cfhtml)) {
+ // cfselection left emtpy for now.
+ nsString cfcontext, cffragment, cfselection;
+ nsresult rv = ParseCFHTML(cfhtml, getter_Copies(cffragment),
+ getter_Copies(cfcontext));
+ if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) {
+ // If we have our private HTML flavor, we will only use the fragment
+ // from the CF_HTML. The rest comes from the clipboard.
+ if (aHavePrivateHTMLFlavor == HavePrivateHTMLFlavor::Yes) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertHTMLWithContextAsSubAction(
+ cffragment, aContextStr, aInfoStr, flavor, safeToInsertData,
+ EditorDOMPoint(), DeleteSelectedContent::Yes,
+ InlineStylesAtInsertionPoint::Clear);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "DeleteSelectedContent::Yes, "
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ } else {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertHTMLWithContextAsSubAction(
+ cffragment, cfcontext, cfselection, flavor, safeToInsertData,
+ EditorDOMPoint(), DeleteSelectedContent::Yes,
+ InlineStylesAtInsertionPoint::Clear);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "DeleteSelectedContent::Yes, "
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ }
+ } else {
+ // In some platforms (like Linux), the clipboard might return data
+ // requested for unknown flavors (for example:
+ // application/x-moz-nativehtml). In this case, treat the data
+ // to be pasted as mere HTML to get the best chance of pasting it
+ // correctly.
+ bestFlavor.AssignLiteral(kHTMLMime);
+ // Fall through the next case
+ }
+ }
+ }
+ if (bestFlavor.EqualsLiteral(kHTMLMime) ||
+ bestFlavor.EqualsLiteral(kTextMime) ||
+ bestFlavor.EqualsLiteral(kMozTextInternal)) {
+ nsAutoString stuffToPaste;
+ if (!GetString(genericDataObj, stuffToPaste)) {
+ nsAutoCString text;
+ if (GetCString(genericDataObj, text)) {
+ CopyUTF8toUTF16(text, stuffToPaste);
+ }
+ }
+
+ if (!stuffToPaste.IsEmpty()) {
+ if (bestFlavor.EqualsLiteral(kHTMLMime)) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertHTMLWithContextAsSubAction(
+ stuffToPaste, aContextStr, aInfoStr, flavor, safeToInsertData,
+ EditorDOMPoint(), DeleteSelectedContent::Yes,
+ InlineStylesAtInsertionPoint::Clear);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "DeleteSelectedContent::Yes, "
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ } else {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv =
+ InsertTextAsSubAction(stuffToPaste, SelectionHandling::Delete);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+ }
+ }
+ }
+ }
+ }
+
+ // Try to scroll the selection into view if the paste succeeded
+ DebugOnly<nsresult> rvIgnored = ScrollSelectionFocusIntoView();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::ScrollSelectionFocusIntoView() failed, but ignored");
+ return NS_OK;
+}
+
+static void GetStringFromDataTransfer(const DataTransfer* aDataTransfer,
+ const nsAString& aType, uint32_t aIndex,
+ nsString& aOutputString) {
+ nsCOMPtr<nsIVariant> variant;
+ DebugOnly<nsresult> rvIgnored = aDataTransfer->GetDataAtNoSecurityCheck(
+ aType, aIndex, getter_AddRefs(variant));
+ if (!variant) {
+ MOZ_ASSERT(aOutputString.IsEmpty());
+ return;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "DataTransfer::GetDataAtNoSecurityCheck() failed, but ignored");
+ variant->GetAsAString(aOutputString);
+ nsContentUtils::PlatformToDOMLineBreaks(aOutputString);
+}
+
+nsresult HTMLEditor::InsertFromDataTransfer(
+ const DataTransfer* aDataTransfer, uint32_t aIndex,
+ nsIPrincipal* aSourcePrincipal, const EditorDOMPoint& aDroppedAt,
+ DeleteSelectedContent aDeleteSelectedContent) {
+ MOZ_ASSERT(GetEditAction() == EditAction::eDrop ||
+ GetEditAction() == EditAction::ePaste);
+ MOZ_ASSERT(mPlaceholderBatch,
+ "HTMLEditor::InsertFromDataTransfer() should be called by "
+ "HandleDropEvent() or paste action and there should've already "
+ "been placeholder transaction");
+ MOZ_ASSERT_IF(GetEditAction() == EditAction::eDrop, aDroppedAt.IsSet());
+
+ ErrorResult error;
+ RefPtr<DOMStringList> types = aDataTransfer->MozTypesAt(aIndex, error);
+ if (error.Failed()) {
+ NS_WARNING("DataTransfer::MozTypesAt() failed");
+ return error.StealNSResult();
+ }
+
+ const bool hasPrivateHTMLFlavor =
+ types->Contains(NS_LITERAL_STRING_FROM_CSTRING(kHTMLContext));
+
+ const bool isPlaintextEditor = IsPlaintextMailComposer();
+ const SafeToInsertData safeToInsertData =
+ IsSafeToInsertData(aSourcePrincipal);
+
+ uint32_t length = types->Length();
+ for (uint32_t i = 0; i < length; i++) {
+ nsAutoString type;
+ types->Item(i, type);
+
+ if (!isPlaintextEditor) {
+ if (type.EqualsLiteral(kFileMime) || type.EqualsLiteral(kJPEGImageMime) ||
+ type.EqualsLiteral(kJPGImageMime) ||
+ type.EqualsLiteral(kPNGImageMime) ||
+ type.EqualsLiteral(kGIFImageMime)) {
+ nsCOMPtr<nsIVariant> variant;
+ DebugOnly<nsresult> rvIgnored = aDataTransfer->GetDataAtNoSecurityCheck(
+ type, aIndex, getter_AddRefs(variant));
+ if (variant) {
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "DataTransfer::GetDataAtNoSecurityCheck() failed, but ignored");
+ nsCOMPtr<nsISupports> object;
+ rvIgnored = variant->GetAsISupports(getter_AddRefs(object));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIVariant::GetAsISupports() failed, but ignored");
+ nsresult rv = InsertObject(NS_ConvertUTF16toUTF8(type), object,
+ safeToInsertData, aDroppedAt,
+ aDeleteSelectedContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertObject() failed");
+ return rv;
+ }
+ } else if (type.EqualsLiteral(kNativeHTMLMime)) {
+ // Windows only clipboard parsing.
+ nsAutoString text;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ NS_ConvertUTF16toUTF8 cfhtml(text);
+
+ nsString cfcontext, cffragment,
+ cfselection; // cfselection left emtpy for now
+
+ nsresult rv = ParseCFHTML(cfhtml, getter_Copies(cffragment),
+ getter_Copies(cfcontext));
+ if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) {
+ if (hasPrivateHTMLFlavor) {
+ // If we have our private HTML flavor, we will only use the fragment
+ // from the CF_HTML. The rest comes from the clipboard.
+ nsAutoString contextString, infoString;
+ GetStringFromDataTransfer(
+ aDataTransfer, NS_LITERAL_STRING_FROM_CSTRING(kHTMLContext),
+ aIndex, contextString);
+ GetStringFromDataTransfer(aDataTransfer,
+ NS_LITERAL_STRING_FROM_CSTRING(kHTMLInfo),
+ aIndex, infoString);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertHTMLWithContextAsSubAction(
+ cffragment, contextString, infoString, type, safeToInsertData,
+ aDroppedAt, aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Clear);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertHTMLWithContextAsSubAction(
+ cffragment, cfcontext, cfselection, type, safeToInsertData,
+ aDroppedAt, aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Clear);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ } else if (type.EqualsLiteral(kHTMLMime)) {
+ nsAutoString text, contextString, infoString;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ GetStringFromDataTransfer(aDataTransfer,
+ NS_LITERAL_STRING_FROM_CSTRING(kHTMLContext),
+ aIndex, contextString);
+ GetStringFromDataTransfer(aDataTransfer,
+ NS_LITERAL_STRING_FROM_CSTRING(kHTMLInfo),
+ aIndex, infoString);
+ if (type.EqualsLiteral(kHTMLMime)) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertHTMLWithContextAsSubAction(
+ text, contextString, infoString, type, safeToInsertData,
+ aDroppedAt, aDeleteSelectedContent,
+ InlineStylesAtInsertionPoint::Clear);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertHTMLWithContextAsSubAction("
+ "InlineStylesAtInsertionPoint::Clear) failed");
+ return rv;
+ }
+ }
+ }
+
+ if (type.EqualsLiteral(kTextMime) || type.EqualsLiteral(kMozTextInternal)) {
+ nsAutoString text;
+ GetStringFromDataTransfer(aDataTransfer, type, aIndex, text);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv = InsertTextAt(text, aDroppedAt, aDeleteSelectedContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAt() failed");
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+HTMLEditor::HavePrivateHTMLFlavor HTMLEditor::ClipboardHasPrivateHTMLFlavor(
+ nsIClipboard* aClipboard) {
+ if (NS_WARN_IF(!aClipboard)) {
+ return HavePrivateHTMLFlavor::No;
+ }
+
+ // check the clipboard for our special kHTMLContext flavor. If that is there,
+ // we know we have our own internal html format on clipboard.
+ bool hasPrivateHTMLFlavor = false;
+ AutoTArray<nsCString, 1> flavArray = {nsDependentCString(kHTMLContext)};
+ nsresult rv = aClipboard->HasDataMatchingFlavors(
+ flavArray, nsIClipboard::kGlobalClipboard, &hasPrivateHTMLFlavor);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIClipboard::HasDataMatchingFlavors(nsIClipboard::"
+ "kGlobalClipboard) failed");
+ return NS_SUCCEEDED(rv) && hasPrivateHTMLFlavor ? HavePrivateHTMLFlavor::Yes
+ : HavePrivateHTMLFlavor::No;
+}
+
+nsresult HTMLEditor::HandlePaste(AutoEditActionDataSetter& aEditActionData,
+ int32_t aClipboardType) {
+ aEditActionData.InitializeDataTransferWithClipboard(
+ SettingDataTransfer::eWithFormat, aClipboardType);
+ nsresult rv = aEditActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return rv;
+ }
+ rv = PasteInternal(aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::PasteInternal() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::PasteInternal(int32_t aClipboardType) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Get Clipboard Service
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIClipboard> clipboard =
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return rv;
+ }
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ nsCOMPtr<nsITransferable> transferable;
+ rv = PrepareHTMLTransferable(getter_AddRefs(transferable));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::PrepareHTMLTransferable() failed");
+ return rv;
+ }
+ if (!transferable) {
+ NS_WARNING("HTMLEditor::PrepareHTMLTransferable() returned nullptr");
+ return NS_ERROR_FAILURE;
+ }
+ // Get the Data from the clipboard
+ auto* windowContext = GetDocument()->GetWindowContext();
+ if (!windowContext) {
+ NS_WARNING("No window context");
+ return NS_ERROR_FAILURE;
+ }
+ rv = clipboard->GetData(transferable, aClipboardType, windowContext);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIClipboard::GetData() failed");
+ return rv;
+ }
+
+ // XXX Why don't you check this first?
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ // also get additional html copy hints, if present
+ nsAutoString contextStr, infoStr;
+
+ // If we have our internal html flavor on the clipboard, there is special
+ // context to use instead of cfhtml context.
+ const HavePrivateHTMLFlavor clipboardHasPrivateHTMLFlavor =
+ ClipboardHasPrivateHTMLFlavor(clipboard);
+ if (clipboardHasPrivateHTMLFlavor == HavePrivateHTMLFlavor::Yes) {
+ nsCOMPtr<nsITransferable> contextTransferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ if (!contextTransferable) {
+ NS_WARNING(
+ "do_CreateInstance() failed to create nsITransferable instance");
+ return NS_ERROR_FAILURE;
+ }
+ DebugOnly<nsresult> rvIgnored = contextTransferable->Init(nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::Init() failed, but ignored");
+ contextTransferable->SetIsPrivateData(transferable->GetIsPrivateData());
+ rvIgnored = contextTransferable->AddDataFlavor(kHTMLContext);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kHTMLContext) failed, but ignored");
+ rvIgnored =
+ clipboard->GetData(contextTransferable, aClipboardType, windowContext);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsIClipboard::GetData() failed, but ignored");
+ nsCOMPtr<nsISupports> contextDataObj;
+ rv = contextTransferable->GetTransferData(kHTMLContext,
+ getter_AddRefs(contextDataObj));
+ if (NS_SUCCEEDED(rv) && contextDataObj) {
+ if (nsCOMPtr<nsISupportsString> str = do_QueryInterface(contextDataObj)) {
+ DebugOnly<nsresult> rvIgnored = str->GetData(contextStr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ }
+ }
+
+ nsCOMPtr<nsITransferable> infoTransferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ if (!infoTransferable) {
+ NS_WARNING(
+ "do_CreateInstance() failed to create nsITransferable instance");
+ return NS_ERROR_FAILURE;
+ }
+ rvIgnored = infoTransferable->Init(nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsITransferable::Init() failed, but ignored");
+ contextTransferable->SetIsPrivateData(transferable->GetIsPrivateData());
+ rvIgnored = infoTransferable->AddDataFlavor(kHTMLInfo);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kHTMLInfo) failed, but ignored");
+
+ rvIgnored =
+ clipboard->GetData(infoTransferable, aClipboardType, windowContext);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsIClipboard::GetData() failed, but ignored");
+ nsCOMPtr<nsISupports> infoDataObj;
+ rv = infoTransferable->GetTransferData(kHTMLInfo,
+ getter_AddRefs(infoDataObj));
+ if (NS_SUCCEEDED(rv) && infoDataObj) {
+ if (nsCOMPtr<nsISupportsString> str = do_QueryInterface(infoDataObj)) {
+ DebugOnly<nsresult> rvIgnored = str->GetData(infoStr);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ }
+ }
+ }
+
+ rv = InsertFromTransferableAtSelection(transferable, contextStr, infoStr,
+ clipboardHasPrivateHTMLFlavor);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertFromTransferableAtSelection() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::HandlePasteTransferable(
+ AutoEditActionDataSetter& aEditActionData, nsITransferable& aTransferable) {
+ // InitializeDataTransfer may fetch input stream in aTransferable, so it
+ // may be invalid after calling this.
+ aEditActionData.InitializeDataTransfer(&aTransferable);
+
+ nsresult rv = aEditActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent(), failed");
+ return rv;
+ }
+
+ RefPtr<DataTransfer> dataTransfer = GetInputEventDataTransfer();
+ if (dataTransfer->HasFile() && dataTransfer->MozItemCount() > 0) {
+ // Now aTransferable has moved to DataTransfer. Use DataTransfer.
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ rv = InsertFromDataTransfer(dataTransfer, 0, nullptr, EditorDOMPoint(),
+ DeleteSelectedContent::Yes);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertFromDataTransfer("
+ "DeleteSelectedContent::Yes) failed");
+ return rv;
+ }
+
+ nsAutoString contextStr, infoStr;
+ rv = InsertFromTransferableAtSelection(&aTransferable, contextStr, infoStr,
+ HavePrivateHTMLFlavor::No);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertFromTransferableAtSelection("
+ "HavePrivateHTMLFlavor::No) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::PasteNoFormattingAsAction(
+ int32_t aClipboardType, DispatchPasteEvent aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal) {
+ if (IsReadonly()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::ePaste,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ editActionData.InitializeDataTransferWithClipboard(
+ SettingDataTransfer::eWithoutFormat, aClipboardType);
+
+ if (aDispatchPasteEvent == DispatchPasteEvent::Yes) {
+ RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!focusManager)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();
+
+ Result<ClipboardEventResult, nsresult> ret =
+ DispatchClipboardEventAndUpdateClipboard(ePasteNoFormatting,
+ aClipboardType);
+ if (MOZ_UNLIKELY(ret.isErr())) {
+ NS_WARNING(
+ "EditorBase::DispatchClipboardEventAndUpdateClipboard("
+ "ePasteNoFormatting) failed");
+ return EditorBase::ToGenericNSResult(ret.unwrapErr());
+ }
+ switch (ret.inspect()) {
+ case ClipboardEventResult::DoDefault:
+ break;
+ case ClipboardEventResult::DefaultPreventedOfPaste:
+ case ClipboardEventResult::IgnoredOrError:
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ case ClipboardEventResult::CopyOrCutHandled:
+ MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
+ }
+
+ // If focus is changed by a "paste" event listener, we should keep handling
+ // the "pasting" in new focused editor because Chrome works as so.
+ const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
+ if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
+ // For the privacy reason, let's top handling it if new focused element is
+ // in different document.
+ if (focusManager->GetFocusedWindow() != GetWindow()) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ RefPtr<EditorBase> editorBase =
+ nsContentUtils::GetActiveEditor(GetPresContext());
+ if (!editorBase || (editorBase->IsHTMLEditor() &&
+ !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+ if (editorBase != this) {
+ if (editorBase->IsHTMLEditor()) {
+ nsresult rv =
+ MOZ_KnownLive(editorBase->AsHTMLEditor())
+ ->PasteNoFormattingAsAction(
+ aClipboardType, DispatchPasteEvent::No, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::PasteNoFormattingAsAction("
+ "DispatchPasteEvent::No) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ nsresult rv = editorBase->PasteAsAction(
+ aClipboardType, DispatchPasteEvent::No, aPrincipal);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::PasteAsAction(DispatchPasteEvent::No) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+ }
+
+ // Dispatch "beforeinput" event after "paste" event. And perhaps, before
+ // committing composition because if pasting is canceled, we don't need to
+ // commit the active composition.
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ DebugOnly<nsresult> rvIgnored = CommitComposition();
+ if (NS_WARN_IF(Destroyed())) {
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CommitComposition() failed, but ignored");
+
+ // Get Clipboard Service
+ nsCOMPtr<nsIClipboard> clipboard(
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return rv;
+ }
+
+ if (!GetDocument()) {
+ NS_WARNING("Editor didn't have document, but ignored");
+ return NS_OK;
+ }
+ auto* windowContext = GetDocument()->GetWindowContext();
+ if (!windowContext) {
+ NS_WARNING("Editor didn't have document window context, but ignored");
+ return NS_OK;
+ }
+
+ Result<nsCOMPtr<nsITransferable>, nsresult> maybeTransferable =
+ EditorUtils::CreateTransferableForPlainText(*GetDocument());
+ if (maybeTransferable.isErr()) {
+ NS_WARNING("EditorUtils::CreateTransferableForPlainText() failed");
+ return EditorBase::ToGenericNSResult(maybeTransferable.unwrapErr());
+ }
+ nsCOMPtr<nsITransferable> transferable(maybeTransferable.unwrap());
+ if (!transferable) {
+ NS_WARNING(
+ "EditorUtils::CreateTransferableForPlainText() returned nullptr, but "
+ "ignored");
+ return NS_OK;
+ }
+
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ // Get the Data from the clipboard
+ rv = clipboard->GetData(transferable, aClipboardType, windowContext);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIClipboard::GetData() failed");
+ return rv;
+ }
+
+ rv = InsertFromTransferableAtSelection(transferable, u""_ns, u""_ns,
+ HavePrivateHTMLFlavor::No);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertFromTransferableAtSelection("
+ "HavePrivateHTMLFlavor::No) failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+// The following arrays contain the MIME types that we can paste. The arrays
+// are used by CanPaste() and CanPasteTransferable() below.
+
+static const char* textEditorFlavors[] = {kTextMime};
+static const char* textHtmlEditorFlavors[] = {kTextMime, kHTMLMime,
+ kJPEGImageMime, kJPGImageMime,
+ kPNGImageMime, kGIFImageMime};
+
+bool HTMLEditor::CanPaste(int32_t aClipboardType) const {
+ if (AreClipboardCommandsUnconditionallyEnabled()) {
+ return true;
+ }
+
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ return false;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return false;
+ }
+
+ // Use the flavors depending on the current editor mask
+ if (IsPlaintextMailComposer()) {
+ AutoTArray<nsCString, ArrayLength(textEditorFlavors)> flavors;
+ flavors.AppendElements<const char*>(Span<const char*>(textEditorFlavors));
+ bool haveFlavors;
+ nsresult rv = clipboard->HasDataMatchingFlavors(flavors, aClipboardType,
+ &haveFlavors);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIClipboard::HasDataMatchingFlavors() failed");
+ return NS_SUCCEEDED(rv) && haveFlavors;
+ }
+
+ AutoTArray<nsCString, ArrayLength(textHtmlEditorFlavors)> flavors;
+ flavors.AppendElements<const char*>(Span<const char*>(textHtmlEditorFlavors));
+ bool haveFlavors;
+ rv = clipboard->HasDataMatchingFlavors(flavors, aClipboardType, &haveFlavors);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIClipboard::HasDataMatchingFlavors() failed");
+ return NS_SUCCEEDED(rv) && haveFlavors;
+}
+
+bool HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable) {
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ return false;
+ }
+
+ // If |aTransferable| is null, assume that a paste will succeed.
+ if (!aTransferable) {
+ return true;
+ }
+
+ // Peek in |aTransferable| to see if it contains a supported MIME type.
+
+ // Use the flavors depending on the current editor mask
+ const char** flavors;
+ size_t length;
+ if (IsPlaintextMailComposer()) {
+ flavors = textEditorFlavors;
+ length = ArrayLength(textEditorFlavors);
+ } else {
+ flavors = textHtmlEditorFlavors;
+ length = ArrayLength(textHtmlEditorFlavors);
+ }
+
+ for (size_t i = 0; i < length; i++, flavors++) {
+ nsCOMPtr<nsISupports> data;
+ nsresult rv =
+ aTransferable->GetTransferData(*flavors, getter_AddRefs(data));
+ if (NS_SUCCEEDED(rv) && data) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+nsresult HTMLEditor::HandlePasteAsQuotation(
+ AutoEditActionDataSetter& aEditActionData, int32_t aClipboardType) {
+ MOZ_ASSERT(aClipboardType == nsIClipboard::kGlobalClipboard ||
+ aClipboardType == nsIClipboard::kSelectionClipboard);
+ aEditActionData.InitializeDataTransferWithClipboard(
+ SettingDataTransfer::eWithFormat, aClipboardType);
+ if (NS_WARN_IF(!aEditActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = aEditActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent(), failed");
+ return rv;
+ }
+
+ if (IsPlaintextMailComposer()) {
+ nsresult rv = PasteAsPlaintextQuotation(aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::PasteAsPlaintextQuotation() failed");
+ return rv;
+ }
+
+ // If it's not in plain text edit mode, paste text into new
+ // <blockquote type="cite"> element after removing selection.
+
+ {
+ // XXX Why don't we test these first?
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertQuotation, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ // Remove Selection and create `<blockquote type="cite">` now.
+ // XXX Why don't we insert the `<blockquote>` into the DOM tree after
+ // pasting the content in clipboard into it?
+ Result<RefPtr<Element>, nsresult> blockquoteElementOrError =
+ DeleteSelectionAndCreateElement(
+ *nsGkAtoms::blockquote,
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [](HTMLEditor&, Element& aBlockquoteElement, const EditorDOMPoint&)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ DebugOnly<nsresult> rvIgnored = aBlockquoteElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::type, u"cite"_ns,
+ aBlockquoteElement.IsInComposedDoc());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ nsPrintfCString(
+ "Element::SetAttr(nsGkAtoms::type, \"cite\", %s) "
+ "failed, but ignored",
+ aBlockquoteElement.IsInComposedDoc() ? "true" : "false")
+ .get());
+ return NS_OK;
+ });
+ if (MOZ_UNLIKELY(blockquoteElementOrError.isErr()) ||
+ NS_WARN_IF(Destroyed())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteSelectionAndCreateElement(nsGkAtoms::blockquote) "
+ "failed");
+ return Destroyed() ? NS_ERROR_EDITOR_DESTROYED
+ : blockquoteElementOrError.unwrapErr();
+ }
+ MOZ_ASSERT(blockquoteElementOrError.inspect());
+
+ // Collapse Selection in the new `<blockquote>` element.
+ rv = CollapseSelectionToStartOf(
+ MOZ_KnownLive(*blockquoteElementOrError.inspect()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionToStartOf() failed");
+ return rv;
+ }
+
+ rv = PasteInternal(aClipboardType);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::PasteInternal() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::PasteAsPlaintextQuotation(int32_t aSelectionType) {
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard =
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return rv;
+ }
+
+ // Create generic Transferable for getting the data
+ nsCOMPtr<nsITransferable> transferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("do_CreateInstance() failed to create nsITransferable instance");
+ return rv;
+ }
+ if (!transferable) {
+ NS_WARNING("do_CreateInstance() returned nullptr");
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Document> destdoc = GetDocument();
+ auto* windowContext = GetDocument()->GetWindowContext();
+ if (!windowContext) {
+ NS_WARNING("Editor didn't have document window context");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsILoadContext* loadContext = destdoc ? destdoc->GetLoadContext() : nullptr;
+ DebugOnly<nsresult> rvIgnored = transferable->Init(loadContext);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::Init() failed, but ignored");
+
+ // We only handle plaintext pastes here
+ rvIgnored = transferable->AddDataFlavor(kTextMime);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsITransferable::AddDataFlavor(kTextMime) failed, but ignored");
+
+ // Get the Data from the clipboard
+ rvIgnored = clipboard->GetData(transferable, aSelectionType, windowContext);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsIClipboard::GetData() failed, but ignored");
+
+ // Now we ask the transferable for the data
+ // it still owns the data, we just have a pointer to it.
+ // If it can't support a "text" output of the data the call will fail
+ nsCOMPtr<nsISupports> genericDataObj;
+ nsAutoCString flavor;
+ rv = transferable->GetAnyTransferData(flavor, getter_AddRefs(genericDataObj));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsITransferable::GetAnyTransferData() failed");
+ return rv;
+ }
+
+ if (!flavor.EqualsLiteral(kTextMime)) {
+ return NS_OK;
+ }
+
+ nsAutoString stuffToPaste;
+ if (!GetString(genericDataObj, stuffToPaste)) {
+ return NS_OK;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertAsPlaintextQuotation(stuffToPaste, true, 0);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsPlaintextQuotation() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::InsertWithQuotationsAsSubAction(
+ const nsAString& aQuotedText) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ // Let the citer quote it for us:
+ nsString quotedStuff;
+ InternetCiter::GetCiteString(aQuotedText, quotedStuff);
+
+ // It's best to put a blank line after the quoted text so that mails
+ // written without thinking won't be so ugly.
+ if (!aQuotedText.IsEmpty() &&
+ (aQuotedText.Last() != HTMLEditUtils::kNewLine)) {
+ quotedStuff.Append(HTMLEditUtils::kNewLine);
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ rv = InsertTextAsSubAction(quotedStuff, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::InsertTextWithQuotations(
+ const nsAString& aStringToInsert) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText);
+ MOZ_ASSERT(!aStringToInsert.IsVoid());
+ editActionData.SetData(aStringToInsert);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (aStringToInsert.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // The whole operation should be undoable in one transaction:
+ // XXX Why isn't enough to use only AutoPlaceholderBatch here?
+ AutoTransactionBatch bundleAllTransactions(*this, __FUNCTION__);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ rv = InsertTextWithQuotationsInternal(aStringToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertTextWithQuotationsInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::InsertTextWithQuotationsInternal(
+ const nsAString& aStringToInsert) {
+ MOZ_ASSERT(!aStringToInsert.IsEmpty());
+ // We're going to loop over the string, collecting up a "hunk"
+ // that's all the same type (quoted or not),
+ // Whenever the quotedness changes (or we reach the string's end)
+ // we will insert the hunk all at once, quoted or non.
+ static const char16_t cite('>');
+ bool curHunkIsQuoted = (aStringToInsert.First() == cite);
+
+ nsAString::const_iterator hunkStart, strEnd;
+ aStringToInsert.BeginReading(hunkStart);
+ aStringToInsert.EndReading(strEnd);
+
+ // In the loop below, we only look for DOM newlines (\n),
+ // because we don't have a FindChars method that can look
+ // for both \r and \n. \r is illegal in the dom anyway,
+ // but in debug builds, let's take the time to verify that
+ // there aren't any there:
+#ifdef DEBUG
+ nsAString::const_iterator dbgStart(hunkStart);
+ if (FindCharInReadable(HTMLEditUtils::kCarriageReturn, dbgStart, strEnd)) {
+ NS_ASSERTION(
+ false,
+ "Return characters in DOM! InsertTextWithQuotations may be wrong");
+ }
+#endif /* DEBUG */
+
+ // Loop over lines:
+ nsresult rv = NS_OK;
+ nsAString::const_iterator lineStart(hunkStart);
+ // We will break from inside when we run out of newlines.
+ for (;;) {
+ // Search for the end of this line (dom newlines, see above):
+ bool found = FindCharInReadable(HTMLEditUtils::kNewLine, lineStart, strEnd);
+ bool quoted = false;
+ if (found) {
+ // if there's another newline, lineStart now points there.
+ // Loop over any consecutive newline chars:
+ nsAString::const_iterator firstNewline(lineStart);
+ while (*lineStart == HTMLEditUtils::kNewLine) {
+ ++lineStart;
+ }
+ quoted = (*lineStart == cite);
+ if (quoted == curHunkIsQuoted) {
+ continue;
+ }
+ // else we're changing state, so we need to insert
+ // from curHunk to lineStart then loop around.
+
+ // But if the current hunk is quoted, then we want to make sure
+ // that any extra newlines on the end do not get included in
+ // the quoted section: blank lines flaking a quoted section
+ // should be considered unquoted, so that if the user clicks
+ // there and starts typing, the new text will be outside of
+ // the quoted block.
+ if (curHunkIsQuoted) {
+ lineStart = firstNewline;
+
+ // 'firstNewline' points to the first '\n'. We want to
+ // ensure that this first newline goes into the hunk
+ // since quoted hunks can be displayed as blocks
+ // (and the newline should become invisible in this case).
+ // So the next line needs to start at the next character.
+ lineStart++;
+ }
+ }
+
+ // If no newline found, lineStart is now strEnd and we can finish up,
+ // inserting from curHunk to lineStart then returning.
+ const nsAString& curHunk = Substring(hunkStart, lineStart);
+ nsCOMPtr<nsINode> dummyNode;
+ if (curHunkIsQuoted) {
+ rv =
+ InsertAsPlaintextQuotation(curHunk, false, getter_AddRefs(dummyNode));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsPlaintextQuotation() failed, "
+ "but might be ignored");
+ } else {
+ rv = InsertTextAsSubAction(curHunk, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed, but might be ignored");
+ }
+ if (!found) {
+ break;
+ }
+ curHunkIsQuoted = quoted;
+ hunkStart = lineStart;
+ }
+
+ // XXX This returns the last result of InsertAsPlaintextQuotation() or
+ // InsertTextAsSubAction() in the loop. This must be a bug.
+ return rv;
+}
+
+nsresult HTMLEditor::InsertAsQuotation(const nsAString& aQuotedText,
+ nsINode** aNodeInserted) {
+ if (IsPlaintextMailComposer()) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText);
+ MOZ_ASSERT(!aQuotedText.IsVoid());
+ editActionData.SetData(aQuotedText);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsPlaintextQuotation() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertBlockquoteElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsAutoString citation;
+ rv = InsertAsCitedQuotationInternal(aQuotedText, citation, false,
+ aNodeInserted);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsCitedQuotationInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+// Insert plaintext as a quotation, with cite marks (e.g. "> ").
+// This differs from its corresponding method in TextEditor
+// in that here, quoted material is enclosed in a <pre> tag
+// in order to preserve the original line wrapping.
+nsresult HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText,
+ bool aAddCites,
+ nsINode** aNodeInserted) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (aNodeInserted) {
+ *aNodeInserted = nullptr;
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertQuotation, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ // Wrap the inserted quote in a <span> so we can distinguish it. If we're
+ // inserting into the <body>, we use a <span> which is displayed as a block
+ // and sized to the screen using 98 viewport width units.
+ // We could use 100vw, but 98vw avoids a horizontal scroll bar where possible.
+ // All this is done to wrap overlong lines to the screen and not to the
+ // container element, the width-restricted body.
+ Result<RefPtr<Element>, nsresult> spanElementOrError =
+ DeleteSelectionAndCreateElement(
+ *nsGkAtoms::span, [](HTMLEditor&, Element& aSpanElement,
+ const EditorDOMPoint& aPointToInsert) {
+ // Add an attribute on the pre node so we'll know it's a quotation.
+ DebugOnly<nsresult> rvIgnored = aSpanElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::mozquote, u"true"_ns,
+ aSpanElement.IsInComposedDoc());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ nsPrintfCString(
+ "Element::SetAttr(nsGkAtoms::mozquote, \"true\", %s) "
+ "failed",
+ aSpanElement.IsInComposedDoc() ? "true" : "false")
+ .get());
+ // Allow wrapping on spans so long lines get wrapped to the screen.
+ if (aPointToInsert.IsContainerHTMLElement(nsGkAtoms::body)) {
+ DebugOnly<nsresult> rvIgnored = aSpanElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::style,
+ nsLiteralString(u"white-space: pre-wrap; display: block; "
+ u"width: 98vw;"),
+ false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::style, \"pre-wrap, block\", "
+ "false) failed, but ignored");
+ } else {
+ DebugOnly<nsresult> rvIgnored =
+ aSpanElement.SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+ u"white-space: pre-wrap;"_ns, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::style, "
+ "\"pre-wrap\", false) failed, but ignored");
+ }
+ return NS_OK;
+ });
+ NS_WARNING_ASSERTION(spanElementOrError.isOk(),
+ "HTMLEditor::DeleteSelectionAndCreateElement(nsGkAtoms::"
+ "span) failed, but ignored");
+
+ // If this succeeded, then set selection inside the pre
+ // so the inserted text will end up there.
+ // If it failed, we don't care what the return value was,
+ // but we'll fall through and try to insert the text anyway.
+ if (spanElementOrError.isOk()) {
+ MOZ_ASSERT(spanElementOrError.inspect());
+ rv = CollapseSelectionToStartOf(
+ MOZ_KnownLive(*spanElementOrError.inspect()));
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionToStartOf() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+ }
+
+ // TODO: We should insert text at specific point rather than at selection.
+ // Then, we can do this before inserting the <span> element.
+ if (aAddCites) {
+ rv = InsertWithQuotationsAsSubAction(aQuotedText);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::InsertWithQuotationsAsSubAction() failed");
+ return rv;
+ }
+ } else {
+ rv = InsertTextAsSubAction(aQuotedText, SelectionHandling::Delete);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+ }
+ }
+
+ // XXX Why don't we check this before inserting the quoted text?
+ if (spanElementOrError.isErr()) {
+ return NS_OK;
+ }
+
+ // Set the selection to just after the inserted node:
+ EditorRawDOMPoint afterNewSpanElement(
+ EditorRawDOMPoint::After(*spanElementOrError.inspect()));
+ NS_WARNING_ASSERTION(
+ afterNewSpanElement.IsSet(),
+ "Failed to set after the new <span> element, but ignored");
+ if (afterNewSpanElement.IsSet()) {
+ nsresult rv = CollapseSelectionTo(afterNewSpanElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+
+ // Note that if !aAddCites, aNodeInserted isn't set.
+ // That's okay because the routines that use aAddCites
+ // don't need to know the inserted node.
+ if (aNodeInserted) {
+ spanElementOrError.unwrap().forget(aNodeInserted);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::Rewrap(bool aRespectNewlines) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eRewrap);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Rewrap makes no sense if there's no wrap column; default to 72.
+ int32_t wrapWidth = WrapWidth();
+ if (wrapWidth <= 0) {
+ wrapWidth = 72;
+ }
+
+ nsAutoString current;
+ const bool isCollapsed = SelectionRef().IsCollapsed();
+ uint32_t flags = nsIDocumentEncoder::OutputFormatted |
+ nsIDocumentEncoder::OutputLFLineBreak;
+ if (!isCollapsed) {
+ flags |= nsIDocumentEncoder::OutputSelectionOnly;
+ }
+ rv = ComputeValueInternal(u"text/plain"_ns, flags, current);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::ComputeValueInternal(text/plain) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (current.IsEmpty()) {
+ return NS_OK;
+ }
+
+ nsString wrapped;
+ uint32_t firstLineOffset = 0; // XXX need to reset this if there is a
+ // selection
+ InternetCiter::Rewrap(current, wrapWidth, firstLineOffset, aRespectNewlines,
+ wrapped);
+
+ if (wrapped.IsEmpty()) {
+ return NS_OK;
+ }
+
+ if (isCollapsed) {
+ DebugOnly<nsresult> rvIgnored = SelectAllInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SelectAllInternal() failed");
+ }
+
+ // The whole operation in InsertTextWithQuotationsInternal() should be
+ // undoable in one transaction.
+ // XXX Why isn't enough to use only AutoPlaceholderBatch here?
+ AutoTransactionBatch bundleAllTransactions(*this, __FUNCTION__);
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertTextWithQuotationsInternal(wrapped);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertTextWithQuotationsInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::InsertAsCitedQuotation(const nsAString& aQuotedText,
+ const nsAString& aCitation,
+ bool aInsertHTML,
+ nsINode** aNodeInserted) {
+ // Don't let anyone insert HTML when we're in plaintext mode.
+ if (IsPlaintextMailComposer()) {
+ NS_ASSERTION(
+ !aInsertHTML,
+ "InsertAsCitedQuotation: trying to insert html into plaintext editor");
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText);
+ MOZ_ASSERT(!aQuotedText.IsVoid());
+ editActionData.SetData(aQuotedText);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsPlaintextQuotation() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertBlockquoteElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertAsCitedQuotationInternal(aQuotedText, aCitation, aInsertHTML,
+ aNodeInserted);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertAsCitedQuotationInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::InsertAsCitedQuotationInternal(
+ const nsAString& aQuotedText, const nsAString& aCitation, bool aInsertHTML,
+ nsINode** aNodeInserted) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsPlaintextMailComposer());
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertQuotation, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ Result<RefPtr<Element>, nsresult> blockquoteElementOrError =
+ DeleteSelectionAndCreateElement(
+ *nsGkAtoms::blockquote,
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [&aCitation](HTMLEditor&, Element& aBlockquoteElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ // Try to set type=cite. Ignore it if this fails.
+ DebugOnly<nsresult> rvIgnored = aBlockquoteElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::type, u"cite"_ns,
+ aBlockquoteElement.IsInComposedDoc());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ nsPrintfCString(
+ "Element::SetAttr(nsGkAtoms::type, \"cite\", %s) failed, "
+ "but ignored",
+ aBlockquoteElement.IsInComposedDoc() ? "true" : "false")
+ .get());
+ if (!aCitation.IsEmpty()) {
+ DebugOnly<nsresult> rvIgnored = aBlockquoteElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::cite, aCitation,
+ aBlockquoteElement.IsInComposedDoc());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ nsPrintfCString(
+ "Element::SetAttr(nsGkAtoms::cite, \"...\", %s) failed, "
+ "but ignored",
+ aBlockquoteElement.IsInComposedDoc() ? "true" : "false")
+ .get());
+ }
+ return NS_OK;
+ });
+ if (MOZ_UNLIKELY(blockquoteElementOrError.isErr() ||
+ NS_WARN_IF(Destroyed()))) {
+ NS_WARNING(
+ "HTMLEditor::DeleteSelectionAndCreateElement(nsGkAtoms::blockquote) "
+ "failed");
+ return Destroyed() ? NS_ERROR_EDITOR_DESTROYED
+ : blockquoteElementOrError.unwrapErr();
+ }
+ MOZ_ASSERT(blockquoteElementOrError.inspect());
+
+ // Set the selection inside the blockquote so aQuotedText will go there:
+ rv = CollapseSelectionTo(
+ EditorRawDOMPoint(blockquoteElementOrError.inspect(), 0u));
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+
+ // TODO: We should insert text at specific point rather than at selection.
+ // Then, we can do this before inserting the <blockquote> element.
+ if (aInsertHTML) {
+ rv = LoadHTML(aQuotedText);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::LoadHTML() failed");
+ return rv;
+ }
+ } else {
+ rv = InsertTextAsSubAction(
+ aQuotedText, SelectionHandling::Delete); // XXX ignore charset
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::LoadHTML() failed");
+ return rv;
+ }
+ }
+
+ // Set the selection to just after the inserted node:
+ EditorRawDOMPoint afterNewBlockquoteElement(
+ EditorRawDOMPoint::After(blockquoteElementOrError.inspect()));
+ NS_WARNING_ASSERTION(
+ afterNewBlockquoteElement.IsSet(),
+ "Failed to set after new <blockquote> element, but ignored");
+ if (afterNewBlockquoteElement.IsSet()) {
+ nsresult rv = CollapseSelectionTo(afterNewBlockquoteElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+
+ if (aNodeInserted) {
+ blockquoteElementOrError.unwrap().forget(aNodeInserted);
+ }
+
+ return NS_OK;
+}
+
+void HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ RemoveHeadChildAndStealBodyChildsChildren(nsINode& aNode) {
+ nsCOMPtr<nsIContent> body, head;
+ // find the body and head nodes if any.
+ // look only at immediate children of aNode.
+ for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::body)) {
+ body = child;
+ } else if (child->IsHTMLElement(nsGkAtoms::head)) {
+ head = child;
+ }
+ }
+ if (head) {
+ ErrorResult ignored;
+ aNode.RemoveChild(*head, ignored);
+ }
+ if (body) {
+ nsCOMPtr<nsIContent> child = body->GetFirstChild();
+ while (child) {
+ ErrorResult ignored;
+ aNode.InsertBefore(*child, body, ignored);
+ child = body->GetFirstChild();
+ }
+
+ ErrorResult ignored;
+ aNode.RemoveChild(*body, ignored);
+ }
+}
+
+// static
+void HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ RemoveIncompleteDescendantsFromInsertingFragment(nsINode& aNode) {
+ nsIContent* child = aNode.GetFirstChild();
+ while (child) {
+ bool isEmptyNodeShouldNotInserted = false;
+ if (HTMLEditUtils::IsAnyListElement(child)) {
+ // Current limitation of HTMLEditor:
+ // Cannot put caret in a list element which does not have list item
+ // element even as a descendant. I.e., HTMLEditor does not support
+ // editing in such empty list element, and does not support to delete
+ // it from outside. Therefore, HTMLWithContextInserter should not
+ // insert empty list element.
+ isEmptyNodeShouldNotInserted = HTMLEditUtils::IsEmptyNode(
+ *child,
+ {
+ // Although we don't check relation between list item element
+ // and parent list element, but it should not be a problem in the
+ // wild because appearing such invalid list element is an edge
+ // case and anyway HTMLEditor supports editing in them.
+ EmptyCheckOption::TreatListItemAsVisible,
+ // A non-editable list item element may make the list element
+ // visible. Although HTMLEditor does not support to edit list
+ // elements which have only non-editable list item elements, but
+ // it should be deleted from outside. Therefore, don't treat
+ // non-editable things as invisible.
+ // TODO: Currently, HTMLEditor does not support deleting such list
+ // element with Backspace. We should fix this issue.
+ });
+ }
+ // TODO: Perhaps, we should delete <table>s if they have no <td>/<th>
+ // element, or something other elements which must have specific
+ // children but they don't.
+ if (isEmptyNodeShouldNotInserted) {
+ nsIContent* nextChild = child->GetNextSibling();
+ OwningNonNull<nsIContent> removingChild(*child);
+ removingChild->Remove();
+ child = nextChild;
+ continue;
+ }
+ if (child->HasChildNodes()) {
+ RemoveIncompleteDescendantsFromInsertingFragment(*child);
+ }
+ child = child->GetNextSibling();
+ }
+}
+
+// static
+bool HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ IsInsertionCookie(const nsIContent& aContent) {
+ // Is this child the magical cookie?
+ if (const auto* comment = Comment::FromNode(&aContent)) {
+ nsAutoString data;
+ comment->GetData(data);
+
+ return data.EqualsLiteral(kInsertCookie);
+ }
+
+ return false;
+}
+
+/**
+ * This function finds the target node that we will be pasting into. aStart is
+ * the context that we're given and aResult will be the target. Initially,
+ * *aResult must be nullptr.
+ *
+ * The target for a paste is found by either finding the node that contains
+ * the magical comment node containing kInsertCookie or, failing that, the
+ * firstChild of the firstChild (until we reach a leaf).
+ */
+bool HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ FindTargetNodeOfContextForPastedHTMLAndRemoveInsertionCookie(
+ nsINode& aStart, nsCOMPtr<nsINode>& aResult) {
+ nsIContent* firstChild = aStart.GetFirstChild();
+ if (!firstChild) {
+ // If the current result is nullptr, then aStart is a leaf, and is the
+ // fallback result.
+ if (!aResult) {
+ aResult = &aStart;
+ }
+ return false;
+ }
+
+ for (nsCOMPtr<nsIContent> child = firstChild; child;
+ child = child->GetNextSibling()) {
+ if (FragmentFromPasteCreator::IsInsertionCookie(*child)) {
+ // Yes it is! Return an error so we bubble out and short-circuit the
+ // search.
+ aResult = &aStart;
+
+ child->Remove();
+
+ return true;
+ }
+
+ if (FindTargetNodeOfContextForPastedHTMLAndRemoveInsertionCookie(*child,
+ aResult)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+class MOZ_STACK_CLASS HTMLEditor::HTMLWithContextInserter::FragmentParser
+ final {
+ public:
+ FragmentParser(const Document& aDocument, SafeToInsertData aSafeToInsertData);
+
+ [[nodiscard]] nsresult ParseContext(const nsAString& aContextString,
+ DocumentFragment** aFragment);
+
+ [[nodiscard]] nsresult ParsePastedHTML(const nsAString& aInputString,
+ nsAtom* aContextLocalNameAtom,
+ DocumentFragment** aFragment);
+
+ private:
+ static nsresult ParseFragment(const nsAString& aStr,
+ nsAtom* aContextLocalName,
+ const Document* aTargetDoc,
+ dom::DocumentFragment** aFragment,
+ SafeToInsertData aSafeToInsertData);
+
+ const Document& mDocument;
+ const SafeToInsertData mSafeToInsertData;
+};
+
+HTMLEditor::HTMLWithContextInserter::FragmentParser::FragmentParser(
+ const Document& aDocument, SafeToInsertData aSafeToInsertData)
+ : mDocument{aDocument}, mSafeToInsertData{aSafeToInsertData} {}
+
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentParser::ParseContext(
+ const nsAString& aContextStr, DocumentFragment** aFragment) {
+ return FragmentParser::ParseFragment(aContextStr, nullptr, &mDocument,
+ aFragment, mSafeToInsertData);
+}
+
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentParser::ParsePastedHTML(
+ const nsAString& aInputString, nsAtom* aContextLocalNameAtom,
+ DocumentFragment** aFragment) {
+ return FragmentParser::ParseFragment(aInputString, aContextLocalNameAtom,
+ &mDocument, aFragment,
+ mSafeToInsertData);
+}
+
+nsresult HTMLEditor::HTMLWithContextInserter::CreateDOMFragmentFromPaste(
+ const nsAString& aInputString, const nsAString& aContextStr,
+ const nsAString& aInfoStr, nsCOMPtr<nsINode>* aOutFragNode,
+ nsCOMPtr<nsINode>* aOutStartNode, nsCOMPtr<nsINode>* aOutEndNode,
+ uint32_t* aOutStartOffset, uint32_t* aOutEndOffset,
+ SafeToInsertData aSafeToInsertData) const {
+ if (NS_WARN_IF(!aOutFragNode) || NS_WARN_IF(!aOutStartNode) ||
+ NS_WARN_IF(!aOutEndNode) || NS_WARN_IF(!aOutStartOffset) ||
+ NS_WARN_IF(!aOutEndOffset)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<const Document> document = mHTMLEditor.GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ FragmentFromPasteCreator fragmentFromPasteCreator;
+
+ const nsresult rv = fragmentFromPasteCreator.Run(
+ *document, aInputString, aContextStr, aInfoStr, aOutFragNode,
+ aOutStartNode, aOutEndNode, aSafeToInsertData);
+
+ *aOutStartOffset = 0;
+ *aOutEndOffset = (*aOutEndNode)->Length();
+
+ return rv;
+}
+
+// static
+nsAtom* HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ DetermineContextLocalNameForParsingPastedHTML(
+ const nsIContent* aParentContentOfPastedHTMLInContext) {
+ if (!aParentContentOfPastedHTMLInContext) {
+ return nsGkAtoms::body;
+ }
+
+ nsAtom* contextLocalNameAtom =
+ aParentContentOfPastedHTMLInContext->NodeInfo()->NameAtom();
+
+ return (aParentContentOfPastedHTMLInContext->IsHTMLElement(nsGkAtoms::html))
+ ? nsGkAtoms::body
+ : contextLocalNameAtom;
+}
+
+// static
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ MergeAndPostProcessFragmentsForPastedHTMLAndContext(
+ DocumentFragment& aDocumentFragmentForPastedHTML,
+ DocumentFragment& aDocumentFragmentForContext,
+ nsIContent& aTargetContentOfContextForPastedHTML) {
+ FragmentFromPasteCreator::RemoveHeadChildAndStealBodyChildsChildren(
+ aDocumentFragmentForPastedHTML);
+
+ FragmentFromPasteCreator::RemoveIncompleteDescendantsFromInsertingFragment(
+ aDocumentFragmentForPastedHTML);
+
+ // unite the two trees
+ IgnoredErrorResult ignoredError;
+ aTargetContentOfContextForPastedHTML.AppendChild(
+ aDocumentFragmentForPastedHTML, ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "nsINode::AppendChild() failed, but ignored");
+ const nsresult rv = FragmentFromPasteCreator::
+ RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces(
+ aDocumentFragmentForContext, NodesToRemove::eOnlyListItems);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces()"
+ " failed");
+ return rv;
+ }
+
+ return rv;
+}
+
+// static
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ PostProcessFragmentForPastedHTMLWithoutContext(
+ DocumentFragment& aDocumentFragmentForPastedHTML) {
+ FragmentFromPasteCreator::RemoveHeadChildAndStealBodyChildsChildren(
+ aDocumentFragmentForPastedHTML);
+
+ FragmentFromPasteCreator::RemoveIncompleteDescendantsFromInsertingFragment(
+ aDocumentFragmentForPastedHTML);
+
+ const nsresult rv = FragmentFromPasteCreator::
+ RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces(
+ aDocumentFragmentForPastedHTML, NodesToRemove::eOnlyListItems);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces() "
+ "failed");
+ return rv;
+ }
+
+ return rv;
+}
+
+// static
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ PreProcessContextDocumentFragmentForMerging(
+ DocumentFragment& aDocumentFragmentForContext) {
+ // The context is expected to contain text nodes only in block level
+ // elements. Hence, if they contain only whitespace, they're invisible.
+ const nsresult rv = FragmentFromPasteCreator::
+ RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces(
+ aDocumentFragmentForContext, NodesToRemove::eAll);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "RemoveNonPreWhiteSpaceOnlyTextNodesForIgnoringInvisibleWhiteSpaces() "
+ "failed");
+ return rv;
+ }
+
+ FragmentFromPasteCreator::RemoveHeadChildAndStealBodyChildsChildren(
+ aDocumentFragmentForContext);
+
+ return rv;
+}
+
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ CreateDocumentFragmentAndGetParentOfPastedHTMLInContext(
+ const Document& aDocument, const nsAString& aInputString,
+ const nsAString& aContextStr, SafeToInsertData aSafeToInsertData,
+ nsCOMPtr<nsINode>& aParentNodeOfPastedHTMLInContext,
+ RefPtr<DocumentFragment>& aDocumentFragmentToInsert) const {
+ // if we have context info, create a fragment for that
+ RefPtr<DocumentFragment> documentFragmentForContext;
+
+ FragmentParser fragmentParser{aDocument, aSafeToInsertData};
+ if (!aContextStr.IsEmpty()) {
+ nsresult rv = fragmentParser.ParseContext(
+ aContextStr, getter_AddRefs(documentFragmentForContext));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentParser::ParseContext() "
+ "failed");
+ return rv;
+ }
+ if (!documentFragmentForContext) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentParser::ParseContext() "
+ "returned nullptr");
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = FragmentFromPasteCreator::PreProcessContextDocumentFragmentForMerging(
+ *documentFragmentForContext);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "PreProcessContextDocumentFragmentForMerging() failed.");
+ return rv;
+ }
+
+ FragmentFromPasteCreator::
+ FindTargetNodeOfContextForPastedHTMLAndRemoveInsertionCookie(
+ *documentFragmentForContext, aParentNodeOfPastedHTMLInContext);
+ MOZ_ASSERT(aParentNodeOfPastedHTMLInContext);
+ }
+
+ nsCOMPtr<nsIContent> parentContentOfPastedHTMLInContext =
+ nsIContent::FromNodeOrNull(aParentNodeOfPastedHTMLInContext);
+ MOZ_ASSERT_IF(aParentNodeOfPastedHTMLInContext,
+ parentContentOfPastedHTMLInContext);
+
+ nsAtom* contextLocalNameAtom =
+ FragmentFromPasteCreator::DetermineContextLocalNameForParsingPastedHTML(
+ parentContentOfPastedHTMLInContext);
+ RefPtr<DocumentFragment> documentFragmentForPastedHTML;
+ nsresult rv = fragmentParser.ParsePastedHTML(
+ aInputString, contextLocalNameAtom,
+ getter_AddRefs(documentFragmentForPastedHTML));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentParser::ParsePastedHTML()"
+ " failed");
+ return rv;
+ }
+ if (!documentFragmentForPastedHTML) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentParser::ParsePastedHTML()"
+ " returned nullptr");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aParentNodeOfPastedHTMLInContext) {
+ const nsresult rv = FragmentFromPasteCreator::
+ MergeAndPostProcessFragmentsForPastedHTMLAndContext(
+ *documentFragmentForPastedHTML, *documentFragmentForContext,
+ *parentContentOfPastedHTMLInContext);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "MergeAndPostProcessFragmentsForPastedHTMLAndContext() failed.");
+ return rv;
+ }
+ aDocumentFragmentToInsert = std::move(documentFragmentForContext);
+ } else {
+ const nsresult rv = PostProcessFragmentForPastedHTMLWithoutContext(
+ *documentFragmentForPastedHTML);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "PostProcessFragmentForPastedHTMLWithoutContext() failed.");
+ return rv;
+ }
+
+ aDocumentFragmentToInsert = std::move(documentFragmentForPastedHTML);
+ }
+
+ return rv;
+}
+
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::Run(
+ const Document& aDocument, const nsAString& aInputString,
+ const nsAString& aContextStr, const nsAString& aInfoStr,
+ nsCOMPtr<nsINode>* aOutFragNode, nsCOMPtr<nsINode>* aOutStartNode,
+ nsCOMPtr<nsINode>* aOutEndNode, SafeToInsertData aSafeToInsertData) const {
+ MOZ_ASSERT(aOutFragNode);
+ MOZ_ASSERT(aOutStartNode);
+ MOZ_ASSERT(aOutEndNode);
+
+ nsCOMPtr<nsINode> parentNodeOfPastedHTMLInContext;
+ RefPtr<DocumentFragment> documentFragmentToInsert;
+ nsresult rv = CreateDocumentFragmentAndGetParentOfPastedHTMLInContext(
+ aDocument, aInputString, aContextStr, aSafeToInsertData,
+ parentNodeOfPastedHTMLInContext, documentFragmentToInsert);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "CreateDocumentFragmentAndGetParentOfPastedHTMLInContext() failed.");
+ return rv;
+ }
+
+ // If there was no context, then treat all of the data we did get as the
+ // pasted data.
+ if (parentNodeOfPastedHTMLInContext) {
+ *aOutEndNode = *aOutStartNode = parentNodeOfPastedHTMLInContext;
+ } else {
+ *aOutEndNode = *aOutStartNode = documentFragmentToInsert;
+ }
+
+ *aOutFragNode = std::move(documentFragmentToInsert);
+
+ if (!aInfoStr.IsEmpty()) {
+ const nsresult rv =
+ FragmentFromPasteCreator::MoveStartAndEndAccordingToHTMLInfo(
+ aInfoStr, aOutStartNode, aOutEndNode);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::"
+ "MoveStartAndEndAccordingToHTMLInfo() failed");
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator::
+ MoveStartAndEndAccordingToHTMLInfo(const nsAString& aInfoStr,
+ nsCOMPtr<nsINode>* aOutStartNode,
+ nsCOMPtr<nsINode>* aOutEndNode) {
+ int32_t sep = aInfoStr.FindChar((char16_t)',');
+ nsAutoString numstr1(Substring(aInfoStr, 0, sep));
+ nsAutoString numstr2(
+ Substring(aInfoStr, sep + 1, aInfoStr.Length() - (sep + 1)));
+
+ // Move the start and end children.
+ nsresult rvIgnored;
+ int32_t num = numstr1.ToInteger(&rvIgnored);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsAString::ToInteger() failed, but ignored");
+ while (num--) {
+ nsINode* tmp = (*aOutStartNode)->GetFirstChild();
+ if (!tmp) {
+ NS_WARNING("aOutStartNode did not have children");
+ return NS_ERROR_FAILURE;
+ }
+ *aOutStartNode = tmp;
+ }
+
+ num = numstr2.ToInteger(&rvIgnored);
+ while (num--) {
+ nsINode* tmp = (*aOutEndNode)->GetLastChild();
+ if (!tmp) {
+ NS_WARNING("aOutEndNode did not have children");
+ return NS_ERROR_FAILURE;
+ }
+ *aOutEndNode = tmp;
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult HTMLEditor::HTMLWithContextInserter::FragmentParser::ParseFragment(
+ const nsAString& aFragStr, nsAtom* aContextLocalName,
+ const Document* aTargetDocument, DocumentFragment** aFragment,
+ SafeToInsertData aSafeToInsertData) {
+ nsAutoScriptBlockerSuppressNodeRemoved autoBlocker;
+
+ nsCOMPtr<Document> doc =
+ nsContentUtils::CreateInertHTMLDocument(aTargetDocument);
+ if (!doc) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<DocumentFragment> fragment =
+ new (doc->NodeInfoManager()) DocumentFragment(doc->NodeInfoManager());
+ nsresult rv = nsContentUtils::ParseFragmentHTML(
+ aFragStr, fragment,
+ aContextLocalName ? aContextLocalName : nsGkAtoms::body,
+ kNameSpaceID_XHTML, false, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsContentUtils::ParseFragmentHTML() failed");
+ if (aSafeToInsertData == SafeToInsertData::No) {
+ nsTreeSanitizer sanitizer(aContextLocalName
+ ? nsIParserUtils::SanitizerAllowStyle
+ : nsIParserUtils::SanitizerAllowComments);
+ sanitizer.Sanitize(fragment);
+ }
+ fragment.forget(aFragment);
+ return rv;
+}
+
+// static
+void HTMLEditor::HTMLWithContextInserter::
+ CollectTopMostChildContentsCompletelyInRange(
+ const EditorRawDOMPoint& aStartPoint,
+ const EditorRawDOMPoint& aEndPoint,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) {
+ MOZ_ASSERT(aStartPoint.IsSetAndValid());
+ MOZ_ASSERT(aEndPoint.IsSetAndValid());
+
+ RefPtr<nsRange> range =
+ nsRange::Create(aStartPoint.ToRawRangeBoundary(),
+ aEndPoint.ToRawRangeBoundary(), IgnoreErrors());
+ if (!range) {
+ NS_WARNING("nsRange::Create() failed");
+ return;
+ }
+ DOMSubtreeIterator iter;
+ if (NS_FAILED(iter.Init(*range))) {
+ NS_WARNING("DOMSubtreeIterator::Init() failed, but ignored");
+ return;
+ }
+
+ iter.AppendAllNodesToArray(aOutArrayOfContents);
+}
+
+/******************************************************************************
+ * HTMLEditor::AutoHTMLFragmentBoundariesFixer
+ ******************************************************************************/
+
+HTMLEditor::AutoHTMLFragmentBoundariesFixer::AutoHTMLFragmentBoundariesFixer(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents) {
+ EnsureBeginsOrEndsWithValidContent(StartOrEnd::start,
+ aArrayOfTopMostChildContents);
+ EnsureBeginsOrEndsWithValidContent(StartOrEnd::end,
+ aArrayOfTopMostChildContents);
+}
+
+// static
+void HTMLEditor::AutoHTMLFragmentBoundariesFixer::
+ CollectTableAndAnyListElementsOfInclusiveAncestorsAt(
+ nsIContent& aContent,
+ nsTArray<OwningNonNull<Element>>& aOutArrayOfListAndTableElements) {
+ for (Element* element = aContent.GetAsElementOrParentElement(); element;
+ element = element->GetParentElement()) {
+ if (HTMLEditUtils::IsAnyListElement(element) ||
+ HTMLEditUtils::IsTable(element)) {
+ aOutArrayOfListAndTableElements.AppendElement(*element);
+ }
+ }
+}
+
+// static
+Element* HTMLEditor::AutoHTMLFragmentBoundariesFixer::
+ GetMostDistantAncestorListOrTableElement(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfTopMostChildContents,
+ const nsTArray<OwningNonNull<Element>>&
+ aInclusiveAncestorsTableOrListElements) {
+ Element* lastFoundAncestorListOrTableElement = nullptr;
+ for (auto& content : aArrayOfTopMostChildContents) {
+ if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
+ Element* tableElement =
+ HTMLEditUtils::GetClosestAncestorTableElement(*content);
+ if (!tableElement) {
+ continue;
+ }
+ // If we find a `<table>` element which is an ancestor of a table
+ // related element and is not an acestor of first nor last of
+ // aArrayOfNodes, return the last found list or `<table>` element.
+ // XXX Is that really expected that this returns a list element in this
+ // case?
+ if (!aInclusiveAncestorsTableOrListElements.Contains(tableElement)) {
+ return lastFoundAncestorListOrTableElement;
+ }
+ // If we find a `<table>` element which is topmost list or `<table>`
+ // element at first or last of aArrayOfNodes, return it.
+ if (aInclusiveAncestorsTableOrListElements.LastElement().get() ==
+ tableElement) {
+ return tableElement;
+ }
+ // Otherwise, store the `<table>` element which is an ancestor but
+ // not topmost ancestor of first or last of aArrayOfNodes.
+ lastFoundAncestorListOrTableElement = tableElement;
+ continue;
+ }
+
+ if (!HTMLEditUtils::IsListItem(content)) {
+ continue;
+ }
+ Element* listElement =
+ HTMLEditUtils::GetClosestAncestorAnyListElement(*content);
+ if (!listElement) {
+ continue;
+ }
+ // If we find a list element which is ancestor of a list item element and
+ // is not an acestor of first nor last of aArrayOfNodes, return the last
+ // found list or `<table>` element.
+ // XXX Is that really expected that this returns a `<table>` element in
+ // this case?
+ if (!aInclusiveAncestorsTableOrListElements.Contains(listElement)) {
+ return lastFoundAncestorListOrTableElement;
+ }
+ // If we find a list element which is topmost list or `<table>` element at
+ // first or last of aArrayOfNodes, return it.
+ if (aInclusiveAncestorsTableOrListElements.LastElement().get() ==
+ listElement) {
+ return listElement;
+ }
+ // Otherwise, store the list element which is an ancestor but not topmost
+ // ancestor of first or last of aArrayOfNodes.
+ lastFoundAncestorListOrTableElement = listElement;
+ }
+
+ // If we find only non-topmost list or `<table>` element, returns the last
+ // found one (meaning bottommost one). Otherwise, nullptr.
+ return lastFoundAncestorListOrTableElement;
+}
+
+Element*
+HTMLEditor::AutoHTMLFragmentBoundariesFixer::FindReplaceableTableElement(
+ Element& aTableElement, nsIContent& aContentMaybeInTableElement) const {
+ MOZ_ASSERT(aTableElement.IsHTMLElement(nsGkAtoms::table));
+ // Perhaps, this is designed for climbing up the DOM tree from
+ // aContentMaybeInTableElement to aTableElement and making sure that
+ // aContentMaybeInTableElement itself or its ancestor is a `<td>`, `<th>`,
+ // `<tr>`, `<thead>`, `<tbody>`, `<tfoot>` or `<caption>`.
+ // But this looks really buggy because this loop may skip aTableElement
+ // as the following NS_ASSERTION. We should write automated tests and
+ // check right behavior.
+ for (Element* element =
+ aContentMaybeInTableElement.GetAsElementOrParentElement();
+ element; element = element->GetParentElement()) {
+ if (!HTMLEditUtils::IsAnyTableElement(element) ||
+ element->IsHTMLElement(nsGkAtoms::table)) {
+ // XXX Perhaps, the original developer of this method assumed that
+ // aTableElement won't be skipped because if it's assumed, we can
+ // stop climbing up the tree in that case.
+ NS_ASSERTION(element != &aTableElement,
+ "The table element which is looking for is ignored");
+ continue;
+ }
+ Element* tableElement = nullptr;
+ for (Element* maybeTableElement = element->GetParentElement();
+ maybeTableElement;
+ maybeTableElement = maybeTableElement->GetParentElement()) {
+ if (maybeTableElement->IsHTMLElement(nsGkAtoms::table)) {
+ tableElement = maybeTableElement;
+ break;
+ }
+ }
+ if (tableElement == &aTableElement) {
+ return element;
+ }
+ // XXX If we find another `<table>` element, why don't we keep searching
+ // from its parent?
+ }
+ return nullptr;
+}
+
+bool HTMLEditor::AutoHTMLFragmentBoundariesFixer::IsReplaceableListElement(
+ Element& aListElement, nsIContent& aContentMaybeInListElement) const {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+ // Perhaps, this is designed for climbing up the DOM tree from
+ // aContentMaybeInListElement to aListElement and making sure that
+ // aContentMaybeInListElement itself or its ancestor is an list item.
+ // But this looks really buggy because this loop may skip aListElement
+ // as the following NS_ASSERTION. We should write automated tests and
+ // check right behavior.
+ for (Element* element =
+ aContentMaybeInListElement.GetAsElementOrParentElement();
+ element; element = element->GetParentElement()) {
+ if (!HTMLEditUtils::IsListItem(element)) {
+ // XXX Perhaps, the original developer of this method assumed that
+ // aListElement won't be skipped because if it's assumed, we can
+ // stop climbing up the tree in that case.
+ NS_ASSERTION(element != &aListElement,
+ "The list element which is looking for is ignored");
+ continue;
+ }
+ Element* listElement =
+ HTMLEditUtils::GetClosestAncestorAnyListElement(*element);
+ if (listElement == &aListElement) {
+ return true;
+ }
+ // XXX If we find another list element, why don't we keep searching
+ // from its parent?
+ }
+ return false;
+}
+
+void HTMLEditor::AutoHTMLFragmentBoundariesFixer::
+ EnsureBeginsOrEndsWithValidContent(StartOrEnd aStartOrEnd,
+ nsTArray<OwningNonNull<nsIContent>>&
+ aArrayOfTopMostChildContents) const {
+ MOZ_ASSERT(!aArrayOfTopMostChildContents.IsEmpty());
+
+ // Collect list elements and table related elements at first or last node
+ // in aArrayOfTopMostChildContents.
+ AutoTArray<OwningNonNull<Element>, 4> inclusiveAncestorsListOrTableElements;
+ CollectTableAndAnyListElementsOfInclusiveAncestorsAt(
+ aStartOrEnd == StartOrEnd::end
+ ? aArrayOfTopMostChildContents.LastElement()
+ : aArrayOfTopMostChildContents[0],
+ inclusiveAncestorsListOrTableElements);
+ if (inclusiveAncestorsListOrTableElements.IsEmpty()) {
+ return;
+ }
+
+ // Get most ancestor list or `<table>` element in
+ // inclusiveAncestorsListOrTableElements which contains earlier
+ // node in aArrayOfTopMostChildContents as far as possible.
+ // XXX With inclusiveAncestorsListOrTableElements, this returns a
+ // list or `<table>` element which contains first or last node of
+ // aArrayOfTopMostChildContents. However, this seems slow when
+ // aStartOrEnd is StartOrEnd::end and only the last node is in
+ // different list or `<table>`. But I'm not sure whether it's
+ // possible case or not. We need to add tests to
+ // test_content_iterator_subtree.html for checking how
+ // SubtreeContentIterator works.
+ Element* listOrTableElement = GetMostDistantAncestorListOrTableElement(
+ aArrayOfTopMostChildContents, inclusiveAncestorsListOrTableElements);
+ if (!listOrTableElement) {
+ return;
+ }
+
+ // If we have pieces of tables or lists to be inserted, let's force the
+ // insertion to deal with table elements right away, so that it doesn't
+ // orphan some table or list contents outside the table or list.
+
+ OwningNonNull<nsIContent>& firstOrLastChildContent =
+ aStartOrEnd == StartOrEnd::end
+ ? aArrayOfTopMostChildContents.LastElement()
+ : aArrayOfTopMostChildContents[0];
+
+ // Find substructure of list or table that must be included in paste.
+ Element* replaceElement;
+ if (HTMLEditUtils::IsAnyListElement(listOrTableElement)) {
+ if (!IsReplaceableListElement(*listOrTableElement,
+ firstOrLastChildContent)) {
+ return;
+ }
+ replaceElement = listOrTableElement;
+ } else {
+ MOZ_ASSERT(listOrTableElement->IsHTMLElement(nsGkAtoms::table));
+ replaceElement = FindReplaceableTableElement(*listOrTableElement,
+ firstOrLastChildContent);
+ if (!replaceElement) {
+ return;
+ }
+ }
+
+ // If we can replace the given list element or found a table related element
+ // in the `<table>` element, insert it into aArrayOfTopMostChildContents which
+ // is tompost children to be inserted instead of descendants of them in
+ // aArrayOfTopMostChildContents.
+ for (size_t i = 0; i < aArrayOfTopMostChildContents.Length();) {
+ OwningNonNull<nsIContent>& content = aArrayOfTopMostChildContents[i];
+ if (content == replaceElement) {
+ // If the element is n aArrayOfTopMostChildContents, its descendants must
+ // not be in the array. Therefore, we don't need to optimize this case.
+ // XXX Perhaps, we can break this loop right now.
+ aArrayOfTopMostChildContents.RemoveElementAt(i);
+ continue;
+ }
+ if (!EditorUtils::IsDescendantOf(content, *replaceElement)) {
+ i++;
+ continue;
+ }
+ // For saving number of calls of EditorUtils::IsDescendantOf(), we should
+ // remove its siblings in the array.
+ nsIContent* parent = content->GetParent();
+ aArrayOfTopMostChildContents.RemoveElementAt(i);
+ while (i < aArrayOfTopMostChildContents.Length() &&
+ aArrayOfTopMostChildContents[i]->GetParent() == parent) {
+ aArrayOfTopMostChildContents.RemoveElementAt(i);
+ }
+ }
+
+ // Now replace the removed nodes with the structural parent
+ if (aStartOrEnd == StartOrEnd::end) {
+ aArrayOfTopMostChildContents.AppendElement(*replaceElement);
+ } else {
+ aArrayOfTopMostChildContents.InsertElementAt(0, *replaceElement);
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp
new file mode 100644
index 0000000000..18f9eda88e
--- /dev/null
+++ b/editor/libeditor/HTMLEditorDeleteHandler.cpp
@@ -0,0 +1,7062 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditor.h"
+#include "HTMLEditorNestedClasses.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "AutoRangeArray.h"
+#include "CSSEditUtils.h"
+#include "EditAction.h"
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditorInlines.h"
+#include "HTMLEditUtils.h"
+#include "WSRunObject.h"
+
+#include "ErrorList.h"
+#include "js/ErrorReport.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ComputedStyle.h" // for ComputedStyle
+#include "mozilla/ContentIterator.h"
+#include "mozilla/EditorDOMPoint.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/InternalMutationEvent.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/SelectionState.h"
+#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
+#include "mozilla/Unused.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsAtom.h"
+#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsFrameSelection.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsRange.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStyleConsts.h" // for StyleWhiteSpace
+#include "nsTArray.h"
+
+// NOTE: This file was split from:
+// https://searchfox.org/mozilla-central/rev/c409dd9235c133ab41eba635f906aa16e050c197/editor/libeditor/HTMLEditSubActionHandler.cpp
+
+namespace mozilla {
+
+using namespace dom;
+using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+using InvisibleWhiteSpaces = HTMLEditUtils::InvisibleWhiteSpaces;
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using ScanLineBreak = HTMLEditUtils::ScanLineBreak;
+using TableBoundary = HTMLEditUtils::TableBoundary;
+using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
+
+static LazyLogModule gOneLineMoverLog("AutoMoveOneLineHandler");
+
+template Result<CaretPoint, nsresult>
+HTMLEditor::DeleteTextAndTextNodesWithTransaction(
+ const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint,
+ TreatEmptyTextNodes aTreatEmptyTextNodes);
+template Result<CaretPoint, nsresult>
+HTMLEditor::DeleteTextAndTextNodesWithTransaction(
+ const EditorDOMPointInText& aStartPoint,
+ const EditorDOMPointInText& aEndPoint,
+ TreatEmptyTextNodes aTreatEmptyTextNodes);
+
+/*****************************************************************************
+ * AutoSetTemporaryAncestorLimiter
+ ****************************************************************************/
+
+class MOZ_RAII AutoSetTemporaryAncestorLimiter final {
+ public:
+ AutoSetTemporaryAncestorLimiter(const HTMLEditor& aHTMLEditor,
+ Selection& aSelection,
+ nsINode& aStartPointNode,
+ AutoRangeArray* aRanges = nullptr) {
+ MOZ_ASSERT(aSelection.GetType() == SelectionType::eNormal);
+
+ if (aSelection.GetAncestorLimiter()) {
+ return;
+ }
+
+ Element* selectionRootElement =
+ aHTMLEditor.FindSelectionRoot(aStartPointNode);
+ if (!selectionRootElement) {
+ return;
+ }
+ aHTMLEditor.InitializeSelectionAncestorLimit(*selectionRootElement);
+ mSelection = &aSelection;
+ // Setting ancestor limiter may change ranges which were outer of
+ // the new limiter. Therefore, we need to reinitialize aRanges.
+ if (aRanges) {
+ aRanges->Initialize(aSelection);
+ }
+ }
+
+ ~AutoSetTemporaryAncestorLimiter() {
+ if (mSelection) {
+ mSelection->SetAncestorLimiter(nullptr);
+ }
+ }
+
+ private:
+ RefPtr<Selection> mSelection;
+};
+
+/*****************************************************************************
+ * AutoDeleteRangesHandler
+ ****************************************************************************/
+
+class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
+ public:
+ explicit AutoDeleteRangesHandler(
+ const AutoDeleteRangesHandler* aParent = nullptr)
+ : mParent(aParent),
+ mOriginalDirectionAndAmount(nsIEditor::eNone),
+ mOriginalStripWrappers(nsIEditor::eNoStrip) {}
+
+ /**
+ * ComputeRangesToDelete() computes actual deletion ranges.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ComputeRangesToDelete(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete, const Element& aEditingHost);
+
+ /**
+ * Deletes content in or around aRangesToDelete.
+ * NOTE: This method creates SelectionBatcher. Therefore, each caller
+ * needs to check if the editor is still available even if this returns
+ * NS_OK.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
+ const Element& aEditingHost);
+
+ private:
+ bool IsHandlingRecursively() const { return mParent != nullptr; }
+
+ bool CanFallbackToDeleteRangesWithTransaction(
+ const AutoRangeArray& aRangesToDelete) const {
+ return !IsHandlingRecursively() && !aRangesToDelete.Ranges().IsEmpty() &&
+ (!aRangesToDelete.IsCollapsed() ||
+ EditorBase::HowToHandleCollapsedRangeFor(
+ mOriginalDirectionAndAmount) !=
+ EditorBase::HowToHandleCollapsedRange::Ignore);
+ }
+
+ /**
+ * HandleDeleteAroundCollapsedRanges() handles deletion with collapsed
+ * ranges. Callers must guarantee that this is called only when
+ * aRangesToDelete.IsCollapsed() returns true.
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aStripWrappers Must be eStrip or eNoStrip.
+ * @param aRangesToDelete Ranges to delete. This `IsCollapsed()` must
+ * return true.
+ * @param aWSRunScannerAtCaret Scanner instance which scanned from
+ * caret point.
+ * @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret
+ * toward aDirectionAndAmount.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleDeleteAroundCollapsedRanges(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
+ const WSRunScanner& aWSRunScannerAtCaret,
+ const WSScanResult& aScanFromCaretPointResult,
+ const Element& aEditingHost);
+ nsresult ComputeRangesToDeleteAroundCollapsedRanges(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete, const WSRunScanner& aWSRunScannerAtCaret,
+ const WSScanResult& aScanFromCaretPointResult,
+ const Element& aEditingHost) const;
+
+ /**
+ * HandleDeleteNonCollapsedRanges() handles deletion with non-collapsed
+ * ranges. Callers must guarantee that this is called only when
+ * aRangesToDelete.IsCollapsed() returns false.
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aStripWrappers Must be eStrip or eNoStrip.
+ * @param aRangesToDelete The ranges to delete.
+ * @param aSelectionWasCollapsed If the caller extended `Selection`
+ * from collapsed, set this to `Yes`.
+ * Otherwise, i.e., `Selection` is not
+ * collapsed from the beginning, set
+ * this to `No`.
+ * @param aEditingHost The editing host.
+ */
+ enum class SelectionWasCollapsed { Yes, No };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleDeleteNonCollapsedRanges(HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ AutoRangeArray& aRangesToDelete,
+ SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost);
+ nsresult ComputeRangesToDeleteNonCollapsedRanges(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete,
+ SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost) const;
+
+ /**
+ * Handle deletion of collapsed ranges in a text node.
+ *
+ * @param aDirectionAndAmount Must be eNext or ePrevious.
+ * @param aCaretPosition The position where caret is. This container
+ * must be a text node.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ HandleDeleteTextAroundCollapsedRanges(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete, const Element& aEditingHost);
+ nsresult ComputeRangesToDeleteTextAroundCollapsedRanges(
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const;
+
+ /**
+ * Handles deletion of collapsed selection at white-spaces in a text node.
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aPointToDelete The point to delete. I.e., typically, caret
+ * position.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ HandleDeleteCollapsedSelectionAtWhiteSpaces(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aPointToDelete, const Element& aEditingHost);
+
+ /**
+ * Handle deletion of collapsed selection in a text node.
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aRangesToDelete Computed selection ranges to delete.
+ * @param aPointAtDeletingChar The visible char position which you want to
+ * delete.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ HandleDeleteCollapsedSelectionAtVisibleChar(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete,
+ const EditorDOMPoint& aPointAtDeletingChar, const Element& aEditingHost);
+
+ /**
+ * Handle deletion of atomic elements like <br>, <hr>, <img>, <input>, etc and
+ * data nodes except text node (e.g., comment node). Note that don't call this
+ * directly with `<hr>` element.
+ *
+ * @param aAtomicContent The atomic content to be deleted.
+ * @param aCaretPoint The caret point (i.e., selection start or
+ * end).
+ * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized
+ * with the caret point.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ HandleDeleteAtomicContent(HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
+ const EditorDOMPoint& aCaretPoint,
+ const WSRunScanner& aWSRunScannerAtCaret,
+ const Element& aEditingHost);
+ nsresult ComputeRangesToDeleteAtomicContent(
+ Element* aEditingHost, const nsIContent& aAtomicContent,
+ AutoRangeArray& aRangesToDelete) const;
+
+ /**
+ * GetAtomicContnetToDelete() returns better content that is deletion of
+ * atomic element. If aScanFromCaretPointResult is special, since this
+ * point may not be editable, we look for better point to remove atomic
+ * content.
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aWSRunScannerAtCaret WSRunScanner instance which was
+ * initialized with the caret point.
+ * @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret
+ * toward aDirectionAndAmount.
+ */
+ static nsIContent* GetAtomicContentToDelete(
+ nsIEditor::EDirection aDirectionAndAmount,
+ const WSRunScanner& aWSRunScannerAtCaret,
+ const WSScanResult& aScanFromCaretPointResult) MOZ_NONNULL_RETURN;
+
+ /**
+ * HandleDeleteAtOtherBlockBoundary() handles deletion at other block boundary
+ * (i.e., immediately before or after a block). If this does not join blocks,
+ * `Run()` may be called recursively with creating another instance.
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aStripWrappers Must be eStrip or eNoStrip.
+ * @param aOtherBlockElement The block element which follows the caret or
+ * is followed by caret.
+ * @param aCaretPoint The caret point (i.e., selection start or
+ * end).
+ * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized
+ * with the caret point.
+ * @param aRangesToDelete Ranges to delete of the caller. This should
+ * be collapsed and the point should match with
+ * aCaretPoint.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleDeleteAtOtherBlockBoundary(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers, Element& aOtherBlockElement,
+ const EditorDOMPoint& aCaretPoint, WSRunScanner& aWSRunScannerAtCaret,
+ AutoRangeArray& aRangesToDelete, const Element& aEditingHost);
+
+ /**
+ * ExtendOrShrinkRangeToDelete() extends aRangeToDelete if there are
+ * an invisible <br> element and/or some parent empty elements.
+ *
+ * @param aFrameSelection If the caller wants range in selection limiter,
+ * set this to non-nullptr which knows the limiter.
+ * @param aRangeToDelete The range to be extended for deletion. This
+ * must not be collapsed, must be positioned.
+ */
+ template <typename EditorDOMRangeType>
+ Result<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToDelete(
+ const HTMLEditor& aHTMLEditor, const nsFrameSelection* aFrameSelection,
+ const EditorDOMRangeType& aRangeToDelete) const;
+
+ /**
+ * A helper method for ExtendOrShrinkRangeToDelete(). This returns shrunken
+ * range if aRangeToDelete selects all over list elements which have some list
+ * item elements to avoid to delete all list items from the list element.
+ */
+ MOZ_NEVER_INLINE_DEBUG static EditorRawDOMRange
+ GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements(
+ const EditorRawDOMRange& aRangeToDelete);
+
+ /**
+ * DeleteUnnecessaryNodes() removes unnecessary nodes around aRange.
+ * Note that aRange is tracked with AutoTrackDOMRange.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteUnnecessaryNodes(HTMLEditor& aHTMLEditor, EditorDOMRange& aRange);
+
+ /**
+ * DeleteUnnecessaryNodesAndCollapseSelection() calls DeleteUnnecessaryNodes()
+ * and then, collapse selection at tracked aSelectionStartPoint or
+ * aSelectionEndPoint (depending on aDirectionAndAmount).
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * If nsIEditor::ePrevious, selection
+ * will be collapsed to aSelectionEndPoint.
+ * Otherwise, selection will be collapsed
+ * to aSelectionStartPoint.
+ * @param aSelectionStartPoint First selection range start after
+ * computing the deleting range.
+ * @param aSelectionEndPoint First selection range end after
+ * computing the deleting range.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteUnnecessaryNodesAndCollapseSelection(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aSelectionStartPoint,
+ const EditorDOMPoint& aSelectionEndPoint);
+
+ /**
+ * If aContent is a text node that contains only collapsed white-space or
+ * empty and editable.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteNodeIfInvisibleAndEditableTextNode(HTMLEditor& aHTMLEditor,
+ nsIContent& aContent);
+
+ /**
+ * DeleteParentBlocksIfEmpty() removes parent block elements if they
+ * don't have visible contents. Note that due performance issue of
+ * WhiteSpaceVisibilityKeeper, this call may be expensive. And also note that
+ * this removes a empty block with a transaction. So, please make sure that
+ * you've already created `AutoPlaceholderBatch`.
+ *
+ * @param aPoint The point whether this method climbing up the DOM
+ * tree to remove empty parent blocks.
+ * @return NS_OK if one or more empty block parents are deleted.
+ * NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND if the point is
+ * not in empty block.
+ * Or NS_ERROR_* if something unexpected occurs.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteParentBlocksWithTransactionIfEmpty(HTMLEditor& aHTMLEditor,
+ const EditorDOMPoint& aPoint);
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ FallbackToDeleteRangesWithTransaction(HTMLEditor& aHTMLEditor,
+ AutoRangeArray& aRangesToDelete) const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteRangesWithTransaction(mOriginalDirectionAndAmount,
+ mOriginalStripWrappers,
+ aRangesToDelete);
+ NS_WARNING_ASSERTION(caretPointOrError.isOk(),
+ "HTMLEditor::DeleteRangesWithTransaction() failed");
+ return caretPointOrError;
+ }
+
+ /**
+ * ComputeRangesToDeleteRangesWithTransaction() computes target ranges
+ * which will be called by `EditorBase::DeleteRangesWithTransaction()`.
+ * TODO: We should not use it for consistency with each deletion handler
+ * in this and nested classes.
+ */
+ nsresult ComputeRangesToDeleteRangesWithTransaction(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete) const;
+
+ nsresult FallbackToComputeRangesToDeleteRangesWithTransaction(
+ const HTMLEditor& aHTMLEditor, AutoRangeArray& aRangesToDelete) const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
+ nsresult rv = ComputeRangesToDeleteRangesWithTransaction(
+ aHTMLEditor, mOriginalDirectionAndAmount, aRangesToDelete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::"
+ "ComputeRangesToDeleteRangesWithTransaction() failed");
+ return rv;
+ }
+
+ class MOZ_STACK_CLASS AutoBlockElementsJoiner final {
+ public:
+ AutoBlockElementsJoiner() = delete;
+ explicit AutoBlockElementsJoiner(
+ AutoDeleteRangesHandler& aDeleteRangesHandler)
+ : mDeleteRangesHandler(&aDeleteRangesHandler),
+ mDeleteRangesHandlerConst(aDeleteRangesHandler) {}
+ explicit AutoBlockElementsJoiner(
+ const AutoDeleteRangesHandler& aDeleteRangesHandler)
+ : mDeleteRangesHandler(nullptr),
+ mDeleteRangesHandlerConst(aDeleteRangesHandler) {}
+
+ /**
+ * PrepareToDeleteAtCurrentBlockBoundary() considers left content and right
+ * content which are joined for handling deletion at current block boundary
+ * (i.e., at start or end of the current block).
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aCurrentBlockElement The current block element.
+ * @param aCaretPoint The caret point (i.e., selection start
+ * or end).
+ * @return true if can continue to handle the
+ * deletion.
+ */
+ bool PrepareToDeleteAtCurrentBlockBoundary(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint);
+
+ /**
+ * PrepareToDeleteAtOtherBlockBoundary() considers left content and right
+ * content which are joined for handling deletion at other block boundary
+ * (i.e., immediately before or after a block).
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aOtherBlockElement The block element which follows the
+ * caret or is followed by caret.
+ * @param aCaretPoint The caret point (i.e., selection start
+ * or end).
+ * @param aWSRunScannerAtCaret WSRunScanner instance which was
+ * initialized with the caret point.
+ * @return true if can continue to handle the
+ * deletion.
+ */
+ bool PrepareToDeleteAtOtherBlockBoundary(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount, Element& aOtherBlockElement,
+ const EditorDOMPoint& aCaretPoint,
+ const WSRunScanner& aWSRunScannerAtCaret);
+
+ /**
+ * PrepareToDeleteNonCollapsedRanges() considers left block element and
+ * right block element which are inclusive ancestor block element of
+ * start and end container of first range of aRangesToDelete.
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aRangesToDelete Ranges to delete. Must not be
+ * collapsed.
+ * @return true if can continue to handle the
+ * deletion.
+ */
+ bool PrepareToDeleteNonCollapsedRanges(
+ const HTMLEditor& aHTMLEditor, const AutoRangeArray& aRangesToDelete);
+
+ /**
+ * Run() executes the joining.
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aStripWrappers Must be eStrip or eNoStrip.
+ * @param aCaretPoint The caret point (i.e., selection start
+ * or end).
+ * @param aRangesToDelete Ranges to delete of the caller.
+ * This should be collapsed and match
+ * with aCaretPoint.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete,
+ const Element& aEditingHost) {
+ switch (mMode) {
+ case Mode::JoinCurrentBlock: {
+ Result<EditActionResult, nsresult> result =
+ HandleDeleteAtCurrentBlockBoundary(
+ aHTMLEditor, aDirectionAndAmount, aCaretPoint, aEditingHost);
+ NS_WARNING_ASSERTION(result.isOk(),
+ "AutoBlockElementsJoiner::"
+ "HandleDeleteAtCurrentBlockBoundary() failed");
+ return result;
+ }
+ case Mode::JoinOtherBlock: {
+ Result<EditActionResult, nsresult> result =
+ HandleDeleteAtOtherBlockBoundary(aHTMLEditor, aDirectionAndAmount,
+ aStripWrappers, aCaretPoint,
+ aRangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(result.isOk(),
+ "AutoBlockElementsJoiner::"
+ "HandleDeleteAtOtherBlockBoundary() failed");
+ return result;
+ }
+ case Mode::DeleteBRElement: {
+ Result<EditActionResult, nsresult> result =
+ DeleteBRElement(aHTMLEditor, aDirectionAndAmount, aEditingHost);
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "AutoBlockElementsJoiner::DeleteBRElement() failed");
+ return result;
+ }
+ case Mode::JoinBlocksInSameParent:
+ case Mode::DeleteContentInRanges:
+ case Mode::DeleteNonCollapsedRanges:
+ MOZ_ASSERT_UNREACHABLE(
+ "This mode should be handled in the other Run()");
+ return Err(NS_ERROR_UNEXPECTED);
+ case Mode::NotInitialized:
+ return EditActionResult::IgnoredResult();
+ }
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ nsresult ComputeRangesToDelete(const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aCaretPoint,
+ AutoRangeArray& aRangesToDelete,
+ const Element& aEditingHost) const {
+ switch (mMode) {
+ case Mode::JoinCurrentBlock: {
+ nsresult rv = ComputeRangesToDeleteAtCurrentBlockBoundary(
+ aHTMLEditor, aCaretPoint, aRangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::"
+ "ComputeRangesToDeleteAtCurrentBlockBoundary() failed");
+ return rv;
+ }
+ case Mode::JoinOtherBlock: {
+ nsresult rv = ComputeRangesToDeleteAtOtherBlockBoundary(
+ aHTMLEditor, aDirectionAndAmount, aCaretPoint, aRangesToDelete,
+ aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::"
+ "ComputeRangesToDeleteAtOtherBlockBoundary() failed");
+ return rv;
+ }
+ case Mode::DeleteBRElement: {
+ nsresult rv = ComputeRangesToDeleteBRElement(aRangesToDelete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::"
+ "ComputeRangesToDeleteBRElement() failed");
+ return rv;
+ }
+ case Mode::JoinBlocksInSameParent:
+ case Mode::DeleteContentInRanges:
+ case Mode::DeleteNonCollapsedRanges:
+ MOZ_ASSERT_UNREACHABLE(
+ "This mode should be handled in the other "
+ "ComputeRangesToDelete()");
+ return NS_ERROR_UNEXPECTED;
+ case Mode::NotInitialized:
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ /**
+ * Run() executes the joining.
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aStripWrappers Whether delete or keep new empty
+ * ancestor elements.
+ * @param aRangesToDelete Ranges to delete. Must not be
+ * collapsed.
+ * @param aSelectionWasCollapsed Whether selection was or was not
+ * collapsed when starting to handle
+ * deletion.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ AutoRangeArray& aRangesToDelete,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost) {
+ switch (mMode) {
+ case Mode::JoinCurrentBlock:
+ case Mode::JoinOtherBlock:
+ case Mode::DeleteBRElement:
+ MOZ_ASSERT_UNREACHABLE(
+ "This mode should be handled in the other Run()");
+ return Err(NS_ERROR_UNEXPECTED);
+ case Mode::JoinBlocksInSameParent: {
+ Result<EditActionResult, nsresult> result =
+ JoinBlockElementsInSameParent(
+ aHTMLEditor, aDirectionAndAmount, aStripWrappers,
+ aRangesToDelete, aSelectionWasCollapsed, aEditingHost);
+ NS_WARNING_ASSERTION(result.isOk(),
+ "AutoBlockElementsJoiner::"
+ "JoinBlockElementsInSameParent() failed");
+ return result;
+ }
+ case Mode::DeleteContentInRanges: {
+ Result<EditActionResult, nsresult> result =
+ DeleteContentInRanges(aHTMLEditor, aDirectionAndAmount,
+ aStripWrappers, aRangesToDelete);
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "AutoBlockElementsJoiner::DeleteContentInRanges() failed");
+ return result;
+ }
+ case Mode::DeleteNonCollapsedRanges: {
+ Result<EditActionResult, nsresult> result =
+ HandleDeleteNonCollapsedRanges(
+ aHTMLEditor, aDirectionAndAmount, aStripWrappers,
+ aRangesToDelete, aSelectionWasCollapsed, aEditingHost);
+ NS_WARNING_ASSERTION(result.isOk(),
+ "AutoBlockElementsJoiner::"
+ "HandleDeleteNonCollapsedRange() failed");
+ return result;
+ }
+ case Mode::NotInitialized:
+ MOZ_ASSERT_UNREACHABLE(
+ "Call Run() after calling a preparation method");
+ return EditActionResult::IgnoredResult();
+ }
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ nsresult ComputeRangesToDelete(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost) const {
+ switch (mMode) {
+ case Mode::JoinCurrentBlock:
+ case Mode::JoinOtherBlock:
+ case Mode::DeleteBRElement:
+ MOZ_ASSERT_UNREACHABLE(
+ "This mode should be handled in the other "
+ "ComputeRangesToDelete()");
+ return NS_ERROR_UNEXPECTED;
+ case Mode::JoinBlocksInSameParent: {
+ nsresult rv = ComputeRangesToJoinBlockElementsInSameParent(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::"
+ "ComputeRangesToJoinBlockElementsInSameParent() failed");
+ return rv;
+ }
+ case Mode::DeleteContentInRanges: {
+ nsresult rv = ComputeRangesToDeleteContentInRanges(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::"
+ "ComputeRangesToDeleteContentInRanges() failed");
+ return rv;
+ }
+ case Mode::DeleteNonCollapsedRanges: {
+ nsresult rv = ComputeRangesToDeleteNonCollapsedRanges(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete,
+ aSelectionWasCollapsed, aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::"
+ "ComputeRangesToDeleteNonCollapsedRanges() failed");
+ return rv;
+ }
+ case Mode::NotInitialized:
+ MOZ_ASSERT_UNREACHABLE(
+ "Call ComputeRangesToDelete() after calling a preparation "
+ "method");
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsIContent* GetLeafContentInOtherBlockElement() const {
+ MOZ_ASSERT(mMode == Mode::JoinOtherBlock);
+ return mLeafContentInOtherBlock;
+ }
+
+ private:
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleDeleteAtCurrentBlockBoundary(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aCaretPoint, const Element& aEditingHost);
+ nsresult ComputeRangesToDeleteAtCurrentBlockBoundary(
+ const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
+ AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleDeleteAtOtherBlockBoundary(HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ const EditorDOMPoint& aCaretPoint,
+ AutoRangeArray& aRangesToDelete,
+ const Element& aEditingHost);
+ // FYI: This method may modify selection, but it won't cause running
+ // script because of `AutoHideSelectionChanges` which blocks
+ // selection change listeners and the selection change event
+ // dispatcher.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
+ ComputeRangesToDeleteAtOtherBlockBoundary(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete,
+ const Element& aEditingHost) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ JoinBlockElementsInSameParent(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ AutoRangeArray& aRangesToDelete,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost);
+ nsresult ComputeRangesToJoinBlockElementsInSameParent(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ DeleteBRElement(HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ const Element& aEditingHost);
+ nsresult ComputeRangesToDeleteBRElement(
+ AutoRangeArray& aRangesToDelete) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ DeleteContentInRanges(HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ AutoRangeArray& aRangesToDelete);
+ nsresult ComputeRangesToDeleteContentInRanges(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleDeleteNonCollapsedRanges(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ AutoRangeArray& aRangesToDelete,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost);
+ nsresult ComputeRangesToDeleteNonCollapsedRanges(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost) const;
+
+ /**
+ * JoinNodesDeepWithTransaction() joins aLeftNode and aRightNode "deeply".
+ * First, they are joined simply, then, new right node is assumed as the
+ * child at length of the left node before joined and new left node is
+ * assumed as its previous sibling. Then, they will be joined again.
+ * And then, these steps are repeated.
+ *
+ * @param aLeftContent The node which will be removed form the tree.
+ * @param aRightContent The node which will be inserted the contents of
+ * aRightContent.
+ * @return The point of the first child of the last right
+ * node. The result is always set if this succeeded.
+ */
+ MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
+ JoinNodesDeepWithTransaction(HTMLEditor& aHTMLEditor,
+ nsIContent& aLeftContent,
+ nsIContent& aRightContent);
+
+ /**
+ * DeleteNodesEntirelyInRangeButKeepTableStructure() removes nodes which are
+ * entirely in aRange. Howevers, if some nodes are part of a table,
+ * removes all children of them instead. I.e., this does not make damage to
+ * table structure at the range, but may remove table entirely if it's
+ * in the range.
+ *
+ * @return true if inclusive ancestor block elements at
+ * start and end of the range should be joined.
+ */
+ MOZ_CAN_RUN_SCRIPT Result<bool, nsresult>
+ DeleteNodesEntirelyInRangeButKeepTableStructure(
+ HTMLEditor& aHTMLEditor, nsRange& aRange,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed);
+ bool NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure(
+ const HTMLEditor& aHTMLEditor,
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
+ const;
+ Result<bool, nsresult>
+ ComputeRangesToDeleteNodesEntirelyInRangeButKeepTableStructure(
+ const HTMLEditor& aHTMLEditor, nsRange& aRange,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
+ const;
+
+ /**
+ * DeleteContentButKeepTableStructure() removes aContent if it's an element
+ * which is part of a table structure. If it's a part of table structure,
+ * removes its all children recursively. I.e., this may delete all of a
+ * table, but won't break table structure partially.
+ *
+ * @param aContent The content which or whose all children should
+ * be removed.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteContentButKeepTableStructure(HTMLEditor& aHTMLEditor,
+ nsIContent& aContent);
+
+ /**
+ * DeleteTextAtStartAndEndOfRange() removes text if start and/or end of
+ * aRange is in a text node.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange);
+
+ class MOZ_STACK_CLASS AutoInclusiveAncestorBlockElementsJoiner final {
+ public:
+ AutoInclusiveAncestorBlockElementsJoiner() = delete;
+ AutoInclusiveAncestorBlockElementsJoiner(
+ nsIContent& aInclusiveDescendantOfLeftBlockElement,
+ nsIContent& aInclusiveDescendantOfRightBlockElement)
+ : mInclusiveDescendantOfLeftBlockElement(
+ aInclusiveDescendantOfLeftBlockElement),
+ mInclusiveDescendantOfRightBlockElement(
+ aInclusiveDescendantOfRightBlockElement),
+ mCanJoinBlocks(false),
+ mFallbackToDeleteLeafContent(false) {}
+
+ bool IsSet() const { return mLeftBlockElement && mRightBlockElement; }
+ bool IsSameBlockElement() const {
+ return mLeftBlockElement && mLeftBlockElement == mRightBlockElement;
+ }
+
+ const EditorDOMPoint& PointRefToPutCaret() const {
+ return mPointToPutCaret;
+ }
+
+ /**
+ * Prepare for joining inclusive ancestor block elements. When this
+ * returns false, the deletion should be canceled.
+ */
+ Result<bool, nsresult> Prepare(const HTMLEditor& aHTMLEditor,
+ const Element& aEditingHost);
+
+ /**
+ * When this returns true, this can join the blocks with `Run()`.
+ */
+ bool CanJoinBlocks() const { return mCanJoinBlocks; }
+
+ /**
+ * When this returns true, `Run()` must return "ignored" so that
+ * caller can skip calling `Run()`. This is available only when
+ * `CanJoinBlocks()` returns `true`.
+ * TODO: This should be merged into `CanJoinBlocks()` in the future.
+ */
+ bool ShouldDeleteLeafContentInstead() const {
+ MOZ_ASSERT(CanJoinBlocks());
+ return mFallbackToDeleteLeafContent;
+ }
+
+ /**
+ * ComputeRangesToDelete() extends aRangesToDelete includes the element
+ * boundaries between joining blocks. If they won't be joined, this
+ * collapses the range to aCaretPoint.
+ */
+ nsresult ComputeRangesToDelete(const HTMLEditor& aHTMLEditor,
+ const EditorDOMPoint& aCaretPoint,
+ AutoRangeArray& aRangesToDelete) const;
+
+ /**
+ * Join inclusive ancestor block elements which are found by preceding
+ * Preare() call.
+ * The right element is always joined to the left element.
+ * If the elements are the same type and not nested within each other,
+ * JoinEditableNodesWithTransaction() is called (example, joining two
+ * list items together into one).
+ * If the elements are not the same type, or one is a descendant of the
+ * other, we instead destroy the right block placing its children into
+ * left block.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
+ HTMLEditor& aHTMLEditor, const Element& aEditingHost);
+
+ private:
+ /**
+ * This method returns true when
+ * `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`,
+ * `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` and
+ * `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` handle it
+ * with the `if` block of their main blocks.
+ */
+ bool CanMergeLeftAndRightBlockElements() const {
+ if (!IsSet()) {
+ return false;
+ }
+ // `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`
+ if (mPointContainingTheOtherBlockElement.GetContainer() ==
+ mRightBlockElement) {
+ return mNewListElementTagNameOfRightListElement.isSome();
+ }
+ // `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()`
+ if (mPointContainingTheOtherBlockElement.GetContainer() ==
+ mLeftBlockElement) {
+ return mNewListElementTagNameOfRightListElement.isSome() &&
+ !mRightBlockElement->GetChildCount();
+ }
+ MOZ_ASSERT(!mPointContainingTheOtherBlockElement.IsSet());
+ // `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()`
+ return mNewListElementTagNameOfRightListElement.isSome() ||
+ mLeftBlockElement->NodeInfo()->NameAtom() ==
+ mRightBlockElement->NodeInfo()->NameAtom();
+ }
+
+ OwningNonNull<nsIContent> mInclusiveDescendantOfLeftBlockElement;
+ OwningNonNull<nsIContent> mInclusiveDescendantOfRightBlockElement;
+ RefPtr<Element> mLeftBlockElement;
+ RefPtr<Element> mRightBlockElement;
+ Maybe<nsAtom*> mNewListElementTagNameOfRightListElement;
+ EditorDOMPoint mPointContainingTheOtherBlockElement;
+ EditorDOMPoint mPointToPutCaret;
+ RefPtr<dom::HTMLBRElement> mPrecedingInvisibleBRElement;
+ bool mCanJoinBlocks;
+ bool mFallbackToDeleteLeafContent;
+ }; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ // AutoInclusiveAncestorBlockElementsJoiner
+
+ enum class Mode {
+ NotInitialized,
+ JoinCurrentBlock,
+ JoinOtherBlock,
+ JoinBlocksInSameParent,
+ DeleteBRElement,
+ DeleteContentInRanges,
+ DeleteNonCollapsedRanges,
+ };
+ AutoDeleteRangesHandler* mDeleteRangesHandler;
+ const AutoDeleteRangesHandler& mDeleteRangesHandlerConst;
+ nsCOMPtr<nsIContent> mLeftContent;
+ nsCOMPtr<nsIContent> mRightContent;
+ nsCOMPtr<nsIContent> mLeafContentInOtherBlock;
+ // mSkippedInvisibleContents stores all content nodes which are skipped at
+ // scanning mLeftContent and mRightContent. The content nodes should be
+ // removed at deletion.
+ AutoTArray<OwningNonNull<nsIContent>, 8> mSkippedInvisibleContents;
+ RefPtr<dom::HTMLBRElement> mBRElement;
+ Mode mMode = Mode::NotInitialized;
+ }; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner
+
+ class MOZ_STACK_CLASS AutoEmptyBlockAncestorDeleter final {
+ public:
+ /**
+ * ScanEmptyBlockInclusiveAncestor() scans an inclusive ancestor element
+ * which is empty and a block element. Then, stores the result and
+ * returns the found empty block element.
+ *
+ * @param aHTMLEditor The HTMLEditor.
+ * @param aStartContent Start content to look for empty ancestors.
+ */
+ [[nodiscard]] Element* ScanEmptyBlockInclusiveAncestor(
+ const HTMLEditor& aHTMLEditor, nsIContent& aStartContent);
+
+ /**
+ * ComputeTargetRanges() computes "target ranges" for deleting
+ * `mEmptyInclusiveAncestorBlockElement`.
+ */
+ nsresult ComputeTargetRanges(const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ const Element& aEditingHost,
+ AutoRangeArray& aRangesToDelete) const;
+
+ /**
+ * Deletes found empty block element by `ScanEmptyBlockInclusiveAncestor()`.
+ * If found one is a list item element, calls
+ * `MaybeInsertBRElementBeforeEmptyListItemElement()` before deleting
+ * the list item element.
+ * If found empty ancestor is not a list item element,
+ * `GetNewCaretPosition()` will be called to determine new caret position.
+ * Finally, removes the empty block ancestor.
+ *
+ * @param aHTMLEditor The HTMLEditor.
+ * @param aDirectionAndAmount If found empty ancestor block is a list item
+ * element, this is ignored. Otherwise:
+ * - If eNext, eNextWord or eToEndOfLine,
+ * collapse Selection to after found empty
+ * ancestor.
+ * - If ePrevious, ePreviousWord or
+ * eToBeginningOfLine, collapse Selection to
+ * end of previous editable node.
+ * - Otherwise, eNone is allowed but does
+ * nothing.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount);
+
+ private:
+ /**
+ * MaybeReplaceSubListWithNewListItem() replaces
+ * mEmptyInclusiveAncestorBlockElement with new list item element
+ * (containing <br>) if:
+ * - mEmptyInclusiveAncestorBlockElement is a list element
+ * - The parent of mEmptyInclusiveAncestorBlockElement is a list element
+ * - The parent becomes empty after deletion
+ * If this does not perform the replacement, returns "ignored".
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ MaybeReplaceSubListWithNewListItem(HTMLEditor& aHTMLEditor);
+
+ /**
+ * MaybeInsertBRElementBeforeEmptyListItemElement() inserts a `<br>` element
+ * if `mEmptyInclusiveAncestorBlockElement` is a list item element which
+ * is first editable element in its parent, and its grand parent is not a
+ * list element, inserts a `<br>` element before the empty list item.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
+ MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor);
+
+ /**
+ * GetNewCaretPosition() returns new caret position after deleting
+ * `mEmptyInclusiveAncestorBlockElement`.
+ */
+ [[nodiscard]] Result<CaretPoint, nsresult> GetNewCaretPosition(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount) const;
+
+ RefPtr<Element> mEmptyInclusiveAncestorBlockElement;
+ }; // HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter
+
+ const AutoDeleteRangesHandler* const mParent;
+ nsIEditor::EDirection mOriginalDirectionAndAmount;
+ nsIEditor::EStripWrappers mOriginalStripWrappers;
+}; // HTMLEditor::AutoDeleteRangesHandler
+
+nsresult HTMLEditor::ComputeTargetRanges(
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Element* editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ aRangesToDelete.RemoveAllRanges();
+ return NS_ERROR_EDITOR_NO_EDITABLE_RANGE;
+ }
+
+ // First check for table selection mode. If so, hand off to table editor.
+ SelectedTableCellScanner scanner(aRangesToDelete);
+ if (scanner.IsInTableCellSelectionMode()) {
+ // If it's in table cell selection mode, we'll delete all childen in
+ // the all selected table cell elements,
+ if (scanner.ElementsRef().Length() == aRangesToDelete.Ranges().Length()) {
+ return NS_OK;
+ }
+ // but will ignore all ranges which does not select a table cell.
+ size_t removedRanges = 0;
+ for (size_t i = 1; i < scanner.ElementsRef().Length(); i++) {
+ if (HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(
+ aRangesToDelete.Ranges()[i - removedRanges]) !=
+ scanner.ElementsRef()[i]) {
+ // XXX Need to manage anchor-focus range too!
+ aRangesToDelete.Ranges().RemoveElementAt(i - removedRanges);
+ removedRanges++;
+ }
+ }
+ return NS_OK;
+ }
+
+ aRangesToDelete.EnsureOnlyEditableRanges(*editingHost);
+ if (aRangesToDelete.Ranges().IsEmpty()) {
+ NS_WARNING(
+ "There is no range which we can delete entire of or around the caret");
+ return NS_ERROR_EDITOR_NO_EDITABLE_RANGE;
+ }
+ AutoDeleteRangesHandler deleteHandler;
+ // Should we delete target ranges which cannot delete actually?
+ nsresult rv = deleteHandler.ComputeRangesToDelete(
+ *this, aDirectionAndAmount, aRangesToDelete, *editingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::ComputeRangesToDelete() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::HandleDeleteSelection(
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
+ aStripWrappers == nsIEditor::eNoStrip);
+
+ if (MOZ_UNLIKELY(!SelectionRef().RangeCount())) {
+ return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
+ }
+
+ RefPtr<Element> editingHost = ComputeEditingHost();
+ if (MOZ_UNLIKELY(!editingHost)) {
+ return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
+ }
+
+ // Remember that we did a selection deletion. Used by
+ // CreateStyleForInsertText()
+ TopLevelEditSubActionDataRef().mDidDeleteSelection = true;
+
+ if (MOZ_UNLIKELY(IsEmpty())) {
+ return EditActionResult::CanceledResult();
+ }
+
+ // First check for table selection mode. If so, hand off to table editor.
+ if (HTMLEditUtils::IsInTableCellSelectionMode(SelectionRef())) {
+ nsresult rv = DeleteTableCellContentsWithTransaction();
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteTableCellContentsWithTransaction() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ AutoRangeArray rangesToDelete(SelectionRef());
+ rangesToDelete.EnsureOnlyEditableRanges(*editingHost);
+ if (MOZ_UNLIKELY(rangesToDelete.Ranges().IsEmpty())) {
+ NS_WARNING(
+ "There is no range which we can delete entire the ranges or around the "
+ "caret");
+ return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
+ }
+ AutoDeleteRangesHandler deleteHandler;
+ Result<EditActionResult, nsresult> result = deleteHandler.Run(
+ *this, aDirectionAndAmount, aStripWrappers, rangesToDelete, *editingHost);
+ if (MOZ_UNLIKELY(result.isErr()) || result.inspect().Canceled()) {
+ NS_WARNING_ASSERTION(result.isOk(),
+ "AutoDeleteRangesHandler::Run() failed");
+ return result;
+ }
+
+ // XXX At here, selection may have no range because of mutation event
+ // listeners can do anything so that we should just return NS_OK instead
+ // of returning error.
+ const auto atNewStartOfSelection =
+ GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!atNewStartOfSelection.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (atNewStartOfSelection.IsInContentNode()) {
+ nsresult rv = DeleteMostAncestorMailCiteElementIfEmpty(
+ MOZ_KnownLive(*atNewStartOfSelection.ContainerAs<nsIContent>()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::DeleteMostAncestorMailCiteElementIfEmpty() failed");
+ return Err(rv);
+ }
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete, const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
+
+ mOriginalDirectionAndAmount = aDirectionAndAmount;
+ mOriginalStripWrappers = nsIEditor::eNoStrip;
+
+ if (aHTMLEditor.mPaddingBRElementForEmptyEditor) {
+ nsresult rv = aRangesToDelete.Collapse(
+ EditorRawDOMPoint(aHTMLEditor.mPaddingBRElementForEmptyEditor));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+
+ SelectionWasCollapsed selectionWasCollapsed = aRangesToDelete.IsCollapsed()
+ ? SelectionWasCollapsed::Yes
+ : SelectionWasCollapsed::No;
+ if (selectionWasCollapsed == SelectionWasCollapsed::Yes) {
+ const auto startPoint =
+ aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!startPoint.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (startPoint.IsInContentNode()) {
+ AutoEmptyBlockAncestorDeleter deleter;
+ if (deleter.ScanEmptyBlockInclusiveAncestor(
+ aHTMLEditor, *startPoint.ContainerAs<nsIContent>())) {
+ nsresult rv = deleter.ComputeTargetRanges(
+ aHTMLEditor, aDirectionAndAmount, *editingHost, aRangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoEmptyBlockAncestorDeleter::ComputeTargetRanges() failed");
+ return rv;
+ }
+ }
+
+ // We shouldn't update caret bidi level right now, but we need to check
+ // whether the deletion will be canceled or not.
+ AutoCaretBidiLevelManager bidiLevelManager(aHTMLEditor, aDirectionAndAmount,
+ startPoint);
+ if (bidiLevelManager.Failed()) {
+ NS_WARNING(
+ "EditorBase::AutoCaretBidiLevelManager failed to initialize itself");
+ return NS_ERROR_FAILURE;
+ }
+ if (bidiLevelManager.Canceled()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ // AutoRangeArray::ExtendAnchorFocusRangeFor() will use `nsFrameSelection`
+ // to extend the range for deletion. But if focus event doesn't receive
+ // yet, ancestor isn't set. So we must set root element of editor to
+ // ancestor temporarily.
+ AutoSetTemporaryAncestorLimiter autoSetter(
+ aHTMLEditor, aHTMLEditor.SelectionRef(), *startPoint.GetContainer(),
+ &aRangesToDelete);
+
+ Result<nsIEditor::EDirection, nsresult> extendResult =
+ aRangesToDelete.ExtendAnchorFocusRangeFor(aHTMLEditor,
+ aDirectionAndAmount);
+ if (extendResult.isErr()) {
+ NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
+ return extendResult.unwrapErr();
+ }
+
+ // For compatibility with other browsers, we should set target ranges
+ // to start from and/or end after an atomic content rather than start
+ // from preceding text node end nor end at following text node start.
+ Result<bool, nsresult> shrunkenResult =
+ aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent(
+ aHTMLEditor, aDirectionAndAmount,
+ AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse,
+ editingHost);
+ if (shrunkenResult.isErr()) {
+ NS_WARNING(
+ "AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent() "
+ "failed");
+ return shrunkenResult.unwrapErr();
+ }
+
+ if (!shrunkenResult.inspect() || !aRangesToDelete.IsCollapsed()) {
+ aDirectionAndAmount = extendResult.unwrap();
+ }
+
+ if (aDirectionAndAmount == nsIEditor::eNone) {
+ MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
+ if (!CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)) {
+ // XXX In this case, do we need to modify the range again?
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ nsresult rv = FallbackToComputeRangesToDeleteRangesWithTransaction(
+ aHTMLEditor, aRangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::"
+ "FallbackToComputeRangesToDeleteRangesWithTransaction() failed");
+ return rv;
+ }
+
+ if (aRangesToDelete.IsCollapsed()) {
+ const auto caretPoint =
+ aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (MOZ_UNLIKELY(NS_WARN_IF(!caretPoint.IsInContentNode()))) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!EditorUtils::IsEditableContent(*caretPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML)) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ WSRunScanner wsRunScannerAtCaret(
+ editingHost, caretPoint,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ WSScanResult scanFromCaretPointResult =
+ aDirectionAndAmount == nsIEditor::eNext
+ ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
+ caretPoint)
+ : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ caretPoint);
+ if (scanFromCaretPointResult.Failed()) {
+ NS_WARNING(
+ "WSRunScanner::Scan(Next|Previous)VisibleNodeOrBlockBoundaryFrom() "
+ "failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (!scanFromCaretPointResult.GetContent()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ if (scanFromCaretPointResult.ReachedBRElement()) {
+ if (scanFromCaretPointResult.BRElementPtr() ==
+ wsRunScannerAtCaret.GetEditingHost()) {
+ return NS_OK;
+ }
+ if (!EditorUtils::IsEditableContent(
+ *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ if (HTMLEditUtils::IsInvisibleBRElement(
+ *scanFromCaretPointResult.BRElementPtr())) {
+ EditorDOMPoint newCaretPosition =
+ aDirectionAndAmount == nsIEditor::eNext
+ ? EditorDOMPoint::After(
+ *scanFromCaretPointResult.BRElementPtr())
+ : EditorDOMPoint(scanFromCaretPointResult.BRElementPtr());
+ if (NS_WARN_IF(!newCaretPosition.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ AutoHideSelectionChanges blockSelectionListeners(
+ aHTMLEditor.SelectionRef());
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPosition);
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_WARN_IF(!aHTMLEditor.SelectionRef().RangeCount())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ aRangesToDelete.Initialize(aHTMLEditor.SelectionRef());
+ AutoDeleteRangesHandler anotherHandler(this);
+ rv = anotherHandler.ComputeRangesToDelete(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Recursive AutoDeleteRangesHandler::ComputeRangesToDelete() "
+ "failed");
+
+ rv = aHTMLEditor.CollapseSelectionTo(caretPoint);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed to "
+ "restore original selection, but ignored");
+
+ MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
+ // If the range is collapsed, there is no content which should
+ // be removed together. In this case, only the invisible `<br>`
+ // element should be selected.
+ if (aRangesToDelete.IsCollapsed()) {
+ nsresult rv = aRangesToDelete.SelectNode(
+ *scanFromCaretPointResult.BRElementPtr());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoRangeArray::SelectNode() failed");
+ return rv;
+ }
+
+ // Otherwise, extend the range to contain the invisible `<br>`
+ // element.
+ if (EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr())
+ .IsBefore(
+ aRangesToDelete
+ .GetFirstRangeStartPoint<EditorRawDOMPoint>())) {
+ nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
+ EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr())
+ .ToRawRangeBoundary(),
+ aRangesToDelete.FirstRangeRef()->EndRef());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsRange::SetStartAndEnd() failed");
+ return rv;
+ }
+ if (aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>()
+ .IsBefore(EditorRawDOMPoint::After(
+ *scanFromCaretPointResult.BRElementPtr()))) {
+ nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
+ aRangesToDelete.FirstRangeRef()->StartRef(),
+ EditorRawDOMPoint::After(
+ *scanFromCaretPointResult.BRElementPtr())
+ .ToRawRangeBoundary());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsRange::SetStartAndEnd() failed");
+ return rv;
+ }
+ NS_WARNING("Was the invisible `<br>` element selected?");
+ return NS_OK;
+ }
+ }
+
+ nsresult rv = ComputeRangesToDeleteAroundCollapsedRanges(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete,
+ wsRunScannerAtCaret, scanFromCaretPointResult, aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges("
+ ") failed");
+ return rv;
+ }
+ }
+
+ nsresult rv = ComputeRangesToDeleteNonCollapsedRanges(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete, selectionWasCollapsed,
+ aEditingHost);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::"
+ "ComputeRangesToDeleteNonCollapsedRanges() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
+ aStripWrappers == nsIEditor::eNoStrip);
+ MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
+
+ mOriginalDirectionAndAmount = aDirectionAndAmount;
+ mOriginalStripWrappers = aStripWrappers;
+
+ if (MOZ_UNLIKELY(aHTMLEditor.IsEmpty())) {
+ return EditActionResult::CanceledResult();
+ }
+
+ // selectionWasCollapsed is used later to determine whether we should join
+ // blocks in HandleDeleteNonCollapsedRanges(). We don't really care about
+ // collapsed because it will be modified by
+ // AutoRangeArray::ExtendAnchorFocusRangeFor() later.
+ // AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner should
+ // happen if the original selection is collapsed and the cursor is at the end
+ // of a block element, in which case
+ // AutoRangeArray::ExtendAnchorFocusRangeFor() would always make the selection
+ // not collapsed.
+ SelectionWasCollapsed selectionWasCollapsed = aRangesToDelete.IsCollapsed()
+ ? SelectionWasCollapsed::Yes
+ : SelectionWasCollapsed::No;
+
+ if (selectionWasCollapsed == SelectionWasCollapsed::Yes) {
+ const auto startPoint =
+ aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!startPoint.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // If we are inside an empty block, delete it.
+ if (startPoint.IsInContentNode()) {
+#ifdef DEBUG
+ nsMutationGuard debugMutation;
+#endif // #ifdef DEBUG
+ AutoEmptyBlockAncestorDeleter deleter;
+ if (deleter.ScanEmptyBlockInclusiveAncestor(
+ aHTMLEditor, *startPoint.ContainerAs<nsIContent>())) {
+ Result<EditActionResult, nsresult> result =
+ deleter.Run(aHTMLEditor, aDirectionAndAmount);
+ if (MOZ_UNLIKELY(result.isErr()) || result.inspect().Handled()) {
+ NS_WARNING_ASSERTION(result.isOk(),
+ "AutoEmptyBlockAncestorDeleter::Run() failed");
+ return result;
+ }
+ }
+ MOZ_ASSERT(!debugMutation.Mutated(0),
+ "AutoEmptyBlockAncestorDeleter shouldn't modify the DOM tree "
+ "if it returns not handled nor error");
+ }
+
+ // Test for distance between caret and text that will be deleted.
+ // Note that this call modifies `nsFrameSelection` without modifying
+ // `Selection`. However, it does not have problem for now because
+ // it'll be referred by `AutoRangeArray::ExtendAnchorFocusRangeFor()`
+ // before modifying `Selection`.
+ // XXX This looks odd. `ExtendAnchorFocusRangeFor()` will extend
+ // anchor-focus range, but here refers the first range.
+ AutoCaretBidiLevelManager bidiLevelManager(aHTMLEditor, aDirectionAndAmount,
+ startPoint);
+ if (MOZ_UNLIKELY(bidiLevelManager.Failed())) {
+ NS_WARNING(
+ "EditorBase::AutoCaretBidiLevelManager failed to initialize itself");
+ return Err(NS_ERROR_FAILURE);
+ }
+ bidiLevelManager.MaybeUpdateCaretBidiLevel(aHTMLEditor);
+ if (bidiLevelManager.Canceled()) {
+ return EditActionResult::CanceledResult();
+ }
+
+ // AutoRangeArray::ExtendAnchorFocusRangeFor() will use `nsFrameSelection`
+ // to extend the range for deletion. But if focus event doesn't receive
+ // yet, ancestor isn't set. So we must set root element of editor to
+ // ancestor temporarily.
+ AutoSetTemporaryAncestorLimiter autoSetter(
+ aHTMLEditor, aHTMLEditor.SelectionRef(), *startPoint.GetContainer(),
+ &aRangesToDelete);
+
+ // Calling `ExtendAnchorFocusRangeFor()` and
+ // `ShrinkRangesIfStartFromOrEndAfterAtomicContent()` may move caret to
+ // the container of deleting atomic content. However, it may be different
+ // from the original caret's container. The original caret container may
+ // be important to put caret after deletion so that let's cache the
+ // original position.
+ Maybe<EditorDOMPoint> caretPoint;
+ if (aRangesToDelete.IsCollapsed() && !aRangesToDelete.Ranges().IsEmpty()) {
+ caretPoint =
+ Some(aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>());
+ if (NS_WARN_IF(!caretPoint.ref().IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ Result<nsIEditor::EDirection, nsresult> extendResult =
+ aRangesToDelete.ExtendAnchorFocusRangeFor(aHTMLEditor,
+ aDirectionAndAmount);
+ if (MOZ_UNLIKELY(extendResult.isErr())) {
+ NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
+ return extendResult.propagateErr();
+ }
+ if (caretPoint.isSome() &&
+ MOZ_UNLIKELY(!caretPoint.ref().IsSetAndValid())) {
+ NS_WARNING("The caret position became invalid");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // If there is only one range and it selects an atomic content, we should
+ // delete it with collapsed range path for making consistent behavior
+ // between both cases, the content is selected case and caret is at it or
+ // after it case.
+ Result<bool, nsresult> shrunkenResult =
+ aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent(
+ aHTMLEditor, aDirectionAndAmount,
+ AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse,
+ &aEditingHost);
+ if (MOZ_UNLIKELY(shrunkenResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent() "
+ "failed");
+ return shrunkenResult.propagateErr();
+ }
+
+ if (!shrunkenResult.inspect() || !aRangesToDelete.IsCollapsed()) {
+ aDirectionAndAmount = extendResult.unwrap();
+ }
+
+ if (aDirectionAndAmount == nsIEditor::eNone) {
+ MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
+ if (!CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)) {
+ return EditActionResult::IgnoredResult();
+ }
+ Result<CaretPoint, nsresult> caretPointOrError =
+ FallbackToDeleteRangesWithTransaction(aHTMLEditor, aRangesToDelete);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::FallbackToDeleteRangesWithTransaction() "
+ "failed");
+ }
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ // Don't return "ignored" to avoid to fall it back to delete ranges
+ // recursively.
+ return EditActionResult::HandledResult();
+ }
+
+ if (aRangesToDelete.IsCollapsed()) {
+ // Use the original caret position for handling the deletion around
+ // collapsed range because the container may be different from the
+ // new collapsed position's container.
+ if (!EditorUtils::IsEditableContent(
+ *caretPoint.ref().ContainerAs<nsIContent>(), EditorType::HTML)) {
+ return EditActionResult::CanceledResult();
+ }
+ WSRunScanner wsRunScannerAtCaret(
+ &aEditingHost, caretPoint.ref(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ WSScanResult scanFromCaretPointResult =
+ aDirectionAndAmount == nsIEditor::eNext
+ ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
+ caretPoint.ref())
+ : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ caretPoint.ref());
+ if (MOZ_UNLIKELY(scanFromCaretPointResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::Scan(Next|Previous)VisibleNodeOrBlockBoundaryFrom() "
+ "failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (!scanFromCaretPointResult.GetContent()) {
+ return EditActionResult::CanceledResult();
+ }
+ // Short circuit for invisible breaks. delete them and recurse.
+ if (scanFromCaretPointResult.ReachedBRElement()) {
+ if (scanFromCaretPointResult.BRElementPtr() == &aEditingHost) {
+ return EditActionResult::HandledResult();
+ }
+ if (!EditorUtils::IsEditableContent(
+ *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) {
+ return EditActionResult::CanceledResult();
+ }
+ if (HTMLEditUtils::IsInvisibleBRElement(
+ *scanFromCaretPointResult.BRElementPtr())) {
+ // TODO: We should extend the range to delete again before/after
+ // the caret point and use `HandleDeleteNonCollapsedRanges()`
+ // instead after we would create delete range computation
+ // method at switching to the new white-space normalizer.
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::
+ DeleteContentNodeAndJoinTextNodesAroundIt(
+ aHTMLEditor,
+ MOZ_KnownLive(*scanFromCaretPointResult.BRElementPtr()),
+ caretPoint.ref(), aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "DeleteContentNodeAndJoinTextNodesAroundIt() failed");
+ return caretPointOrError.propagateErr();
+ }
+ if (caretPointOrError.inspect().HasCaretPointSuggestion()) {
+ caretPoint = Some(caretPointOrError.unwrap().UnwrapCaretPoint());
+ }
+ if (NS_WARN_IF(!caretPoint->IsSetAndValid())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ AutoRangeArray rangesToDelete(caretPoint.ref());
+ if (aHTMLEditor.MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT)) {
+ // Let's check whether there is new invisible `<br>` element
+ // for avoiding infinite recursive calls.
+ WSRunScanner wsRunScannerAtCaret(
+ &aEditingHost, caretPoint.ref(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ WSScanResult scanFromCaretPointResult =
+ aDirectionAndAmount == nsIEditor::eNext
+ ? wsRunScannerAtCaret
+ .ScanNextVisibleNodeOrBlockBoundaryFrom(
+ caretPoint.ref())
+ : wsRunScannerAtCaret
+ .ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ caretPoint.ref());
+ if (MOZ_UNLIKELY(scanFromCaretPointResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::Scan(Next|Previous)"
+ "VisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (MOZ_UNLIKELY(
+ scanFromCaretPointResult.ReachedInvisibleBRElement())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+ AutoDeleteRangesHandler anotherHandler(this);
+ Result<EditActionResult, nsresult> result =
+ anotherHandler.Run(aHTMLEditor, aDirectionAndAmount,
+ aStripWrappers, rangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(
+ result.isOk(), "Recursive AutoDeleteRangesHandler::Run() failed");
+ return result;
+ }
+ }
+
+ Result<EditActionResult, nsresult> result =
+ HandleDeleteAroundCollapsedRanges(
+ aHTMLEditor, aDirectionAndAmount, aStripWrappers, aRangesToDelete,
+ wsRunScannerAtCaret, scanFromCaretPointResult, aEditingHost);
+ NS_WARNING_ASSERTION(result.isOk(),
+ "AutoDeleteRangesHandler::"
+ "HandleDeleteAroundCollapsedRanges() failed");
+ return result;
+ }
+ }
+
+ Result<EditActionResult, nsresult> result = HandleDeleteNonCollapsedRanges(
+ aHTMLEditor, aDirectionAndAmount, aStripWrappers, aRangesToDelete,
+ selectionWasCollapsed, aEditingHost);
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges() failed");
+ return result;
+}
+
+nsresult
+HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete, const WSRunScanner& aWSRunScannerAtCaret,
+ const WSScanResult& aScanFromCaretPointResult,
+ const Element& aEditingHost) const {
+ if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() ||
+ aScanFromCaretPointResult.InNonCollapsibleCharacters() ||
+ aScanFromCaretPointResult.ReachedPreformattedLineBreak()) {
+ nsresult rv = aRangesToDelete.Collapse(
+ aScanFromCaretPointResult.Point<EditorRawDOMPoint>());
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return NS_ERROR_FAILURE;
+ }
+ rv = ComputeRangesToDeleteTextAroundCollapsedRanges(
+ aDirectionAndAmount, aRangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::"
+ "ComputeRangesToDeleteTextAroundCollapsedRanges() failed");
+ return rv;
+ }
+
+ if (aScanFromCaretPointResult.ReachedSpecialContent() ||
+ aScanFromCaretPointResult.ReachedBRElement() ||
+ aScanFromCaretPointResult.ReachedHRElement() ||
+ aScanFromCaretPointResult.ReachedNonEditableOtherBlockElement()) {
+ if (aScanFromCaretPointResult.GetContent() ==
+ aWSRunScannerAtCaret.GetEditingHost()) {
+ return NS_OK;
+ }
+ nsIContent* atomicContent = GetAtomicContentToDelete(
+ aDirectionAndAmount, aWSRunScannerAtCaret, aScanFromCaretPointResult);
+ if (!HTMLEditUtils::IsRemovableNode(*atomicContent)) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::GetAtomicContentToDelete() cannot find "
+ "removable atomic content");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = ComputeRangesToDeleteAtomicContent(
+ aWSRunScannerAtCaret.GetEditingHost(), *atomicContent, aRangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::ComputeRangesToDeleteAtomicContent() failed");
+ return rv;
+ }
+
+ if (aScanFromCaretPointResult.ReachedOtherBlockElement()) {
+ if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) {
+ return NS_ERROR_FAILURE;
+ }
+ AutoBlockElementsJoiner joiner(*this);
+ if (!joiner.PrepareToDeleteAtOtherBlockBoundary(
+ aHTMLEditor, aDirectionAndAmount,
+ *aScanFromCaretPointResult.ElementPtr(),
+ aWSRunScannerAtCaret.ScanStartRef(), aWSRunScannerAtCaret)) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ nsresult rv = joiner.ComputeRangesToDelete(
+ aHTMLEditor, aDirectionAndAmount, aWSRunScannerAtCaret.ScanStartRef(),
+ aRangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::ComputeRangesToDelete() "
+ "failed (other block boundary)");
+ return rv;
+ }
+
+ if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) {
+ if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) {
+ return NS_ERROR_FAILURE;
+ }
+ AutoBlockElementsJoiner joiner(*this);
+ if (!joiner.PrepareToDeleteAtCurrentBlockBoundary(
+ aHTMLEditor, aDirectionAndAmount,
+ *aScanFromCaretPointResult.ElementPtr(),
+ aWSRunScannerAtCaret.ScanStartRef())) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ nsresult rv = joiner.ComputeRangesToDelete(
+ aHTMLEditor, aDirectionAndAmount, aWSRunScannerAtCaret.ScanStartRef(),
+ aRangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::ComputeRangesToDelete() "
+ "failed (current block boundary)");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
+ const WSRunScanner& aWSRunScannerAtCaret,
+ const WSScanResult& aScanFromCaretPointResult,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(aRangesToDelete.IsCollapsed());
+ MOZ_ASSERT(aDirectionAndAmount != nsIEditor::eNone);
+ MOZ_ASSERT(aWSRunScannerAtCaret.ScanStartRef().IsInContentNode());
+ MOZ_ASSERT(EditorUtils::IsEditableContent(
+ *aWSRunScannerAtCaret.ScanStartRef().ContainerAs<nsIContent>(),
+ EditorType::HTML));
+
+ if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
+ if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() ||
+ aScanFromCaretPointResult.InNonCollapsibleCharacters() ||
+ aScanFromCaretPointResult.ReachedPreformattedLineBreak()) {
+ nsresult rv = aRangesToDelete.Collapse(
+ aScanFromCaretPointResult.Point<EditorRawDOMPoint>());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<CaretPoint, nsresult> caretPointOrError =
+ HandleDeleteTextAroundCollapsedRanges(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::HandleDeleteTextAroundCollapsedRanges() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPoint() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+ }
+
+ if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() ||
+ aScanFromCaretPointResult.ReachedPreformattedLineBreak()) {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ HandleDeleteCollapsedSelectionAtWhiteSpaces(
+ aHTMLEditor, aDirectionAndAmount,
+ aWSRunScannerAtCaret.ScanStartRef(), aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::"
+ "HandleDeleteCollapsedSelectionAtWhiteSpaces() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+
+ if (aScanFromCaretPointResult.InNonCollapsibleCharacters()) {
+ if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsText())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<CaretPoint, nsresult> caretPointOrError =
+ HandleDeleteCollapsedSelectionAtVisibleChar(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete,
+ aScanFromCaretPointResult.Point<EditorDOMPoint>(), aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::"
+ "HandleDeleteCollapsedSelectionAtVisibleChar() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+
+ if (aScanFromCaretPointResult.ReachedSpecialContent() ||
+ aScanFromCaretPointResult.ReachedBRElement() ||
+ aScanFromCaretPointResult.ReachedHRElement() ||
+ aScanFromCaretPointResult.ReachedNonEditableOtherBlockElement()) {
+ if (aScanFromCaretPointResult.GetContent() == &aEditingHost) {
+ return EditActionResult::HandledResult();
+ }
+ nsCOMPtr<nsIContent> atomicContent = GetAtomicContentToDelete(
+ aDirectionAndAmount, aWSRunScannerAtCaret, aScanFromCaretPointResult);
+ if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(*atomicContent))) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::GetAtomicContentToDelete() cannot find "
+ "removable atomic content");
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<CaretPoint, nsresult> caretPointOrError = HandleDeleteAtomicContent(
+ aHTMLEditor, *atomicContent, aWSRunScannerAtCaret.ScanStartRef(),
+ aWSRunScannerAtCaret, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("AutoDeleteRangesHandler::HandleDeleteAtomicContent() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+
+ if (aScanFromCaretPointResult.ReachedOtherBlockElement()) {
+ if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ AutoBlockElementsJoiner joiner(*this);
+ if (!joiner.PrepareToDeleteAtOtherBlockBoundary(
+ aHTMLEditor, aDirectionAndAmount,
+ *aScanFromCaretPointResult.ElementPtr(),
+ aWSRunScannerAtCaret.ScanStartRef(), aWSRunScannerAtCaret)) {
+ return EditActionResult::CanceledResult();
+ }
+ Result<EditActionResult, nsresult> result = joiner.Run(
+ aHTMLEditor, aDirectionAndAmount, aStripWrappers,
+ aWSRunScannerAtCaret.ScanStartRef(), aRangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "AutoBlockElementsJoiner::Run() failed (other block boundary)");
+ return result;
+ }
+
+ if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) {
+ if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ AutoBlockElementsJoiner joiner(*this);
+ if (!joiner.PrepareToDeleteAtCurrentBlockBoundary(
+ aHTMLEditor, aDirectionAndAmount,
+ *aScanFromCaretPointResult.ElementPtr(),
+ aWSRunScannerAtCaret.ScanStartRef())) {
+ return EditActionResult::CanceledResult();
+ }
+ Result<EditActionResult, nsresult> result = joiner.Run(
+ aHTMLEditor, aDirectionAndAmount, aStripWrappers,
+ aWSRunScannerAtCaret.ScanStartRef(), aRangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "AutoBlockElementsJoiner::Run() failed (current block boundary)");
+ return result;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("New type of reached content hasn't been handled yet");
+ return EditActionResult::IgnoredResult();
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::
+ ComputeRangesToDeleteTextAroundCollapsedRanges(
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const {
+ MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext ||
+ aDirectionAndAmount == nsIEditor::ePrevious);
+
+ const auto caretPosition =
+ aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
+ MOZ_ASSERT(caretPosition.IsSetAndValid());
+ if (MOZ_UNLIKELY(NS_WARN_IF(!caretPosition.IsInContentNode()))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorDOMRangeInTexts rangeToDelete;
+ if (aDirectionAndAmount == nsIEditor::eNext) {
+ Result<EditorDOMRangeInTexts, nsresult> result =
+ WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom(caretPosition,
+ aEditingHost);
+ if (result.isErr()) {
+ NS_WARNING(
+ "WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom() failed");
+ return result.unwrapErr();
+ }
+ rangeToDelete = result.unwrap();
+ if (!rangeToDelete.IsPositioned()) {
+ return NS_OK; // no range to delete, but consume it.
+ }
+ } else {
+ Result<EditorDOMRangeInTexts, nsresult> result =
+ WSRunScanner::GetRangeInTextNodesToBackspaceFrom(caretPosition,
+ aEditingHost);
+ if (result.isErr()) {
+ NS_WARNING("WSRunScanner::GetRangeInTextNodesToBackspaceFrom() failed");
+ return result.unwrapErr();
+ }
+ rangeToDelete = result.unwrap();
+ if (!rangeToDelete.IsPositioned()) {
+ return NS_OK; // no range to delete, but consume it.
+ }
+ }
+
+ nsresult rv = aRangesToDelete.SetStartAndEnd(rangeToDelete.StartRef(),
+ rangeToDelete.EndRef());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoArrayRanges::SetStartAndEnd() failed");
+ return rv;
+}
+
+Result<CaretPoint, nsresult>
+HTMLEditor::AutoDeleteRangesHandler::HandleDeleteTextAroundCollapsedRanges(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete, const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext ||
+ aDirectionAndAmount == nsIEditor::ePrevious);
+
+ nsresult rv = ComputeRangesToDeleteTextAroundCollapsedRanges(
+ aDirectionAndAmount, aRangesToDelete, aEditingHost);
+ if (NS_FAILED(rv)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed())) {
+ return CaretPoint(EditorDOMPoint()); // no range to delete
+ }
+
+ // FYI: rangeToDelete does not contain newly empty inline ancestors which
+ // are removed by DeleteTextAndNormalizeSurroundingWhiteSpaces().
+ // So, if `getTargetRanges()` needs to include parent empty elements,
+ // we need to extend the range with
+ // HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement().
+ EditorRawDOMRange rangeToDelete(aRangesToDelete.FirstRangeRef());
+ if (MOZ_UNLIKELY(!rangeToDelete.IsInTextNodes())) {
+ NS_WARNING("The extended range to delete character was not in text nodes");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndNormalizeSurroundingWhiteSpaces(
+ rangeToDelete.StartRef().AsInText(),
+ rangeToDelete.EndRef().AsInText(),
+ TreatEmptyTextNodes::RemoveAllEmptyInlineAncestors,
+ aDirectionAndAmount == nsIEditor::eNext ? DeleteDirection::Forward
+ : DeleteDirection::Backward);
+ aHTMLEditor.TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces = true;
+ NS_WARNING_ASSERTION(
+ caretPointOrError.isOk(),
+ "HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces() failed");
+ return caretPointOrError;
+}
+
+Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ HandleDeleteCollapsedSelectionAtWhiteSpaces(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aPointToDelete, const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible());
+
+ EditorDOMPoint pointToPutCaret;
+ if (aDirectionAndAmount == nsIEditor::eNext) {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace(
+ aHTMLEditor, aPointToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace() failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, aHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ } else {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace(
+ aHTMLEditor, aPointToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace() failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, aHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+ const auto newCaretPosition =
+ aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (MOZ_UNLIKELY(!newCaretPosition.IsSet())) {
+ NS_WARNING("There was no selection range");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ newCaretPosition);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()"
+ " failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return CaretPoint(std::move(pointToPutCaret));
+}
+
+Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ HandleDeleteCollapsedSelectionAtVisibleChar(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete,
+ const EditorDOMPoint& aPointAtDeletingChar,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible());
+ MOZ_ASSERT(aPointAtDeletingChar.IsSet());
+ MOZ_ASSERT(aPointAtDeletingChar.IsInTextNode());
+
+ OwningNonNull<Text> visibleTextNode =
+ *aPointAtDeletingChar.ContainerAs<Text>();
+ EditorDOMPoint startToDelete, endToDelete;
+ // FIXME: This does not care grapheme cluster of complicate character
+ // sequence like Emoji.
+ // TODO: Investigate what happens if a grapheme cluster which should be
+ // delete once is split to multiple text nodes.
+ // TODO: We should stop using this path, instead, we should extend the range
+ // before calling this method.
+ if (aDirectionAndAmount == nsIEditor::ePrevious) {
+ if (MOZ_UNLIKELY(aPointAtDeletingChar.IsStartOfContainer())) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ startToDelete = aPointAtDeletingChar.PreviousPoint();
+ endToDelete = aPointAtDeletingChar;
+ // Bug 1068979: delete both codepoints if surrogate pair
+ if (!startToDelete.IsStartOfContainer()) {
+ const nsTextFragment* text = &visibleTextNode->TextFragment();
+ if (text->IsLowSurrogateFollowingHighSurrogateAt(
+ startToDelete.Offset())) {
+ startToDelete.RewindOffset();
+ }
+ }
+ } else {
+ if (NS_WARN_IF(aRangesToDelete.Ranges().IsEmpty()) ||
+ NS_WARN_IF(aRangesToDelete.FirstRangeRef()->GetStartContainer() !=
+ aPointAtDeletingChar.GetContainer()) ||
+ NS_WARN_IF(aRangesToDelete.FirstRangeRef()->GetEndContainer() !=
+ aPointAtDeletingChar.GetContainer())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ startToDelete = aRangesToDelete.FirstRangeRef()->StartRef();
+ endToDelete = aRangesToDelete.FirstRangeRef()->EndRef();
+ }
+
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
+ aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret position because we'll set caret position below
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+
+ if (aHTMLEditor.MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_NODEREMOVED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
+ NS_EVENT_BITS_MUTATION_ATTRMODIFIED |
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) &&
+ (NS_WARN_IF(!startToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!startToDelete.IsInTextNode()) ||
+ NS_WARN_IF(!endToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!endToDelete.IsInTextNode()) ||
+ NS_WARN_IF(startToDelete.ContainerAs<Text>() != visibleTextNode) ||
+ NS_WARN_IF(endToDelete.ContainerAs<Text>() != visibleTextNode) ||
+ NS_WARN_IF(startToDelete.Offset() >= endToDelete.Offset()))) {
+ NS_WARNING("Mutation event listener changed the DOM tree");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ EditorDOMPoint pointToPutCaret = startToDelete;
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextWithTransaction(
+ visibleTextNode, startToDelete.Offset(),
+ endToDelete.Offset() - startToDelete.Offset());
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, aHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+
+ // XXX When Backspace key is pressed, Chromium removes following empty
+ // text nodes when removing the last character of the non-empty text
+ // node. However, Edge never removes empty text nodes even if
+ // selection is in the following empty text node(s). For now, we
+ // should keep our traditional behavior same as Edge for backward
+ // compatibility.
+ // XXX When Delete key is pressed, Edge removes all preceding empty
+ // text nodes when removing the first character of the non-empty
+ // text node. Chromium removes only selected empty text node and
+ // following empty text nodes and the first character of the
+ // non-empty text node. For now, we should keep our traditional
+ // behavior same as Chromium for backward compatibility.
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ nsresult rv =
+ DeleteNodeIfInvisibleAndEditableTextNode(aHTMLEditor, visibleTextNode);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode() "
+ "failed, but ignored");
+ }
+
+ if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // XXX `Selection` may be modified by mutation event listeners so
+ // that we should use EditorDOMPoint::AtEndOf(visibleTextNode)
+ // instead. (Perhaps, we don't and/or shouldn't need to do this
+ // if the text node is preformatted.)
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ pointToPutCaret);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()"
+ " failed");
+ return caretPointOrError.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ // Remember that we did a ranged delete for the benefit of
+ // AfterEditInner().
+ aHTMLEditor.TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange = true;
+ return CaretPoint(std::move(pointToPutCaret));
+}
+
+// static
+nsIContent* HTMLEditor::AutoDeleteRangesHandler::GetAtomicContentToDelete(
+ nsIEditor::EDirection aDirectionAndAmount,
+ const WSRunScanner& aWSRunScannerAtCaret,
+ const WSScanResult& aScanFromCaretPointResult) {
+ MOZ_ASSERT(aScanFromCaretPointResult.GetContent());
+
+ if (!aScanFromCaretPointResult.ReachedSpecialContent()) {
+ return aScanFromCaretPointResult.GetContent();
+ }
+
+ if (!aScanFromCaretPointResult.GetContent()->IsText() ||
+ HTMLEditUtils::IsRemovableNode(*aScanFromCaretPointResult.GetContent())) {
+ return aScanFromCaretPointResult.GetContent();
+ }
+
+ // aScanFromCaretPointResult is non-removable text node.
+ // Since we try removing atomic content, we look for removable node from
+ // scanned point that is non-removable text.
+ nsIContent* removableRoot = aScanFromCaretPointResult.GetContent();
+ while (removableRoot && !HTMLEditUtils::IsRemovableNode(*removableRoot)) {
+ removableRoot = removableRoot->GetParent();
+ }
+
+ if (removableRoot) {
+ return removableRoot;
+ }
+
+ // Not found better content. This content may not be removable.
+ return aScanFromCaretPointResult.GetContent();
+}
+
+nsresult
+HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAtomicContent(
+ Element* aEditingHost, const nsIContent& aAtomicContent,
+ AutoRangeArray& aRangesToDelete) const {
+ EditorDOMRange rangeToDelete =
+ WSRunScanner::GetRangesForDeletingAtomicContent(aEditingHost,
+ aAtomicContent);
+ if (!rangeToDelete.IsPositioned()) {
+ NS_WARNING("WSRunScanner::GetRangeForDeleteAContentNode() failed");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = aRangesToDelete.SetStartAndEnd(rangeToDelete.StartRef(),
+ rangeToDelete.EndRef());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoRangeArray::SetStartAndEnd() failed");
+ return rv;
+}
+
+Result<CaretPoint, nsresult>
+HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAtomicContent(
+ HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
+ const EditorDOMPoint& aCaretPoint, const WSRunScanner& aWSRunScannerAtCaret,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!HTMLEditUtils::IsInvisibleBRElement(aAtomicContent));
+ MOZ_ASSERT(&aAtomicContent != aWSRunScannerAtCaret.GetEditingHost());
+
+ EditorDOMPoint pointToPutCaret = aCaretPoint;
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt(
+ aHTMLEditor, aAtomicContent, aCaretPoint, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "DeleteContentNodeAndJoinTextNodesAroundIt() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, aHTMLEditor,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ pointToPutCaret);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()"
+ " failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion});
+ if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+}
+
+bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ PrepareToDeleteAtOtherBlockBoundary(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount, Element& aOtherBlockElement,
+ const EditorDOMPoint& aCaretPoint,
+ const WSRunScanner& aWSRunScannerAtCaret) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(aCaretPoint.IsSetAndValid());
+
+ mMode = Mode::JoinOtherBlock;
+
+ // Make sure it's not a table element. If so, cancel the operation
+ // (translation: users cannot backspace or delete across table cells)
+ if (HTMLEditUtils::IsAnyTableElement(&aOtherBlockElement)) {
+ return false;
+ }
+
+ // First find the adjacent node in the block
+ if (aDirectionAndAmount == nsIEditor::ePrevious) {
+ mLeafContentInOtherBlock = HTMLEditUtils::GetLastLeafContent(
+ aOtherBlockElement, {LeafNodeType::OnlyEditableLeafNode},
+ BlockInlineCheck::Unused, &aOtherBlockElement);
+ mLeftContent = mLeafContentInOtherBlock;
+ mRightContent = aCaretPoint.GetContainerAs<nsIContent>();
+ } else {
+ mLeafContentInOtherBlock = HTMLEditUtils::GetFirstLeafContent(
+ aOtherBlockElement, {LeafNodeType::OnlyEditableLeafNode},
+ BlockInlineCheck::Unused, &aOtherBlockElement);
+ mLeftContent = aCaretPoint.GetContainerAs<nsIContent>();
+ mRightContent = mLeafContentInOtherBlock;
+ }
+
+ // Next to a block. See if we are between the block and a `<br>`.
+ // If so, we really want to delete the `<br>`. Else join content at
+ // selection to the block.
+ WSScanResult scanFromCaretResult =
+ aDirectionAndAmount == nsIEditor::eNext
+ ? aWSRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ aCaretPoint)
+ : aWSRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
+ aCaretPoint);
+ // If we found a `<br>` element, we need to delete it instead of joining the
+ // contents.
+ if (scanFromCaretResult.ReachedBRElement()) {
+ mBRElement = scanFromCaretResult.BRElementPtr();
+ mMode = Mode::DeleteBRElement;
+ return true;
+ }
+
+ return mLeftContent && mRightContent;
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ ComputeRangesToDeleteBRElement(AutoRangeArray& aRangesToDelete) const {
+ MOZ_ASSERT(mBRElement);
+ // XXX Why don't we scan invisible leading white-spaces which follows the
+ // `<br>` element?
+ nsresult rv = aRangesToDelete.SelectNode(*mBRElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::SelectNode() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(mBRElement);
+
+ // If we're deleting selection (not replacing with new content), we should
+ // put caret to end of preceding text node if there is. Then, users can type
+ // text in it like the other browsers.
+ EditorDOMPoint pointToPutCaret = [&]() {
+ if (!MayEditActionDeleteAroundCollapsedSelection(
+ aHTMLEditor.GetEditAction())) {
+ return EditorDOMPoint();
+ }
+ WSRunScanner scanner(&aEditingHost, EditorRawDOMPoint(mBRElement),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ WSScanResult maybePreviousText =
+ scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ EditorRawDOMPoint(mBRElement));
+ if (maybePreviousText.IsContentEditable() &&
+ maybePreviousText.InVisibleOrCollapsibleCharacters() &&
+ !HTMLEditor::GetLinkElement(maybePreviousText.TextPtr())) {
+ return maybePreviousText.Point<EditorDOMPoint>();
+ }
+ WSScanResult maybeNextText = scanner.ScanNextVisibleNodeOrBlockBoundaryFrom(
+ EditorRawDOMPoint::After(*mBRElement));
+ if (maybeNextText.IsContentEditable() &&
+ maybeNextText.InVisibleOrCollapsibleCharacters()) {
+ return maybeNextText.Point<EditorDOMPoint>();
+ }
+ return EditorDOMPoint();
+ }();
+
+ // If we found a `<br>` element, we should delete it instead of joining the
+ // contents.
+ nsresult rv =
+ aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(*mBRElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+
+ if (mLeftContent && mRightContent &&
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) !=
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent)) {
+ return EditActionResult::HandledResult();
+ }
+
+ // Put selection at edge of block and we are done.
+ if (NS_WARN_IF(!mLeafContentInOtherBlock)) {
+ // XXX This must be odd case. The other block can be empty.
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_SUCCEEDED(rv)) {
+ // If we prefer to use style in the previous line, we should forget
+ // previous styles since the caret position has all styles which we want
+ // to use with new content.
+ if (nsIEditor::DirectionIsBackspace(aDirectionAndAmount)) {
+ aHTMLEditor.TopLevelEditSubActionDataRef()
+ .mCachedPendingStyles->Clear();
+ }
+ // And we don't want to keep extending a link at ex-end of the previous
+ // paragraph.
+ if (HTMLEditor::GetLinkElement(pointToPutCaret.GetContainer())) {
+ aHTMLEditor.mPendingStylesToApplyToNewContent
+ ->ClearLinkAndItsSpecifiedStyle();
+ }
+ } else {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ EditorRawDOMPoint newCaretPosition =
+ HTMLEditUtils::GetGoodCaretPointFor<EditorRawDOMPoint>(
+ *mLeafContentInOtherBlock, aDirectionAndAmount);
+ if (MOZ_UNLIKELY(!newCaretPosition.IsSet())) {
+ NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ rv = aHTMLEditor.CollapseSelectionTo(newCaretPosition);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ ComputeRangesToDeleteAtOtherBlockBoundary(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete,
+ const Element& aEditingHost) const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(aCaretPoint.IsSetAndValid());
+ MOZ_ASSERT(mLeftContent);
+ MOZ_ASSERT(mRightContent);
+
+ if (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) !=
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent)) {
+ if (!mDeleteRangesHandlerConst.CanFallbackToDeleteRangesWithTransaction(
+ aRangesToDelete)) {
+ nsresult rv = aRangesToDelete.Collapse(aCaretPoint);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+ nsresult rv = mDeleteRangesHandlerConst
+ .FallbackToComputeRangesToDeleteRangesWithTransaction(
+ aHTMLEditor, aRangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::"
+ "FallbackToComputeRangesToDeleteRangesWithTransaction() failed");
+ return rv;
+ }
+
+ AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
+ *mRightContent);
+ Result<bool, nsresult> canJoinThem =
+ joiner.Prepare(aHTMLEditor, aEditingHost);
+ if (canJoinThem.isErr()) {
+ NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
+ return canJoinThem.unwrapErr();
+ }
+ if (canJoinThem.inspect() && joiner.CanJoinBlocks() &&
+ !joiner.ShouldDeleteLeafContentInstead()) {
+ nsresult rv =
+ joiner.ComputeRangesToDelete(aHTMLEditor, aCaretPoint, aRangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoInclusiveAncestorBlockElementsJoiner::ComputeRangesToDelete() "
+ "failed");
+ return rv;
+ }
+
+ // If AutoInclusiveAncestorBlockElementsJoiner didn't handle it and it's not
+ // canceled, user may want to modify the start leaf node or the last leaf
+ // node of the block.
+ if (mLeafContentInOtherBlock == aCaretPoint.GetContainer()) {
+ return NS_OK;
+ }
+
+ AutoHideSelectionChanges hideSelectionChanges(aHTMLEditor.SelectionRef());
+
+ // If it's ignored, it didn't modify the DOM tree. In this case, user must
+ // want to delete nearest leaf node in the other block element.
+ // TODO: We need to consider this before calling ComputeRangesToDelete() for
+ // computing the deleting range.
+ EditorRawDOMPoint newCaretPoint =
+ aDirectionAndAmount == nsIEditor::ePrevious
+ ? EditorRawDOMPoint::AtEndOf(*mLeafContentInOtherBlock)
+ : EditorRawDOMPoint(mLeafContentInOtherBlock, 0);
+ // If new caret position is same as current caret position, we can do
+ // nothing anymore.
+ if (aRangesToDelete.IsCollapsed() &&
+ aRangesToDelete.FocusRef() == newCaretPoint.ToRawRangeBoundary()) {
+ return NS_OK;
+ }
+ // TODO: Stop modifying the `Selection` for computing the targer ranges.
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPoint);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ if (NS_SUCCEEDED(rv)) {
+ aRangesToDelete.Initialize(aHTMLEditor.SelectionRef());
+ AutoDeleteRangesHandler anotherHandler(mDeleteRangesHandlerConst);
+ rv = anotherHandler.ComputeRangesToDelete(aHTMLEditor, aDirectionAndAmount,
+ aRangesToDelete, aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Recursive AutoDeleteRangesHandler::ComputeRangesToDelete() failed");
+ }
+ // Restore selection.
+ nsresult rvCollapsingSelectionTo =
+ aHTMLEditor.CollapseSelectionTo(aCaretPoint);
+ if (MOZ_UNLIKELY(rvCollapsingSelectionTo == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvCollapsingSelectionTo),
+ "EditorBase::CollapseSelectionTo() failed to restore caret position");
+ return NS_SUCCEEDED(rv) && NS_SUCCEEDED(rvCollapsingSelectionTo)
+ ? NS_OK
+ : NS_ERROR_FAILURE;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoBlockElementsJoiner::HandleDeleteAtOtherBlockBoundary(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(aCaretPoint.IsSetAndValid());
+ MOZ_ASSERT(mDeleteRangesHandler);
+ MOZ_ASSERT(mLeftContent);
+ MOZ_ASSERT(mRightContent);
+
+ if (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) !=
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent)) {
+ // If we have not deleted `<br>` element and are not called recursively,
+ // we should call `DeleteRangesWithTransaction()` here.
+ if (!mDeleteRangesHandler->CanFallbackToDeleteRangesWithTransaction(
+ aRangesToDelete)) {
+ return EditActionResult::IgnoredResult();
+ }
+ Result<CaretPoint, nsresult> caretPointOrError =
+ mDeleteRangesHandler->FallbackToDeleteRangesWithTransaction(
+ aHTMLEditor, aRangesToDelete);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::FallbackToDeleteRangesWithTransaction() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ // Don't return "ignored" to avoid to fall it back to delete ranges
+ // recursively.
+ return EditActionResult::HandledResult();
+ }
+
+ // Else we are joining content to block
+ AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
+ *mRightContent);
+ Result<bool, nsresult> canJoinThem =
+ joiner.Prepare(aHTMLEditor, aEditingHost);
+ if (MOZ_UNLIKELY(canJoinThem.isErr())) {
+ NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
+ return canJoinThem.propagateErr();
+ }
+
+ if (!canJoinThem.inspect()) {
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(aCaretPoint);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return EditActionResult::CanceledResult();
+ }
+
+ auto result = EditActionResult::IgnoredResult();
+ EditorDOMPoint pointToPutCaret(aCaretPoint);
+ if (joiner.CanJoinBlocks()) {
+ {
+ AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<EditActionResult, nsresult> joinResult =
+ joiner.Run(aHTMLEditor, aEditingHost);
+ if (MOZ_UNLIKELY(joinResult.isErr())) {
+ NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Run() failed");
+ return joinResult;
+ }
+ result |= joinResult.unwrap();
+#ifdef DEBUG
+ if (joiner.ShouldDeleteLeafContentInstead()) {
+ NS_ASSERTION(
+ result.Ignored(),
+ "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
+ "returning ignored, but returned not ignored");
+ } else {
+ NS_ASSERTION(
+ !result.Ignored(),
+ "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
+ "returning handled, but returned ignored");
+ }
+#endif // #ifdef DEBUG
+ // If we're deleting selection (not replacing with new content) and
+ // AutoInclusiveAncestorBlockElementsJoiner computed new caret position,
+ // we should use it. Otherwise, we should keep the our traditional
+ // behavior.
+ if (result.Handled() && joiner.PointRefToPutCaret().IsSet()) {
+ nsresult rv =
+ aHTMLEditor.CollapseSelectionTo(joiner.PointRefToPutCaret());
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored");
+ return result;
+ }
+ // If we prefer to use style in the previous line, we should forget
+ // previous styles since the caret position has all styles which we want
+ // to use with new content.
+ if (nsIEditor::DirectionIsBackspace(aDirectionAndAmount)) {
+ aHTMLEditor.TopLevelEditSubActionDataRef()
+ .mCachedPendingStyles->Clear();
+ }
+ // And we don't want to keep extending a link at ex-end of the previous
+ // paragraph.
+ if (HTMLEditor::GetLinkElement(
+ joiner.PointRefToPutCaret().GetContainer())) {
+ aHTMLEditor.mPendingStylesToApplyToNewContent
+ ->ClearLinkAndItsSpecifiedStyle();
+ }
+ return result;
+ }
+ }
+
+ // If AutoInclusiveAncestorBlockElementsJoiner didn't handle it and it's not
+ // canceled, user may want to modify the start leaf node or the last leaf
+ // node of the block.
+ if (result.Ignored() &&
+ mLeafContentInOtherBlock != aCaretPoint.GetContainer()) {
+ // If it's ignored, it didn't modify the DOM tree. In this case, user
+ // must want to delete nearest leaf node in the other block element.
+ // TODO: We need to consider this before calling Run() for computing the
+ // deleting range.
+ EditorRawDOMPoint newCaretPoint =
+ aDirectionAndAmount == nsIEditor::ePrevious
+ ? EditorRawDOMPoint::AtEndOf(*mLeafContentInOtherBlock)
+ : EditorRawDOMPoint(mLeafContentInOtherBlock, 0);
+ // If new caret position is same as current caret position, we can do
+ // nothing anymore.
+ if (aRangesToDelete.IsCollapsed() &&
+ aRangesToDelete.FocusRef() == newCaretPoint.ToRawRangeBoundary()) {
+ return EditActionResult::CanceledResult();
+ }
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPoint);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ AutoRangeArray rangesToDelete(aHTMLEditor.SelectionRef());
+ AutoDeleteRangesHandler anotherHandler(mDeleteRangesHandler);
+ Result<EditActionResult, nsresult> fallbackResult =
+ anotherHandler.Run(aHTMLEditor, aDirectionAndAmount, aStripWrappers,
+ rangesToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(fallbackResult.isErr())) {
+ NS_WARNING("Recursive AutoDeleteRangesHandler::Run() failed");
+ return fallbackResult;
+ }
+ result |= fallbackResult.unwrap();
+ return result;
+ }
+ } else {
+ result.MarkAsHandled();
+ }
+
+ // Otherwise, we must have deleted the selection as user expected.
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return result;
+}
+
+bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ PrepareToDeleteAtCurrentBlockBoundary(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+
+ // At edge of our block. Look beside it and see if we can join to an
+ // adjacent block
+ mMode = Mode::JoinCurrentBlock;
+
+ // Don't break the basic structure of the HTML document.
+ if (aCurrentBlockElement.IsAnyOfHTMLElements(nsGkAtoms::html, nsGkAtoms::head,
+ nsGkAtoms::body)) {
+ return false;
+ }
+
+ // Make sure it's not a table element. If so, cancel the operation
+ // (translation: users cannot backspace or delete across table cells)
+ if (HTMLEditUtils::IsAnyTableElement(&aCurrentBlockElement)) {
+ return false;
+ }
+
+ Element* editingHost = aHTMLEditor.ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost)) {
+ return false;
+ }
+
+ auto ScanJoinTarget = [&]() -> nsIContent* {
+ nsIContent* targetContent =
+ aDirectionAndAmount == nsIEditor::ePrevious
+ ? HTMLEditUtils::GetPreviousContent(
+ aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, editingHost)
+ : HTMLEditUtils::GetNextContent(
+ aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, editingHost);
+ // If found content is an invisible text node, let's scan visible things.
+ auto IsIgnorableDataNode = [](nsIContent* aContent) {
+ return aContent && HTMLEditUtils::IsRemovableNode(*aContent) &&
+ ((aContent->IsText() &&
+ aContent->AsText()->TextIsOnlyWhitespace() &&
+ !HTMLEditUtils::IsVisibleTextNode(*aContent->AsText())) ||
+ (aContent->IsCharacterData() && !aContent->IsText()));
+ };
+ if (!IsIgnorableDataNode(targetContent)) {
+ return targetContent;
+ }
+ MOZ_ASSERT(mSkippedInvisibleContents.IsEmpty());
+ for (nsIContent* adjacentContent =
+ aDirectionAndAmount == nsIEditor::ePrevious
+ ? HTMLEditUtils::GetPreviousContent(
+ *targetContent, {WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ editingHost)
+ : HTMLEditUtils::GetNextContent(
+ *targetContent, {WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ editingHost);
+ adjacentContent;
+ adjacentContent =
+ aDirectionAndAmount == nsIEditor::ePrevious
+ ? HTMLEditUtils::GetPreviousContent(
+ *adjacentContent, {WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ editingHost)
+ : HTMLEditUtils::GetNextContent(
+ *adjacentContent, {WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ editingHost)) {
+ // If non-editable element is found, we should not skip it to avoid
+ // joining too far nodes.
+ if (!HTMLEditUtils::IsSimplyEditableNode(*adjacentContent)) {
+ break;
+ }
+ // If block element is found, we should join last leaf content in it.
+ if (HTMLEditUtils::IsBlockElement(
+ *adjacentContent,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ nsIContent* leafContent =
+ aDirectionAndAmount == nsIEditor::ePrevious
+ ? HTMLEditUtils::GetLastLeafContent(
+ *adjacentContent, {LeafNodeType::OnlyEditableLeafNode})
+ : HTMLEditUtils::GetFirstLeafContent(
+ *adjacentContent, {LeafNodeType::OnlyEditableLeafNode});
+ mSkippedInvisibleContents.AppendElement(*targetContent);
+ return leafContent ? leafContent : adjacentContent;
+ }
+ // Only when the found node is an invisible text node or a non-text data
+ // node, we should keep scanning.
+ if (IsIgnorableDataNode(adjacentContent)) {
+ mSkippedInvisibleContents.AppendElement(*targetContent);
+ targetContent = adjacentContent;
+ continue;
+ }
+ // Otherwise, we find a visible things. We should join with last found
+ // invisible text node.
+ break;
+ }
+ return targetContent;
+ };
+
+ if (aDirectionAndAmount == nsIEditor::ePrevious) {
+ mLeftContent = ScanJoinTarget();
+ mRightContent = aCaretPoint.GetContainerAs<nsIContent>();
+ } else {
+ mRightContent = ScanJoinTarget();
+ mLeftContent = aCaretPoint.GetContainerAs<nsIContent>();
+ }
+
+ // Nothing to join
+ if (!mLeftContent || !mRightContent) {
+ return false;
+ }
+
+ // Don't cross table boundaries.
+ return HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) ==
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent);
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ ComputeRangesToDeleteAtCurrentBlockBoundary(
+ const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
+ AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const {
+ MOZ_ASSERT(mLeftContent);
+ MOZ_ASSERT(mRightContent);
+
+ AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
+ *mRightContent);
+ Result<bool, nsresult> canJoinThem =
+ joiner.Prepare(aHTMLEditor, aEditingHost);
+ if (canJoinThem.isErr()) {
+ NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
+ return canJoinThem.unwrapErr();
+ }
+ if (canJoinThem.inspect()) {
+ nsresult rv =
+ joiner.ComputeRangesToDelete(aHTMLEditor, aCaretPoint, aRangesToDelete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoInclusiveAncestorBlockElementsJoiner::"
+ "ComputeRangesToDelete() failed");
+ return rv;
+ }
+
+ // In this case, nothing will be deleted so that the affected range should
+ // be collapsed.
+ nsresult rv = aRangesToDelete.Collapse(aCaretPoint);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoBlockElementsJoiner::HandleDeleteAtCurrentBlockBoundary(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aCaretPoint, const Element& aEditingHost) {
+ MOZ_ASSERT(mLeftContent);
+ MOZ_ASSERT(mRightContent);
+
+ AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
+ *mRightContent);
+ Result<bool, nsresult> canJoinThem =
+ joiner.Prepare(aHTMLEditor, aEditingHost);
+ if (MOZ_UNLIKELY(canJoinThem.isErr())) {
+ NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
+ return Err(canJoinThem.unwrapErr());
+ }
+
+ if (!canJoinThem.inspect()) {
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(aCaretPoint);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return EditActionResult::CanceledResult();
+ }
+
+ EditActionResult result = EditActionResult::IgnoredResult();
+ EditorDOMPoint pointToPutCaret(aCaretPoint);
+ if (joiner.CanJoinBlocks()) {
+ AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &pointToPutCaret);
+ Result<EditActionResult, nsresult> joinResult =
+ joiner.Run(aHTMLEditor, aEditingHost);
+ if (MOZ_UNLIKELY(joinResult.isErr())) {
+ NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Run() failed");
+ return joinResult;
+ }
+ result |= joinResult.unwrap();
+#ifdef DEBUG
+ if (joiner.ShouldDeleteLeafContentInstead()) {
+ NS_ASSERTION(result.Ignored(),
+ "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
+ "returning ignored, but returned not ignored");
+ } else {
+ NS_ASSERTION(!result.Ignored(),
+ "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
+ "returning handled, but returned ignored");
+ }
+#endif // #ifdef DEBUG
+
+ // Cleaning up invisible nodes which are skipped at scanning mLeftContent or
+ // mRightContent.
+ for (const OwningNonNull<nsIContent>& content : mSkippedInvisibleContents) {
+ nsresult rv =
+ aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ mSkippedInvisibleContents.Clear();
+
+ // If we're deleting selection (not replacing with new content) and
+ // AutoInclusiveAncestorBlockElementsJoiner computed new caret position, we
+ // should use it. Otherwise, we should keep the our traditional behavior.
+ if (result.Handled() && joiner.PointRefToPutCaret().IsSet()) {
+ nsresult rv =
+ aHTMLEditor.CollapseSelectionTo(joiner.PointRefToPutCaret());
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored");
+ return result;
+ }
+ // If we prefer to use style in the previous line, we should forget
+ // previous styles since the caret position has all styles which we want
+ // to use with new content.
+ if (nsIEditor::DirectionIsBackspace(aDirectionAndAmount)) {
+ aHTMLEditor.TopLevelEditSubActionDataRef()
+ .mCachedPendingStyles->Clear();
+ }
+ // And we don't want to keep extending a link at ex-end of the previous
+ // paragraph.
+ if (HTMLEditor::GetLinkElement(
+ joiner.PointRefToPutCaret().GetContainer())) {
+ aHTMLEditor.mPendingStylesToApplyToNewContent
+ ->ClearLinkAndItsSpecifiedStyle();
+ }
+ return result;
+ }
+ }
+ // This should claim that trying to join the block means that
+ // this handles the action because the caller shouldn't do anything
+ // anymore in this case.
+ result.MarkAsHandled();
+
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return result;
+}
+
+nsresult
+HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteNonCollapsedRanges(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost) const {
+ MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
+
+ if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->StartRef().IsSet()) ||
+ NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->EndRef().IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aRangesToDelete.Ranges().Length() == 1) {
+ nsFrameSelection* frameSelection =
+ aHTMLEditor.SelectionRef().GetFrameSelection();
+ if (NS_WARN_IF(!frameSelection)) {
+ return NS_ERROR_FAILURE;
+ }
+ Result<EditorRawDOMRange, nsresult> result = ExtendOrShrinkRangeToDelete(
+ aHTMLEditor, frameSelection,
+ EditorRawDOMRange(aRangesToDelete.FirstRangeRef()));
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete() failed");
+ return NS_ERROR_FAILURE;
+ }
+ EditorRawDOMRange newRange(result.unwrap());
+ if (MOZ_UNLIKELY(NS_FAILED(aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
+ newRange.StartRef().ToRawRangeBoundary(),
+ newRange.EndRef().ToRawRangeBoundary())))) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (MOZ_UNLIKELY(
+ NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()))) {
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_WARN_IF(aRangesToDelete.FirstRangeRef()->Collapsed())) {
+ return NS_OK; // Hmm, there is nothing to delete...?
+ }
+ }
+
+ if (!aHTMLEditor.IsPlaintextMailComposer()) {
+ EditorDOMRange firstRange(aRangesToDelete.FirstRangeRef());
+ EditorDOMRange extendedRange =
+ WSRunScanner::GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries(
+ aHTMLEditor.ComputeEditingHost(),
+ EditorDOMRange(aRangesToDelete.FirstRangeRef()));
+ if (firstRange != extendedRange) {
+ nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
+ extendedRange.StartRef().ToRawRangeBoundary(),
+ extendedRange.EndRef().ToRawRangeBoundary());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ if (aRangesToDelete.FirstRangeRef()->GetStartContainer() ==
+ aRangesToDelete.FirstRangeRef()->GetEndContainer()) {
+ if (!aRangesToDelete.FirstRangeRef()->Collapsed()) {
+ nsresult rv = ComputeRangesToDeleteRangesWithTransaction(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction("
+ ") failed");
+ return rv;
+ }
+ // `DeleteUnnecessaryNodesAndCollapseSelection()` may delete parent
+ // elements, but it does not affect computing target ranges. Therefore,
+ // we don't need to touch aRangesToDelete in this case.
+ return NS_OK;
+ }
+
+ Element* startCiteNode = aHTMLEditor.GetMostDistantAncestorMailCiteElement(
+ *aRangesToDelete.FirstRangeRef()->GetStartContainer());
+ Element* endCiteNode = aHTMLEditor.GetMostDistantAncestorMailCiteElement(
+ *aRangesToDelete.FirstRangeRef()->GetEndContainer());
+
+ if (startCiteNode && !endCiteNode) {
+ aDirectionAndAmount = nsIEditor::eNext;
+ } else if (!startCiteNode && endCiteNode) {
+ aDirectionAndAmount = nsIEditor::ePrevious;
+ }
+
+ AutoBlockElementsJoiner joiner(*this);
+ if (!joiner.PrepareToDeleteNonCollapsedRanges(aHTMLEditor, aRangesToDelete)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = joiner.ComputeRangesToDelete(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aSelectionWasCollapsed,
+ aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::ComputeRangesToDelete() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
+ SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
+
+ if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->StartRef().IsSet()) ||
+ NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->EndRef().IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ MOZ_ASSERT_IF(aRangesToDelete.Ranges().Length() == 1,
+ aRangesToDelete.IsFirstRangeEditable(aEditingHost));
+
+ // Else we have a non-collapsed selection. First adjust the selection.
+ // XXX Why do we extend selection only when there is only one range?
+ if (aRangesToDelete.Ranges().Length() == 1) {
+ nsFrameSelection* frameSelection =
+ aHTMLEditor.SelectionRef().GetFrameSelection();
+ if (NS_WARN_IF(!frameSelection)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<EditorRawDOMRange, nsresult> result = ExtendOrShrinkRangeToDelete(
+ aHTMLEditor, frameSelection,
+ EditorRawDOMRange(aRangesToDelete.FirstRangeRef()));
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorRawDOMRange newRange(result.unwrap());
+ if (NS_FAILED(aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
+ newRange.StartRef().ToRawRangeBoundary(),
+ newRange.EndRef().ToRawRangeBoundary()))) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (NS_WARN_IF(aRangesToDelete.FirstRangeRef()->Collapsed())) {
+ // Hmm, there is nothing to delete...?
+ // In this case, the callers want collapsed selection. Therefore, we need
+ // to change the `Selection` here.
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(
+ aRangesToDelete.GetFirstRangeStartPoint<EditorRawDOMPoint>());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+ MOZ_ASSERT(aRangesToDelete.IsFirstRangeEditable(aEditingHost));
+ }
+
+ // Remember that we did a ranged delete for the benefit of AfterEditInner().
+ aHTMLEditor.TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange = true;
+
+ // Figure out if the endpoints are in nodes that can be merged. Adjust
+ // surrounding white-space in preparation to delete selection.
+ if (!aHTMLEditor.IsPlaintextMailComposer()) {
+ {
+ AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
+ &aRangesToDelete.FirstRangeRef());
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRange(
+ aHTMLEditor, EditorDOMRange(aRangesToDelete.FirstRangeRef()),
+ aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::PrepareToDeleteRange() failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret point suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+ if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()) ||
+ (aHTMLEditor.MayHaveMutationEventListeners() &&
+ NS_WARN_IF(!aRangesToDelete.IsFirstRangeEditable(aEditingHost)))) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRange() made the first "
+ "range invalid");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ // XXX This is odd. We do we simply use `DeleteRangesWithTransaction()`
+ // only when **first** range is in same container?
+ if (aRangesToDelete.FirstRangeRef()->GetStartContainer() ==
+ aRangesToDelete.FirstRangeRef()->GetEndContainer()) {
+ // Because of previous DOM tree changes, the range may be collapsed.
+ // If we've already removed all contents in the range, we shouldn't
+ // delete anything around the caret.
+ if (!aRangesToDelete.FirstRangeRef()->Collapsed()) {
+ {
+ AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
+ &aRangesToDelete.FirstRangeRef());
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteRangesWithTransaction(
+ aDirectionAndAmount, aStripWrappers, aRangesToDelete);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("EditorBase::DeleteRangesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ }
+ if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()) ||
+ (aHTMLEditor.MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_NODEREMOVED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
+ NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED) &&
+ NS_WARN_IF(!aRangesToDelete.IsFirstRangeEditable(aEditingHost)))) {
+ NS_WARNING(
+ "EditorBase::DeleteRangesWithTransaction() made the first range "
+ "invalid");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+ // However, even if the range is removed, we may need to clean up the
+ // containers which become empty.
+ nsresult rv = DeleteUnnecessaryNodesAndCollapseSelection(
+ aHTMLEditor, aDirectionAndAmount,
+ EditorDOMPoint(aRangesToDelete.FirstRangeRef()->StartRef()),
+ EditorDOMPoint(aRangesToDelete.FirstRangeRef()->EndRef()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection("
+ ") failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ if (NS_WARN_IF(
+ !aRangesToDelete.FirstRangeRef()->GetStartContainer()->IsContent()) ||
+ NS_WARN_IF(
+ !aRangesToDelete.FirstRangeRef()->GetEndContainer()->IsContent())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Figure out mailcite ancestors
+ RefPtr<Element> startCiteNode =
+ aHTMLEditor.GetMostDistantAncestorMailCiteElement(
+ *aRangesToDelete.FirstRangeRef()->GetStartContainer());
+ RefPtr<Element> endCiteNode =
+ aHTMLEditor.GetMostDistantAncestorMailCiteElement(
+ *aRangesToDelete.FirstRangeRef()->GetEndContainer());
+
+ // If we only have a mailcite at one of the two endpoints, set the
+ // directionality of the deletion so that the selection will end up
+ // outside the mailcite.
+ if (startCiteNode && !endCiteNode) {
+ aDirectionAndAmount = nsIEditor::eNext;
+ } else if (!startCiteNode && endCiteNode) {
+ aDirectionAndAmount = nsIEditor::ePrevious;
+ }
+
+ AutoBlockElementsJoiner joiner(*this);
+ if (!joiner.PrepareToDeleteNonCollapsedRanges(aHTMLEditor, aRangesToDelete)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<EditActionResult, nsresult> result =
+ joiner.Run(aHTMLEditor, aDirectionAndAmount, aStripWrappers,
+ aRangesToDelete, aSelectionWasCollapsed, aEditingHost);
+ NS_WARNING_ASSERTION(result.isOk(), "AutoBlockElementsJoiner::Run() failed");
+ return result;
+}
+
+bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ PrepareToDeleteNonCollapsedRanges(const HTMLEditor& aHTMLEditor,
+ const AutoRangeArray& aRangesToDelete) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
+
+ mLeftContent = HTMLEditUtils::GetInclusiveAncestorElement(
+ *aRangesToDelete.FirstRangeRef()->GetStartContainer()->AsContent(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ mRightContent = HTMLEditUtils::GetInclusiveAncestorElement(
+ *aRangesToDelete.FirstRangeRef()->GetEndContainer()->AsContent(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ // Note that mLeftContent and/or mRightContent can be nullptr if editing host
+ // is an inline element. If both editable ancestor block is exactly same
+ // one or one reaches an inline editing host, we can just delete the content
+ // in ranges.
+ if (mLeftContent == mRightContent || !mLeftContent || !mRightContent) {
+ MOZ_ASSERT_IF(!mLeftContent || !mRightContent,
+ aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->AsContent()
+ ->GetEditingHost() == aRangesToDelete.FirstRangeRef()
+ ->GetEndContainer()
+ ->AsContent()
+ ->GetEditingHost());
+ mMode = Mode::DeleteContentInRanges;
+ return true;
+ }
+
+ // If left block and right block are adjacent siblings and they are same
+ // type of elements, we can merge them after deleting the selected contents.
+ // MOOSE: this could conceivably screw up a table.. fix me.
+ if (mLeftContent->GetParentNode() == mRightContent->GetParentNode() &&
+ HTMLEditUtils::CanContentsBeJoined(*mLeftContent, *mRightContent) &&
+ // XXX What's special about these three types of block?
+ (mLeftContent->IsHTMLElement(nsGkAtoms::p) ||
+ HTMLEditUtils::IsListItem(mLeftContent) ||
+ HTMLEditUtils::IsHeader(*mLeftContent))) {
+ mMode = Mode::JoinBlocksInSameParent;
+ return true;
+ }
+
+ mMode = Mode::DeleteNonCollapsedRanges;
+ return true;
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ ComputeRangesToDeleteContentInRanges(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete) const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
+ MOZ_ASSERT(mMode == Mode::DeleteContentInRanges);
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->AsContent()
+ ->GetEditingHost());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->AsContent()
+ ->GetEditingHost() == aRangesToDelete.FirstRangeRef()
+ ->GetEndContainer()
+ ->AsContent()
+ ->GetEditingHost());
+ MOZ_ASSERT(!mLeftContent == !mRightContent);
+ MOZ_ASSERT_IF(mLeftContent, mLeftContent->IsElement());
+ MOZ_ASSERT_IF(mLeftContent, aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->IsInclusiveDescendantOf(mLeftContent));
+ MOZ_ASSERT_IF(mRightContent, mRightContent->IsElement());
+ MOZ_ASSERT_IF(mRightContent, aRangesToDelete.FirstRangeRef()
+ ->GetEndContainer()
+ ->IsInclusiveDescendantOf(mRightContent));
+ MOZ_ASSERT_IF(!mLeftContent,
+ HTMLEditUtils::IsInlineContent(
+ *aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->AsContent()
+ ->GetEditingHost(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle));
+
+ nsresult rv =
+ mDeleteRangesHandlerConst.ComputeRangesToDeleteRangesWithTransaction(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::"
+ "ComputeRangesToDeleteRangesWithTransaction() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoBlockElementsJoiner::DeleteContentInRanges(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ AutoRangeArray& aRangesToDelete) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
+ MOZ_ASSERT(mMode == Mode::DeleteContentInRanges);
+ MOZ_ASSERT(mDeleteRangesHandler);
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->AsContent()
+ ->GetEditingHost());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->AsContent()
+ ->GetEditingHost() == aRangesToDelete.FirstRangeRef()
+ ->GetEndContainer()
+ ->AsContent()
+ ->GetEditingHost());
+ MOZ_ASSERT_IF(mLeftContent, mLeftContent->IsElement());
+ MOZ_ASSERT_IF(mLeftContent, aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->IsInclusiveDescendantOf(mLeftContent));
+ MOZ_ASSERT_IF(mRightContent, mRightContent->IsElement());
+ MOZ_ASSERT_IF(mRightContent, aRangesToDelete.FirstRangeRef()
+ ->GetEndContainer()
+ ->IsInclusiveDescendantOf(mRightContent));
+ MOZ_ASSERT_IF(!mLeftContent,
+ HTMLEditUtils::IsInlineContent(
+ *aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->AsContent()
+ ->GetEditingHost(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle));
+
+ // XXX This is also odd. We do we simply use
+ // `DeleteRangesWithTransaction()` only when **first** range is in
+ // same block?
+ {
+ AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
+ &aRangesToDelete.FirstRangeRef());
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteRangesWithTransaction(
+ aDirectionAndAmount, aStripWrappers, aRangesToDelete);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ if (NS_WARN_IF(caretPointOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "EditorBase::DeleteRangesWithTransaction() failed, but ignored");
+ } else {
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ }
+ }
+
+ if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ nsresult rv =
+ mDeleteRangesHandler->DeleteUnnecessaryNodesAndCollapseSelection(
+ aHTMLEditor, aDirectionAndAmount,
+ EditorDOMPoint(aRangesToDelete.FirstRangeRef()->StartRef()),
+ EditorDOMPoint(aRangesToDelete.FirstRangeRef()->EndRef()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection() "
+ "failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ ComputeRangesToJoinBlockElementsInSameParent(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete) const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
+ MOZ_ASSERT(mMode == Mode::JoinBlocksInSameParent);
+ MOZ_ASSERT(mLeftContent);
+ MOZ_ASSERT(mLeftContent->IsElement());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->IsInclusiveDescendantOf(mLeftContent));
+ MOZ_ASSERT(mRightContent);
+ MOZ_ASSERT(mRightContent->IsElement());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetEndContainer()
+ ->IsInclusiveDescendantOf(mRightContent));
+ MOZ_ASSERT(mLeftContent->GetParentNode() == mRightContent->GetParentNode());
+
+ nsresult rv =
+ mDeleteRangesHandlerConst.ComputeRangesToDeleteRangesWithTransaction(
+ aHTMLEditor, aDirectionAndAmount, aRangesToDelete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::"
+ "ComputeRangesToDeleteRangesWithTransaction() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoBlockElementsJoiner::JoinBlockElementsInSameParent(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ AutoRangeArray& aRangesToDelete,
+ SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
+ MOZ_ASSERT(mMode == Mode::JoinBlocksInSameParent);
+ MOZ_ASSERT(mLeftContent);
+ MOZ_ASSERT(mLeftContent->IsElement());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->IsInclusiveDescendantOf(mLeftContent));
+ MOZ_ASSERT(mRightContent);
+ MOZ_ASSERT(mRightContent->IsElement());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetEndContainer()
+ ->IsInclusiveDescendantOf(mRightContent));
+ MOZ_ASSERT(mLeftContent->GetParentNode() == mRightContent->GetParentNode());
+
+ const bool backspaceInRightBlock =
+ aSelectionWasCollapsed == SelectionWasCollapsed::Yes &&
+ nsIEditor::DirectionIsBackspace(aDirectionAndAmount);
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteRangesWithTransaction(aDirectionAndAmount,
+ aStripWrappers, aRangesToDelete);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("EditorBase::DeleteRangesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+
+ if (NS_WARN_IF(!mLeftContent->GetParentNode()) ||
+ NS_WARN_IF(!mRightContent->GetParentNode()) ||
+ NS_WARN_IF(mLeftContent->GetParentNode() !=
+ mRightContent->GetParentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ auto startOfRightContent =
+ HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
+ *mRightContent);
+ AutoTrackDOMPoint trackStartOfRightContent(aHTMLEditor.RangeUpdaterRef(),
+ &startOfRightContent);
+ Result<EditorDOMPoint, nsresult> atFirstChildOfTheLastRightNodeOrError =
+ JoinNodesDeepWithTransaction(aHTMLEditor, MOZ_KnownLive(*mLeftContent),
+ MOZ_KnownLive(*mRightContent));
+ if (MOZ_UNLIKELY(atFirstChildOfTheLastRightNodeOrError.isErr())) {
+ NS_WARNING("HTMLEditor::JoinNodesDeepWithTransaction() failed");
+ return atFirstChildOfTheLastRightNodeOrError.propagateErr();
+ }
+ MOZ_ASSERT(atFirstChildOfTheLastRightNodeOrError.inspect().IsSet());
+ trackStartOfRightContent.FlushAndStopTracking();
+ if (NS_WARN_IF(!startOfRightContent.IsSet()) ||
+ NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // If we're deleting selection (not replacing with new content) and the joined
+ // point follows a text node, we should put caret to end of the preceding text
+ // node because the other browsers insert following inputs into there.
+ if (MayEditActionDeleteAroundCollapsedSelection(
+ aHTMLEditor.GetEditAction())) {
+ WSRunScanner scanner(&aEditingHost, startOfRightContent,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ WSScanResult maybePreviousText =
+ scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent);
+ if (maybePreviousText.IsContentEditable() &&
+ maybePreviousText.InVisibleOrCollapsibleCharacters()) {
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(
+ maybePreviousText.Point<EditorRawDOMPoint>());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ // If we prefer to use style in the previous line, we should forget
+ // previous styles since the caret position has all styles which we want
+ // to use with new content.
+ if (backspaceInRightBlock) {
+ aHTMLEditor.TopLevelEditSubActionDataRef()
+ .mCachedPendingStyles->Clear();
+ }
+ // And we don't want to keep extending a link at ex-end of the previous
+ // paragraph.
+ if (HTMLEditor::GetLinkElement(maybePreviousText.TextPtr())) {
+ aHTMLEditor.mPendingStylesToApplyToNewContent
+ ->ClearLinkAndItsSpecifiedStyle();
+ }
+ return EditActionResult::HandledResult();
+ }
+ }
+
+ // Otherwise, we should put caret at start of the right content.
+ rv = aHTMLEditor.CollapseSelectionTo(
+ atFirstChildOfTheLastRightNodeOrError.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+Result<bool, nsresult>
+HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ ComputeRangesToDeleteNodesEntirelyInRangeButKeepTableStructure(
+ const HTMLEditor& aHTMLEditor, nsRange& aRange,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
+ const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+
+ AutoTArray<OwningNonNull<nsIContent>, 10> arrayOfTopChildren;
+ DOMSubtreeIterator iter;
+ nsresult rv = iter.Init(aRange);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("DOMSubtreeIterator::Init() failed");
+ return Err(rv);
+ }
+ iter.AppendAllNodesToArray(arrayOfTopChildren);
+ return NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure(
+ aHTMLEditor, arrayOfTopChildren, aSelectionWasCollapsed);
+}
+
+Result<bool, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoBlockElementsJoiner::DeleteNodesEntirelyInRangeButKeepTableStructure(
+ HTMLEditor& aHTMLEditor, nsRange& aRange,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+
+ // Build a list of direct child nodes in the range
+ AutoTArray<OwningNonNull<nsIContent>, 10> arrayOfTopChildren;
+ DOMSubtreeIterator iter;
+ nsresult rv = iter.Init(aRange);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("DOMSubtreeIterator::Init() failed");
+ return Err(rv);
+ }
+ iter.AppendAllNodesToArray(arrayOfTopChildren);
+
+ // Now that we have the list, delete non-table elements
+ bool needsToJoinLater =
+ NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure(
+ aHTMLEditor, arrayOfTopChildren, aSelectionWasCollapsed);
+ for (auto& content : arrayOfTopChildren) {
+ // XXX After here, the child contents in the array may have been moved
+ // to somewhere or removed. We should handle it.
+ //
+ // MOZ_KnownLive because 'arrayOfTopChildren' is guaranteed to
+ // keep it alive.
+ //
+ // Even with https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 fixed
+ // this might need to stay, because 'arrayOfTopChildren' is not const,
+ // so it's not obvious how to prove via static analysis that it won't
+ // change and release us.
+ nsresult rv =
+ DeleteContentButKeepTableStructure(aHTMLEditor, MOZ_KnownLive(content));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoBlockElementsJoiner::DeleteContentButKeepTableStructure() failed, "
+ "but ignored");
+ }
+ return needsToJoinLater;
+}
+
+bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure(
+ const HTMLEditor& aHTMLEditor,
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
+ const {
+ // If original selection was collapsed, we need always to join the nodes.
+ // XXX Why?
+ if (aSelectionWasCollapsed ==
+ AutoDeleteRangesHandler::SelectionWasCollapsed::No) {
+ return true;
+ }
+ // If something visible is deleted, no need to join. Visible means
+ // all nodes except non-visible textnodes and breaks.
+ if (aArrayOfContents.IsEmpty()) {
+ return true;
+ }
+ for (const OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ if (content->IsText()) {
+ if (HTMLEditUtils::IsInVisibleTextFrames(aHTMLEditor.GetPresContext(),
+ *content->AsText())) {
+ return false;
+ }
+ continue;
+ }
+ // XXX If it's an element node, we should check whether it has visible
+ // frames or not.
+ if (!content->IsElement() ||
+ HTMLEditUtils::IsEmptyNode(
+ *content->AsElement(),
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ continue;
+ }
+ if (!HTMLEditUtils::IsInvisibleBRElement(*content)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange) {
+ EditorDOMPoint rangeStart(aRange.StartRef());
+ EditorDOMPoint rangeEnd(aRange.EndRef());
+ if (rangeStart.IsInTextNode() && !rangeStart.IsEndOfContainer()) {
+ // Delete to last character
+ OwningNonNull<Text> textNode = *rangeStart.ContainerAs<Text>();
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextWithTransaction(
+ textNode, rangeStart.Offset(),
+ rangeStart.GetContainer()->Length() - rangeStart.Offset());
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ }
+ if (rangeEnd.IsInTextNode() && !rangeEnd.IsStartOfContainer()) {
+ // Delete to first character
+ OwningNonNull<Text> textNode = *rangeEnd.ContainerAs<Text>();
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextWithTransaction(textNode, 0, rangeEnd.Offset());
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ ComputeRangesToDeleteNonCollapsedRanges(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost) const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
+ MOZ_ASSERT(mLeftContent);
+ MOZ_ASSERT(mLeftContent->IsElement());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->IsInclusiveDescendantOf(mLeftContent));
+ MOZ_ASSERT(mRightContent);
+ MOZ_ASSERT(mRightContent->IsElement());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetEndContainer()
+ ->IsInclusiveDescendantOf(mRightContent));
+
+ for (OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
+ Result<bool, nsresult> result =
+ ComputeRangesToDeleteNodesEntirelyInRangeButKeepTableStructure(
+ aHTMLEditor, range, aSelectionWasCollapsed);
+ if (result.isErr()) {
+ NS_WARNING(
+ "AutoBlockElementsJoiner::"
+ "ComputeRangesToDeleteNodesEntirelyInRangeButKeepTableStructure() "
+ "failed");
+ return result.unwrapErr();
+ }
+ if (!result.unwrap()) {
+ return NS_OK;
+ }
+ }
+
+ AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
+ *mRightContent);
+ Result<bool, nsresult> canJoinThem =
+ joiner.Prepare(aHTMLEditor, aEditingHost);
+ if (canJoinThem.isErr()) {
+ NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
+ return canJoinThem.unwrapErr();
+ }
+
+ if (!canJoinThem.unwrap()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ if (!joiner.CanJoinBlocks()) {
+ return NS_OK;
+ }
+
+ nsresult rv = joiner.ComputeRangesToDelete(aHTMLEditor, EditorDOMPoint(),
+ aRangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoInclusiveAncestorBlockElementsJoiner::ComputeRangesToDelete() "
+ "failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoBlockElementsJoiner::HandleDeleteNonCollapsedRanges(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers,
+ AutoRangeArray& aRangesToDelete,
+ AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
+ MOZ_ASSERT(mDeleteRangesHandler);
+ MOZ_ASSERT(mLeftContent);
+ MOZ_ASSERT(mLeftContent->IsElement());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->IsInclusiveDescendantOf(mLeftContent));
+ MOZ_ASSERT(mRightContent);
+ MOZ_ASSERT(mRightContent->IsElement());
+ MOZ_ASSERT(aRangesToDelete.FirstRangeRef()
+ ->GetEndContainer()
+ ->IsInclusiveDescendantOf(mRightContent));
+
+ const bool backspaceInRightBlock =
+ aSelectionWasCollapsed == SelectionWasCollapsed::Yes &&
+ nsIEditor::DirectionIsBackspace(aDirectionAndAmount);
+
+ // Otherwise, delete every nodes in all ranges, then, clean up something.
+ EditActionResult result = EditActionResult::IgnoredResult();
+ EditorDOMPoint pointToPutCaret;
+ while (true) {
+ AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
+ &aRangesToDelete.FirstRangeRef());
+
+ bool joinInclusiveAncestorBlockElements = true;
+ for (auto& range : aRangesToDelete.Ranges()) {
+ Result<bool, nsresult> deleteResult =
+ DeleteNodesEntirelyInRangeButKeepTableStructure(
+ aHTMLEditor, MOZ_KnownLive(range), aSelectionWasCollapsed);
+ if (MOZ_UNLIKELY(deleteResult.isErr())) {
+ NS_WARNING(
+ "AutoBlockElementsJoiner::"
+ "DeleteNodesEntirelyInRangeButKeepTableStructure() failed");
+ return deleteResult.propagateErr();
+ }
+ // XXX Completely odd. Why don't we join blocks around each range?
+ joinInclusiveAncestorBlockElements &= deleteResult.unwrap();
+ }
+
+ // Check endpoints for possible text deletion. We can assume that if
+ // text node is found, we can delete to end or to begining as
+ // appropriate, since the case where both sel endpoints in same text
+ // node was already handled (we wouldn't be here)
+ nsresult rv = DeleteTextAtStartAndEndOfRange(
+ aHTMLEditor, MOZ_KnownLive(aRangesToDelete.FirstRangeRef()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoBlockElementsJoiner::DeleteTextAtStartAndEndOfRange() failed");
+ return Err(rv);
+ }
+
+ if (!joinInclusiveAncestorBlockElements) {
+ break;
+ }
+
+ AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
+ *mRightContent);
+ Result<bool, nsresult> canJoinThem =
+ joiner.Prepare(aHTMLEditor, aEditingHost);
+ if (canJoinThem.isErr()) {
+ NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
+ return canJoinThem.propagateErr();
+ }
+
+ // If we're joining blocks: if deleting forward the selection should
+ // be collapsed to the end of the selection, if deleting backward the
+ // selection should be collapsed to the beginning of the selection.
+ // But if we're not joining then the selection should collapse to the
+ // beginning of the selection if we'redeleting forward, because the
+ // end of the selection will still be in the next block. And same
+ // thing for deleting backwards (selection should collapse to the end,
+ // because the beginning will still be in the first block). See Bug
+ // 507936.
+ if (aDirectionAndAmount == nsIEditor::eNext) {
+ aDirectionAndAmount = nsIEditor::ePrevious;
+ } else {
+ aDirectionAndAmount = nsIEditor::eNext;
+ }
+
+ if (!canJoinThem.inspect()) {
+ result.MarkAsCanceled();
+ break;
+ }
+
+ if (!joiner.CanJoinBlocks()) {
+ break;
+ }
+
+ Result<EditActionResult, nsresult> joinResult =
+ joiner.Run(aHTMLEditor, aEditingHost);
+ if (MOZ_UNLIKELY(joinResult.isErr())) {
+ NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Run() failed");
+ return joinResult;
+ }
+ result |= joinResult.unwrap();
+#ifdef DEBUG
+ if (joiner.ShouldDeleteLeafContentInstead()) {
+ NS_ASSERTION(result.Ignored(),
+ "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
+ "returning ignored, but returned not ignored");
+ } else {
+ NS_ASSERTION(!result.Ignored(),
+ "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
+ "returning handled, but returned ignored");
+ }
+#endif // #ifdef DEBUG
+ pointToPutCaret = joiner.PointRefToPutCaret();
+ break;
+ }
+
+ // If we're deleting selection (not replacing with new content) and
+ // AutoInclusiveAncestorBlockElementsJoiner computed new caret position, we
+ // should use it. Otherwise, we should keep the traditional behavior.
+ if (result.Handled() && pointToPutCaret.IsSet()) {
+ EditorDOMRange range(aRangesToDelete.FirstRangeRef());
+ nsresult rv =
+ mDeleteRangesHandler->DeleteUnnecessaryNodes(aHTMLEditor, range);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoDeleteRangesHandler::DeleteUnnecessaryNodes() failed");
+ return Err(rv);
+ }
+ rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ // If we prefer to use style in the previous line, we should forget
+ // previous styles since the caret position has all styles which we want
+ // to use with new content.
+ if (backspaceInRightBlock) {
+ aHTMLEditor.TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
+ }
+ // And we don't want to keep extending a link at ex-end of the previous
+ // paragraph.
+ if (HTMLEditor::GetLinkElement(pointToPutCaret.GetContainer())) {
+ aHTMLEditor.mPendingStylesToApplyToNewContent
+ ->ClearLinkAndItsSpecifiedStyle();
+ }
+ return result;
+ }
+
+ nsresult rv =
+ mDeleteRangesHandler->DeleteUnnecessaryNodesAndCollapseSelection(
+ aHTMLEditor, aDirectionAndAmount,
+ EditorDOMPoint(aRangesToDelete.FirstRangeRef()->StartRef()),
+ EditorDOMPoint(aRangesToDelete.FirstRangeRef()->EndRef()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection() "
+ "failed");
+ return Err(rv);
+ }
+
+ result.MarkAsHandled();
+ return result;
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::DeleteUnnecessaryNodes(
+ HTMLEditor& aHTMLEditor, EditorDOMRange& aRange) {
+ MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(EditorUtils::IsEditableContent(
+ *aRange.StartRef().ContainerAs<nsIContent>(), EditorType::HTML));
+ MOZ_ASSERT(EditorUtils::IsEditableContent(
+ *aRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML));
+
+ // If we're handling DnD, this is called to delete dragging item from the
+ // tree. In this case, we should remove parent blocks if it becomes empty.
+ if (aHTMLEditor.GetEditAction() == EditAction::eDrop ||
+ aHTMLEditor.GetEditAction() == EditAction::eDeleteByDrag) {
+ MOZ_ASSERT(aRange.Collapsed() ||
+ (aRange.StartRef().GetContainer()->GetNextSibling() ==
+ aRange.EndRef().GetContainer() &&
+ aRange.StartRef().IsEndOfContainer() &&
+ aRange.EndRef().IsStartOfContainer()));
+ AutoTrackDOMRange trackRange(aHTMLEditor.RangeUpdaterRef(), &aRange);
+
+ nsresult rv = DeleteParentBlocksWithTransactionIfEmpty(aHTMLEditor,
+ aRange.StartRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::DeleteParentBlocksWithTransactionIfEmpty() failed");
+ return rv;
+ }
+ aHTMLEditor.TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks =
+ rv == NS_OK;
+ // If we removed parent blocks, Selection should be collapsed at where
+ // the most ancestor empty block has been.
+ if (aHTMLEditor.TopLevelEditSubActionDataRef()
+ .mDidDeleteEmptyParentBlocks) {
+ return NS_OK;
+ }
+ }
+
+ if (NS_WARN_IF(!aRange.IsInContentNodes()) ||
+ NS_WARN_IF(!EditorUtils::IsEditableContent(
+ *aRange.StartRef().ContainerAs<nsIContent>(), EditorType::HTML)) ||
+ NS_WARN_IF(!EditorUtils::IsEditableContent(
+ *aRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML))) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+
+ // We might have left only collapsed white-space in the start/end nodes
+ AutoTrackDOMRange trackRange(aHTMLEditor.RangeUpdaterRef(), &aRange);
+
+ OwningNonNull<nsIContent> startContainer =
+ *aRange.StartRef().ContainerAs<nsIContent>();
+ OwningNonNull<nsIContent> endContainer =
+ *aRange.EndRef().ContainerAs<nsIContent>();
+ nsresult rv =
+ DeleteNodeIfInvisibleAndEditableTextNode(aHTMLEditor, startContainer);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode() "
+ "failed to remove start node, but ignored");
+ // If we've not handled the selection end container, and it's still
+ // editable, let's handle it.
+ if (aRange.InSameContainer() ||
+ !EditorUtils::IsEditableContent(
+ *aRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)) {
+ return NS_OK;
+ }
+ rv = DeleteNodeIfInvisibleAndEditableTextNode(aHTMLEditor, endContainer);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode() "
+ "failed to remove end node, but ignored");
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ const EditorDOMPoint& aSelectionStartPoint,
+ const EditorDOMPoint& aSelectionEndPoint) {
+ EditorDOMRange range(aSelectionStartPoint, aSelectionEndPoint);
+ nsresult rv = DeleteUnnecessaryNodes(aHTMLEditor, range);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoDeleteRangesHandler::DeleteUnnecessaryNodes() failed");
+ return rv;
+ }
+
+ if (aHTMLEditor.GetEditAction() == EditAction::eDrop ||
+ aHTMLEditor.GetEditAction() == EditAction::eDeleteByDrag) {
+ // If we removed parent blocks, Selection should be collapsed at where
+ // the most ancestor empty block has been.
+ // XXX I think that if the range is not in active editing host, we should
+ // not try to collapse selection here.
+ if (aHTMLEditor.TopLevelEditSubActionDataRef()
+ .mDidDeleteEmptyParentBlocks) {
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(range.StartRef());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ }
+
+ rv = aHTMLEditor.CollapseSelectionTo(
+ aDirectionAndAmount == nsIEditor::ePrevious ? range.EndRef()
+ : range.StartRef());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+nsresult
+HTMLEditor::AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode(
+ HTMLEditor& aHTMLEditor, nsIContent& aContent) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+
+ Text* text = aContent.GetAsText();
+ if (!text) {
+ return NS_OK;
+ }
+
+ if (!HTMLEditUtils::IsRemovableFromParentNode(*text) ||
+ HTMLEditUtils::IsVisibleTextNode(*text)) {
+ return NS_OK;
+ }
+
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+}
+
+nsresult
+HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) {
+ MOZ_ASSERT(aPoint.IsSet());
+ MOZ_ASSERT(aHTMLEditor.mPlaceholderBatch);
+
+ // First, check there is visible contents before the point in current block.
+ RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost();
+ WSRunScanner wsScannerForPoint(
+ editingHost, aPoint, BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (!wsScannerForPoint.StartsFromCurrentBlockBoundary()) {
+ // If there is visible node before the point, we shouldn't remove the
+ // parent block.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+ if (NS_WARN_IF(!wsScannerForPoint.GetStartReasonContent()) ||
+ NS_WARN_IF(!wsScannerForPoint.GetStartReasonContent()->GetParentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+ if (editingHost == wsScannerForPoint.GetStartReasonContent()) {
+ // If we reach editing host, there is no parent blocks which can be removed.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+ if (HTMLEditUtils::IsTableCellOrCaption(
+ *wsScannerForPoint.GetStartReasonContent())) {
+ // If we reach a <td>, <th> or <caption>, we shouldn't remove it even
+ // becomes empty because removing such element changes the structure of
+ // the <table>.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ // Next, check there is visible contents after the point in current block.
+ WSScanResult forwardScanFromPointResult =
+ wsScannerForPoint.ScanNextVisibleNodeOrBlockBoundaryFrom(aPoint);
+ if (forwardScanFromPointResult.Failed()) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (forwardScanFromPointResult.ReachedBRElement()) {
+ // XXX In my understanding, this is odd. The end reason may not be
+ // same as the reached <br> element because the equality is
+ // guaranteed only when ReachedCurrentBlockBoundary() returns true.
+ // However, looks like that this code assumes that
+ // GetEndReasonContent() returns the (or a) <br> element.
+ NS_ASSERTION(wsScannerForPoint.GetEndReasonContent() ==
+ forwardScanFromPointResult.BRElementPtr(),
+ "End reason is not the reached <br> element");
+ // If the <br> element is visible, we shouldn't remove the parent block.
+ if (HTMLEditUtils::IsVisibleBRElement(
+ *wsScannerForPoint.GetEndReasonContent())) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+ if (wsScannerForPoint.GetEndReasonContent()->GetNextSibling()) {
+ WSScanResult scanResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ editingHost,
+ EditorRawDOMPoint::After(
+ *wsScannerForPoint.GetEndReasonContent()),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (scanResult.Failed()) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (!scanResult.ReachedCurrentBlockBoundary()) {
+ // If we couldn't reach the block's end after the invisible <br>,
+ // that means that there is visible content.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+ }
+ } else if (!forwardScanFromPointResult.ReachedCurrentBlockBoundary()) {
+ // If we couldn't reach the block's end, the block has visible content.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ // Delete the parent block.
+ EditorDOMPoint nextPoint(
+ wsScannerForPoint.GetStartReasonContent()->GetParentNode(), 0);
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ MOZ_KnownLive(*wsScannerForPoint.GetStartReasonContent()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ // If we reach editing host, return NS_OK.
+ if (nextPoint.GetContainer() == editingHost) {
+ return NS_OK;
+ }
+
+ // Otherwise, we need to check whether we're still in empty block or not.
+
+ // If we have mutation event listeners, the next point is now outside of
+ // editing host or editing hos has been changed.
+ if (aHTMLEditor.MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_NODEREMOVED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
+ NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED)) {
+ Element* newEditingHost = aHTMLEditor.ComputeEditingHost();
+ if (NS_WARN_IF(!newEditingHost) ||
+ NS_WARN_IF(newEditingHost != editingHost)) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ if (NS_WARN_IF(!EditorUtils::IsDescendantOf(*nextPoint.GetContainer(),
+ *newEditingHost))) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ }
+
+ rv = DeleteParentBlocksWithTransactionIfEmpty(aHTMLEditor, nextPoint);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoDeleteRangesHandler::"
+ "DeleteParentBlocksWithTransactionIfEmpty() failed");
+ return rv;
+}
+
+nsresult
+HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction(
+ const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
+ AutoRangeArray& aRangesToDelete) const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
+
+ EditorBase::HowToHandleCollapsedRange howToHandleCollapsedRange =
+ EditorBase::HowToHandleCollapsedRangeFor(aDirectionAndAmount);
+ if (NS_WARN_IF(aRangesToDelete.IsCollapsed() &&
+ howToHandleCollapsedRange ==
+ EditorBase::HowToHandleCollapsedRange::Ignore)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto extendRangeToSelectCharacterForward =
+ [](nsRange& aRange, const EditorRawDOMPointInText& aCaretPoint) -> void {
+ const nsTextFragment& textFragment =
+ aCaretPoint.ContainerAs<Text>()->TextFragment();
+ if (!textFragment.GetLength()) {
+ return;
+ }
+ if (textFragment.IsHighSurrogateFollowedByLowSurrogateAt(
+ aCaretPoint.Offset())) {
+ DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd(
+ aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset(),
+ aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() + 2);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsRange::SetStartAndEnd() failed");
+ return;
+ }
+ DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd(
+ aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset(),
+ aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() + 1);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsRange::SetStartAndEnd() failed");
+ };
+ auto extendRangeToSelectCharacterBackward =
+ [](nsRange& aRange, const EditorRawDOMPointInText& aCaretPoint) -> void {
+ if (aCaretPoint.IsStartOfContainer()) {
+ return;
+ }
+ const nsTextFragment& textFragment =
+ aCaretPoint.ContainerAs<Text>()->TextFragment();
+ if (!textFragment.GetLength()) {
+ return;
+ }
+ if (textFragment.IsLowSurrogateFollowingHighSurrogateAt(
+ aCaretPoint.Offset() - 1)) {
+ DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd(
+ aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() - 2,
+ aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsRange::SetStartAndEnd() failed");
+ return;
+ }
+ DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd(
+ aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() - 1,
+ aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsRange::SetStartAndEnd() failed");
+ };
+
+ RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost();
+ for (OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
+ // If it's not collapsed, `DeleteRangeTransaction::Create()` will be called
+ // with it and `DeleteRangeTransaction` won't modify the range.
+ if (!range->Collapsed()) {
+ continue;
+ }
+
+ if (howToHandleCollapsedRange ==
+ EditorBase::HowToHandleCollapsedRange::Ignore) {
+ continue;
+ }
+
+ // In the other cases, `EditorBase::CreateTransactionForCollapsedRange()`
+ // will handle the collapsed range.
+ EditorRawDOMPoint caretPoint(range->StartRef());
+ if (howToHandleCollapsedRange ==
+ EditorBase::HowToHandleCollapsedRange::ExtendBackward &&
+ caretPoint.IsStartOfContainer()) {
+ nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent(
+ *caretPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, editingHost);
+ if (!previousEditableContent) {
+ continue;
+ }
+ if (!previousEditableContent->IsText()) {
+ IgnoredErrorResult ignoredError;
+ range->SelectNode(*previousEditableContent, ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "nsRange::SelectNode() failed");
+ continue;
+ }
+
+ extendRangeToSelectCharacterBackward(
+ range,
+ EditorRawDOMPointInText::AtEndOf(*previousEditableContent->AsText()));
+ continue;
+ }
+
+ if (howToHandleCollapsedRange ==
+ EditorBase::HowToHandleCollapsedRange::ExtendForward &&
+ caretPoint.IsEndOfContainer()) {
+ nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent(
+ *caretPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, editingHost);
+ if (!nextEditableContent) {
+ continue;
+ }
+
+ if (!nextEditableContent->IsText()) {
+ IgnoredErrorResult ignoredError;
+ range->SelectNode(*nextEditableContent, ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "nsRange::SelectNode() failed");
+ continue;
+ }
+
+ extendRangeToSelectCharacterForward(
+ range, EditorRawDOMPointInText(nextEditableContent->AsText(), 0));
+ continue;
+ }
+
+ if (caretPoint.IsInTextNode()) {
+ if (howToHandleCollapsedRange ==
+ EditorBase::HowToHandleCollapsedRange::ExtendBackward) {
+ extendRangeToSelectCharacterBackward(
+ range, EditorRawDOMPointInText(caretPoint.ContainerAs<Text>(),
+ caretPoint.Offset()));
+ continue;
+ }
+ extendRangeToSelectCharacterForward(
+ range, EditorRawDOMPointInText(caretPoint.ContainerAs<Text>(),
+ caretPoint.Offset()));
+ continue;
+ }
+
+ nsIContent* editableContent =
+ howToHandleCollapsedRange ==
+ EditorBase::HowToHandleCollapsedRange::ExtendBackward
+ ? HTMLEditUtils::GetPreviousContent(
+ caretPoint, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, editingHost)
+ : HTMLEditUtils::GetNextContent(
+ caretPoint, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, editingHost);
+ if (!editableContent) {
+ continue;
+ }
+ while (editableContent && editableContent->IsCharacterData() &&
+ !editableContent->Length()) {
+ editableContent =
+ howToHandleCollapsedRange ==
+ EditorBase::HowToHandleCollapsedRange::ExtendBackward
+ ? HTMLEditUtils::GetPreviousContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, editingHost)
+ : HTMLEditUtils::GetNextContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, editingHost);
+ }
+ if (!editableContent) {
+ continue;
+ }
+
+ if (!editableContent->IsText()) {
+ IgnoredErrorResult ignoredError;
+ range->SelectNode(*editableContent, ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "nsRange::SelectNode() failed");
+ continue;
+ }
+
+ if (howToHandleCollapsedRange ==
+ EditorBase::HowToHandleCollapsedRange::ExtendBackward) {
+ extendRangeToSelectCharacterBackward(
+ range, EditorRawDOMPointInText::AtEndOf(*editableContent->AsText()));
+ continue;
+ }
+ extendRangeToSelectCharacterForward(
+ range, EditorRawDOMPointInText(editableContent->AsText(), 0));
+ }
+
+ return NS_OK;
+}
+
+template <typename EditorDOMPointType>
+Result<CaretPoint, nsresult> HTMLEditor::DeleteTextAndTextNodesWithTransaction(
+ const EditorDOMPointType& aStartPoint, const EditorDOMPointType& aEndPoint,
+ TreatEmptyTextNodes aTreatEmptyTextNodes) {
+ if (NS_WARN_IF(!aStartPoint.IsSet()) || NS_WARN_IF(!aEndPoint.IsSet())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ // MOOSE: this routine needs to be modified to preserve the integrity of the
+ // wsFragment info.
+
+ if (aStartPoint == aEndPoint) {
+ // Nothing to delete
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ RefPtr<Element> editingHost = ComputeEditingHost();
+ auto DeleteEmptyContentNodeWithTransaction =
+ [this, &aTreatEmptyTextNodes, &editingHost](nsIContent& aContent)
+ MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> nsresult {
+ OwningNonNull<nsIContent> nodeToRemove = aContent;
+ if (aTreatEmptyTextNodes ==
+ TreatEmptyTextNodes::RemoveAllEmptyInlineAncestors) {
+ Element* emptyParentElementToRemove =
+ HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
+ nodeToRemove, BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ editingHost);
+ if (emptyParentElementToRemove) {
+ nodeToRemove = *emptyParentElementToRemove;
+ }
+ }
+ nsresult rv = DeleteNodeWithTransaction(nodeToRemove);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ };
+
+ if (aStartPoint.GetContainer() == aEndPoint.GetContainer() &&
+ aStartPoint.IsInTextNode()) {
+ if (aTreatEmptyTextNodes !=
+ TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries &&
+ aStartPoint.IsStartOfContainer() && aEndPoint.IsEndOfContainer()) {
+ nsresult rv = DeleteEmptyContentNodeWithTransaction(
+ MOZ_KnownLive(*aStartPoint.template ContainerAs<Text>()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("deleteEmptyContentNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ return CaretPoint(EditorDOMPoint());
+ }
+ RefPtr<Text> textNode = aStartPoint.template ContainerAs<Text>();
+ Result<CaretPoint, nsresult> caretPointOrError =
+ DeleteTextWithTransaction(*textNode, aStartPoint.Offset(),
+ aEndPoint.Offset() - aStartPoint.Offset());
+ NS_WARNING_ASSERTION(caretPointOrError.isOk(),
+ "HTMLEditor::DeleteTextWithTransaction() failed");
+ return caretPointOrError;
+ }
+
+ RefPtr<nsRange> range =
+ nsRange::Create(aStartPoint.ToRawRangeBoundary(),
+ aEndPoint.ToRawRangeBoundary(), IgnoreErrors());
+ if (!range) {
+ NS_WARNING("nsRange::Create() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Collect editable text nodes in the given range.
+ AutoTArray<OwningNonNull<Text>, 16> arrayOfTextNodes;
+ DOMIterator iter;
+ if (NS_FAILED(iter.Init(*range))) {
+ return CaretPoint(EditorDOMPoint()); // Nothing to delete in the range.
+ }
+ iter.AppendNodesToArray(
+ +[](nsINode& aNode, void*) {
+ MOZ_ASSERT(aNode.IsText());
+ return HTMLEditUtils::IsSimplyEditableNode(aNode);
+ },
+ arrayOfTextNodes);
+ EditorDOMPoint pointToPutCaret;
+ for (OwningNonNull<Text>& textNode : arrayOfTextNodes) {
+ if (textNode == aStartPoint.GetContainer()) {
+ if (aStartPoint.IsEndOfContainer()) {
+ continue;
+ }
+ if (aStartPoint.IsStartOfContainer() &&
+ aTreatEmptyTextNodes !=
+ TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) {
+ AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(),
+ &pointToPutCaret);
+ nsresult rv = DeleteEmptyContentNodeWithTransaction(
+ MOZ_KnownLive(*aStartPoint.template ContainerAs<Text>()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("DeleteEmptyContentNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ continue;
+ }
+ AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ DeleteTextWithTransaction(MOZ_KnownLive(textNode),
+ aStartPoint.Offset(),
+ textNode->Length() - aStartPoint.Offset());
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ continue;
+ }
+
+ if (textNode == aEndPoint.GetContainer()) {
+ if (aEndPoint.IsStartOfContainer()) {
+ break;
+ }
+ if (aEndPoint.IsEndOfContainer() &&
+ aTreatEmptyTextNodes !=
+ TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) {
+ AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(),
+ &pointToPutCaret);
+ nsresult rv = DeleteEmptyContentNodeWithTransaction(
+ MOZ_KnownLive(*aEndPoint.template ContainerAs<Text>()));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "DeleteEmptyContentNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ DeleteTextWithTransaction(MOZ_KnownLive(textNode), 0,
+ aEndPoint.Offset());
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return CaretPoint(pointToPutCaret);
+ }
+
+ nsresult rv =
+ DeleteEmptyContentNodeWithTransaction(MOZ_KnownLive(textNode));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("DeleteEmptyContentNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ return CaretPoint(pointToPutCaret);
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoBlockElementsJoiner::JoinNodesDeepWithTransaction(
+ HTMLEditor& aHTMLEditor, nsIContent& aLeftContent,
+ nsIContent& aRightContent) {
+ // While the rightmost children and their descendants of the left node match
+ // the leftmost children and their descendants of the right node, join them
+ // up.
+
+ nsCOMPtr<nsIContent> leftContentToJoin = &aLeftContent;
+ nsCOMPtr<nsIContent> rightContentToJoin = &aRightContent;
+ nsCOMPtr<nsINode> parentNode = aRightContent.GetParentNode();
+
+ EditorDOMPoint ret;
+ while (leftContentToJoin && rightContentToJoin && parentNode &&
+ HTMLEditUtils::CanContentsBeJoined(*leftContentToJoin,
+ *rightContentToJoin)) {
+ // Do the join
+ Result<JoinNodesResult, nsresult> joinNodesResult =
+ aHTMLEditor.JoinNodesWithTransaction(*leftContentToJoin,
+ *rightContentToJoin);
+ if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
+ NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
+ return joinNodesResult.propagateErr();
+ }
+
+ ret = joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!ret.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (parentNode->IsText()) {
+ // We've joined all the way down to text nodes, we're done!
+ return ret;
+ }
+
+ // Get new left and right nodes, and begin anew
+ rightContentToJoin = ret.GetCurrentChildAtOffset();
+ if (rightContentToJoin) {
+ leftContentToJoin = rightContentToJoin->GetPreviousSibling();
+ } else {
+ leftContentToJoin = nullptr;
+ }
+
+ // Skip over non-editable nodes
+ while (leftContentToJoin && !EditorUtils::IsEditableContent(
+ *leftContentToJoin, EditorType::HTML)) {
+ leftContentToJoin = leftContentToJoin->GetPreviousSibling();
+ }
+ if (!leftContentToJoin) {
+ return ret;
+ }
+
+ while (rightContentToJoin && !EditorUtils::IsEditableContent(
+ *rightContentToJoin, EditorType::HTML)) {
+ rightContentToJoin = rightContentToJoin->GetNextSibling();
+ }
+ if (!rightContentToJoin) {
+ return ret;
+ }
+ }
+
+ if (!ret.IsSet()) {
+ NS_WARNING("HTMLEditor::JoinNodesDeepWithTransaction() joined no contents");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return ret;
+}
+
+Result<bool, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner::Prepare(
+ const HTMLEditor& aHTMLEditor, const Element& aEditingHost) {
+ mLeftBlockElement = HTMLEditUtils::GetInclusiveAncestorElement(
+ mInclusiveDescendantOfLeftBlockElement,
+ HTMLEditUtils::ClosestEditableBlockElementExceptHRElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ mRightBlockElement = HTMLEditUtils::GetInclusiveAncestorElement(
+ mInclusiveDescendantOfRightBlockElement,
+ HTMLEditUtils::ClosestEditableBlockElementExceptHRElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+
+ if (NS_WARN_IF(!IsSet())) {
+ mCanJoinBlocks = false;
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ // Don't join the blocks if both of them are basic structure of the HTML
+ // document (Note that `<body>` can be joined with its children).
+ if (mLeftBlockElement->IsAnyOfHTMLElements(nsGkAtoms::html, nsGkAtoms::head,
+ nsGkAtoms::body) &&
+ mRightBlockElement->IsAnyOfHTMLElements(nsGkAtoms::html, nsGkAtoms::head,
+ nsGkAtoms::body)) {
+ mCanJoinBlocks = false;
+ return false;
+ }
+
+ if (HTMLEditUtils::IsAnyTableElement(mLeftBlockElement) ||
+ HTMLEditUtils::IsAnyTableElement(mRightBlockElement)) {
+ // Do not try to merge table elements, cancel the deletion.
+ mCanJoinBlocks = false;
+ return false;
+ }
+
+ // Bail if both blocks the same
+ if (IsSameBlockElement()) {
+ mCanJoinBlocks = true; // XXX Anyway, Run() will ingore this case.
+ mFallbackToDeleteLeafContent = true;
+ return true;
+ }
+
+ // Joining a list item to its parent is a NOP.
+ if (HTMLEditUtils::IsAnyListElement(mLeftBlockElement) &&
+ HTMLEditUtils::IsListItem(mRightBlockElement) &&
+ mRightBlockElement->GetParentNode() == mLeftBlockElement) {
+ mCanJoinBlocks = false;
+ return true;
+ }
+
+ // Special rule here: if we are trying to join list items, and they are in
+ // different lists, join the lists instead.
+ if (HTMLEditUtils::IsListItem(mLeftBlockElement) &&
+ HTMLEditUtils::IsListItem(mRightBlockElement)) {
+ // XXX leftListElement and/or rightListElement may be not list elements.
+ Element* leftListElement = mLeftBlockElement->GetParentElement();
+ Element* rightListElement = mRightBlockElement->GetParentElement();
+ EditorDOMPoint atChildInBlock;
+ if (leftListElement && rightListElement &&
+ leftListElement != rightListElement &&
+ !EditorUtils::IsDescendantOf(*leftListElement, *mRightBlockElement,
+ &atChildInBlock) &&
+ !EditorUtils::IsDescendantOf(*rightListElement, *mLeftBlockElement,
+ &atChildInBlock)) {
+ // There are some special complications if the lists are descendants of
+ // the other lists' items. Note that it is okay for them to be
+ // descendants of the other lists themselves, which is the usual case for
+ // sublists in our implementation.
+ MOZ_DIAGNOSTIC_ASSERT(!atChildInBlock.IsSet());
+ mLeftBlockElement = leftListElement;
+ mRightBlockElement = rightListElement;
+ mNewListElementTagNameOfRightListElement =
+ Some(leftListElement->NodeInfo()->NameAtom());
+ }
+ }
+
+ if (!EditorUtils::IsDescendantOf(*mLeftBlockElement, *mRightBlockElement,
+ &mPointContainingTheOtherBlockElement)) {
+ Unused << EditorUtils::IsDescendantOf(
+ *mRightBlockElement, *mLeftBlockElement,
+ &mPointContainingTheOtherBlockElement);
+ }
+
+ if (mPointContainingTheOtherBlockElement.GetContainer() ==
+ mRightBlockElement) {
+ mPrecedingInvisibleBRElement =
+ WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
+ aHTMLEditor.ComputeEditingHost(),
+ EditorDOMPoint::AtEndOf(mLeftBlockElement),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ // `WhiteSpaceVisibilityKeeper::
+ // MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`
+ // returns ignored when:
+ // - No preceding invisible `<br>` element and
+ // - mNewListElementTagNameOfRightListElement is nothing and
+ // - There is no content to move from right block element.
+ if (!mPrecedingInvisibleBRElement) {
+ if (CanMergeLeftAndRightBlockElements()) {
+ // Always marked as handled in this case.
+ mFallbackToDeleteLeafContent = false;
+ } else {
+ // Marked as handled only when it actually moves a content node.
+ Result<bool, nsresult> firstLineHasContent =
+ AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
+ mPointContainingTheOtherBlockElement
+ .NextPoint<EditorDOMPoint>(),
+ aEditingHost);
+ mFallbackToDeleteLeafContent =
+ firstLineHasContent.isOk() && !firstLineHasContent.inspect();
+ }
+ } else {
+ // Marked as handled when deleting the invisible `<br>` element.
+ mFallbackToDeleteLeafContent = false;
+ }
+ } else if (mPointContainingTheOtherBlockElement.GetContainer() ==
+ mLeftBlockElement) {
+ mPrecedingInvisibleBRElement =
+ WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
+ aHTMLEditor.ComputeEditingHost(),
+ mPointContainingTheOtherBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ // `WhiteSpaceVisibilityKeeper::
+ // MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()`
+ // returns ignored when:
+ // - No preceding invisible `<br>` element and
+ // - mNewListElementTagNameOfRightListElement is some and
+ // - The right block element has no children
+ // or,
+ // - No preceding invisible `<br>` element and
+ // - mNewListElementTagNameOfRightListElement is nothing and
+ // - There is no content to move from right block element.
+ if (!mPrecedingInvisibleBRElement) {
+ if (CanMergeLeftAndRightBlockElements()) {
+ // Marked as handled only when it actualy moves a content node.
+ Result<bool, nsresult> rightBlockHasContent =
+ aHTMLEditor.CanMoveChildren(*mRightBlockElement,
+ *mLeftBlockElement);
+ mFallbackToDeleteLeafContent =
+ rightBlockHasContent.isOk() && !rightBlockHasContent.inspect();
+ } else {
+ // Marked as handled only when it actually moves a content node.
+ Result<bool, nsresult> firstLineHasContent =
+ AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
+ EditorDOMPoint(mRightBlockElement, 0u), aEditingHost);
+ mFallbackToDeleteLeafContent =
+ firstLineHasContent.isOk() && !firstLineHasContent.inspect();
+ }
+ } else {
+ // Marked as handled when deleting the invisible `<br>` element.
+ mFallbackToDeleteLeafContent = false;
+ }
+ } else {
+ mPrecedingInvisibleBRElement =
+ WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
+ aHTMLEditor.ComputeEditingHost(),
+ EditorDOMPoint::AtEndOf(mLeftBlockElement),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ // `WhiteSpaceVisibilityKeeper::
+ // MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` always
+ // return "handled".
+ mFallbackToDeleteLeafContent = false;
+ }
+
+ mCanJoinBlocks = true;
+ return true;
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ AutoInclusiveAncestorBlockElementsJoiner::ComputeRangesToDelete(
+ const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
+ AutoRangeArray& aRangesToDelete) const {
+ MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
+ MOZ_ASSERT(mLeftBlockElement);
+ MOZ_ASSERT(mRightBlockElement);
+
+ if (IsSameBlockElement()) {
+ if (!aCaretPoint.IsSet()) {
+ return NS_OK; // The ranges are not collapsed, keep them as-is.
+ }
+ nsresult rv = aRangesToDelete.Collapse(aCaretPoint);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+
+ EditorDOMPoint pointContainingTheOtherBlock;
+ if (!EditorUtils::IsDescendantOf(*mLeftBlockElement, *mRightBlockElement,
+ &pointContainingTheOtherBlock)) {
+ Unused << EditorUtils::IsDescendantOf(
+ *mRightBlockElement, *mLeftBlockElement, &pointContainingTheOtherBlock);
+ }
+ EditorDOMRange range =
+ WSRunScanner::GetRangeForDeletingBlockElementBoundaries(
+ aHTMLEditor, *mLeftBlockElement, *mRightBlockElement,
+ pointContainingTheOtherBlock);
+ if (!range.IsPositioned()) {
+ NS_WARNING(
+ "WSRunScanner::GetRangeForDeletingBlockElementBoundaries() failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (!aCaretPoint.IsSet()) {
+ // Don't shrink the original range.
+ bool noNeedToChangeStart = false;
+ const auto atStart =
+ aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (atStart.IsBefore(range.StartRef())) {
+ // If the range starts from end of a container, and computed block
+ // boundaries range starts from an invisible `<br>` element, we
+ // may need to shrink the range.
+ Element* editingHost = aHTMLEditor.ComputeEditingHost();
+ NS_WARNING_ASSERTION(editingHost, "There was no editing host");
+ nsIContent* nextContent =
+ atStart.IsEndOfContainer() && range.StartRef().GetChild() &&
+ HTMLEditUtils::IsInvisibleBRElement(
+ *range.StartRef().GetChild())
+ ? HTMLEditUtils::GetNextContent(
+ *atStart.ContainerAs<nsIContent>(),
+ {WalkTreeOption::IgnoreDataNodeExceptText,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ editingHost)
+ : nullptr;
+ if (!nextContent || nextContent != range.StartRef().GetChild()) {
+ noNeedToChangeStart = true;
+ range.SetStart(
+ aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>());
+ }
+ }
+ if (range.EndRef().IsBefore(
+ aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>())) {
+ if (noNeedToChangeStart) {
+ return NS_OK; // We don't need to modify the range.
+ }
+ range.SetEnd(aRangesToDelete.GetFirstRangeEndPoint<EditorDOMPoint>());
+ }
+ }
+ // XXX Oddly, we join blocks only at the first range.
+ nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
+ range.StartRef().ToRawRangeBoundary(),
+ range.EndRef().ToRawRangeBoundary());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoRangeArray::SetStartAndEnd() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner::Run(
+ HTMLEditor& aHTMLEditor, const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(mLeftBlockElement);
+ MOZ_ASSERT(mRightBlockElement);
+
+ if (IsSameBlockElement()) {
+ return EditActionResult::IgnoredResult();
+ }
+
+ if (!mCanJoinBlocks) {
+ return EditActionResult::HandledResult();
+ }
+
+ EditorDOMPoint startOfRightContent;
+
+ // If the left block element is in the right block element, move the hard
+ // line including the right block element to end of the left block.
+ // However, if we are merging list elements, we don't join them.
+ Result<EditActionResult, nsresult> result(NS_ERROR_NOT_INITIALIZED);
+ if (mPointContainingTheOtherBlockElement.GetContainer() ==
+ mRightBlockElement) {
+ startOfRightContent = mPointContainingTheOtherBlockElement.NextPoint();
+ if (Element* element = startOfRightContent.GetChildAs<Element>()) {
+ startOfRightContent =
+ HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
+ *element);
+ }
+ AutoTrackDOMPoint trackStartOfRightBlock(aHTMLEditor.RangeUpdaterRef(),
+ &startOfRightContent);
+ result = WhiteSpaceVisibilityKeeper::
+ MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement(
+ aHTMLEditor, MOZ_KnownLive(*mLeftBlockElement),
+ MOZ_KnownLive(*mRightBlockElement),
+ mPointContainingTheOtherBlockElement,
+ mNewListElementTagNameOfRightListElement,
+ MOZ_KnownLive(mPrecedingInvisibleBRElement), aEditingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement() "
+ "failed");
+ return result;
+ }
+ if (NS_WARN_IF(!startOfRightContent.IsSet()) ||
+ NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ // If the right block element is in the left block element:
+ // - move list item elements in the right block element to where the left
+ // list element is
+ // - or first hard line in the right block element to where:
+ // - the left block element is.
+ // - or the given left content in the left block is.
+ else if (mPointContainingTheOtherBlockElement.GetContainer() ==
+ mLeftBlockElement) {
+ startOfRightContent =
+ HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
+ *mRightBlockElement);
+ AutoTrackDOMPoint trackStartOfRightBlock(aHTMLEditor.RangeUpdaterRef(),
+ &startOfRightContent);
+ result = WhiteSpaceVisibilityKeeper::
+ MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement(
+ aHTMLEditor, MOZ_KnownLive(*mLeftBlockElement),
+ MOZ_KnownLive(*mRightBlockElement),
+ mPointContainingTheOtherBlockElement,
+ MOZ_KnownLive(*mInclusiveDescendantOfLeftBlockElement),
+ mNewListElementTagNameOfRightListElement,
+ MOZ_KnownLive(mPrecedingInvisibleBRElement), aEditingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement() "
+ "failed");
+ return result;
+ }
+ trackStartOfRightBlock.FlushAndStopTracking();
+ if (NS_WARN_IF(!startOfRightContent.IsSet()) ||
+ NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ }
+
+ // Normal case. Blocks are siblings, or at least close enough. An example
+ // of the latter is <p>paragraph</p><ul><li>one<li>two<li>three</ul>. The
+ // first li and the p are not true siblings, but we still want to join them
+ // if you backspace from li into p.
+ else {
+ MOZ_ASSERT(!mPointContainingTheOtherBlockElement.IsSet());
+
+ startOfRightContent =
+ HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
+ *mRightBlockElement);
+ AutoTrackDOMPoint trackStartOfRightBlock(aHTMLEditor.RangeUpdaterRef(),
+ &startOfRightContent);
+ result = WhiteSpaceVisibilityKeeper::
+ MergeFirstLineOfRightBlockElementIntoLeftBlockElement(
+ aHTMLEditor, MOZ_KnownLive(*mLeftBlockElement),
+ MOZ_KnownLive(*mRightBlockElement),
+ mNewListElementTagNameOfRightListElement,
+ MOZ_KnownLive(mPrecedingInvisibleBRElement), aEditingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MergeFirstLineOfRightBlockElementIntoLeftBlockElement() failed");
+ return result;
+ }
+ trackStartOfRightBlock.FlushAndStopTracking();
+ if (NS_WARN_IF(!startOfRightContent.IsSet()) ||
+ NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ // If we're deleting selection (meaning not replacing selection with new
+ // content), we should put caret to end of preceding text node if there is.
+ // Then, users can type text into it like the other browsers.
+ if (MayEditActionDeleteAroundCollapsedSelection(
+ aHTMLEditor.GetEditAction())) {
+ WSRunScanner scanner(&aEditingHost, startOfRightContent,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ WSScanResult maybePreviousText =
+ scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent);
+ if (maybePreviousText.IsContentEditable() &&
+ maybePreviousText.InVisibleOrCollapsibleCharacters()) {
+ mPointToPutCaret = maybePreviousText.Point<EditorDOMPoint>();
+ }
+ }
+ return result;
+}
+
+// static
+Result<bool, nsresult>
+HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
+ const EditorDOMPoint& aPointInHardLine, const Element& aEditingHost) {
+ if (NS_WARN_IF(!aPointInHardLine.IsSet()) ||
+ NS_WARN_IF(aPointInHardLine.IsInNativeAnonymousSubtree())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ RefPtr<nsRange> oneLineRange =
+ AutoRangeArray::CreateRangeWrappingStartAndEndLinesContainingBoundaries(
+ aPointInHardLine, aPointInHardLine,
+ EditSubAction::eMergeBlockContents,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle, aEditingHost);
+ if (!oneLineRange || oneLineRange->Collapsed() ||
+ !oneLineRange->IsPositioned() ||
+ !oneLineRange->GetStartContainer()->IsContent() ||
+ !oneLineRange->GetEndContainer()->IsContent()) {
+ return false;
+ }
+
+ // If there is only a padding `<br>` element in a empty block, it's selected
+ // by `UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement()`.
+ // However, it won't be moved. Although it'll be deleted,
+ // AutoMoveOneLineHandler returns "ignored". Therefore, we should return
+ // `false` in this case.
+ if (nsIContent* childContent = oneLineRange->GetChildAtStartOffset()) {
+ if (childContent->IsHTMLElement(nsGkAtoms::br) &&
+ childContent->GetParent()) {
+ if (const Element* blockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *childContent->GetParent(),
+ HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ if (HTMLEditUtils::IsEmptyNode(
+ *blockElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ return false;
+ }
+ }
+ }
+ }
+
+ nsINode* commonAncestor = oneLineRange->GetClosestCommonInclusiveAncestor();
+ // Currently, we move non-editable content nodes too.
+ EditorRawDOMPoint startPoint(oneLineRange->StartRef());
+ if (!startPoint.IsEndOfContainer()) {
+ return true;
+ }
+ EditorRawDOMPoint endPoint(oneLineRange->EndRef());
+ if (!endPoint.IsStartOfContainer()) {
+ return true;
+ }
+ if (startPoint.GetContainer() != commonAncestor) {
+ while (true) {
+ EditorRawDOMPoint pointInParent(startPoint.GetContainerAs<nsIContent>());
+ if (NS_WARN_IF(!pointInParent.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (pointInParent.GetContainer() == commonAncestor) {
+ startPoint = pointInParent;
+ break;
+ }
+ if (!pointInParent.IsEndOfContainer()) {
+ return true;
+ }
+ }
+ }
+ if (endPoint.GetContainer() != commonAncestor) {
+ while (true) {
+ EditorRawDOMPoint pointInParent(endPoint.GetContainerAs<nsIContent>());
+ if (NS_WARN_IF(!pointInParent.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (pointInParent.GetContainer() == commonAncestor) {
+ endPoint = pointInParent;
+ break;
+ }
+ if (!pointInParent.IsStartOfContainer()) {
+ return true;
+ }
+ }
+ }
+ // If start point and end point in the common ancestor are direct siblings,
+ // there is no content to move or delete.
+ // E.g., `<b>abc<br>[</b><i>]<br>def</i>`.
+ return startPoint.GetNextSiblingOfChild() != endPoint.GetChild();
+}
+
+nsresult HTMLEditor::AutoMoveOneLineHandler::Prepare(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointInHardLine,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointInHardLine.IsInContentNode());
+ MOZ_ASSERT(mPointToInsert.IsSetAndValid());
+
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
+ ("Prepare(aHTMLEditor=%p, aPointInHardLine=%s, aEditingHost=%s), "
+ "mPointToInsert=%s, mMoveToEndOfContainer=%s",
+ &aHTMLEditor, ToString(aPointInHardLine).c_str(),
+ ToString(aEditingHost).c_str(), ToString(mPointToInsert).c_str(),
+ ForceMoveToEndOfContainer() ? "MoveToEndOfContainer::Yes"
+ : "MoveToEndOfContainer::No"));
+
+ if (NS_WARN_IF(mPointToInsert.IsInNativeAnonymousSubtree())) {
+ MOZ_LOG(
+ gOneLineMoverLog, LogLevel::Error,
+ ("Failed because mPointToInsert was in a native anonymous subtree"));
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ mSrcInclusiveAncestorBlock =
+ aPointInHardLine.IsInContentNode()
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *aPointInHardLine.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)
+ : nullptr;
+ mDestInclusiveAncestorBlock =
+ mPointToInsert.IsInContentNode()
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *mPointToInsert.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)
+ : nullptr;
+ mMovingToParentBlock =
+ mDestInclusiveAncestorBlock && mSrcInclusiveAncestorBlock &&
+ mDestInclusiveAncestorBlock != mSrcInclusiveAncestorBlock &&
+ mSrcInclusiveAncestorBlock->IsInclusiveDescendantOf(
+ mDestInclusiveAncestorBlock);
+ mTopmostSrcAncestorBlockInDestBlock =
+ mMovingToParentBlock
+ ? AutoMoveOneLineHandler::
+ GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement(
+ *mSrcInclusiveAncestorBlock, *mDestInclusiveAncestorBlock)
+ : nullptr;
+ MOZ_ASSERT_IF(mMovingToParentBlock, mTopmostSrcAncestorBlockInDestBlock);
+
+ mPreserveWhiteSpaceStyle =
+ AutoMoveOneLineHandler::ConsiderWhetherPreserveWhiteSpaceStyle(
+ aPointInHardLine.GetContainerAs<nsIContent>(),
+ mDestInclusiveAncestorBlock);
+
+ AutoRangeArray rangesToWrapTheLine(aPointInHardLine);
+ rangesToWrapTheLine.ExtendRangesToWrapLines(
+ EditSubAction::eMergeBlockContents,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle, aEditingHost);
+ MOZ_ASSERT(rangesToWrapTheLine.Ranges().Length() <= 1u);
+ mLineRange = EditorDOMRange(rangesToWrapTheLine.FirstRangeRef());
+
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
+ ("mSrcInclusiveAncestorBlock=%s, mDestInclusiveAncestorBlock=%s, "
+ "mMovingToParentBlock=%s, mTopmostSrcAncestorBlockInDestBlock=%s, "
+ "mPreserveWhiteSpaceStyle=%s, mLineRange=%s",
+ mSrcInclusiveAncestorBlock
+ ? ToString(*mSrcInclusiveAncestorBlock).c_str()
+ : "nullptr",
+ mDestInclusiveAncestorBlock
+ ? ToString(*mDestInclusiveAncestorBlock).c_str()
+ : "nullptr",
+ mMovingToParentBlock ? "true" : "false",
+ mTopmostSrcAncestorBlockInDestBlock
+ ? ToString(*mTopmostSrcAncestorBlockInDestBlock).c_str()
+ : "nullptr",
+ ToString(mPreserveWhiteSpaceStyle).c_str(),
+ ToString(mLineRange).c_str()));
+
+ return NS_OK;
+}
+
+Result<CaretPoint, nsresult>
+HTMLEditor::AutoMoveOneLineHandler::SplitToMakeTheLineIsolated(
+ HTMLEditor& aHTMLEditor, const nsIContent& aNewContainer,
+ const Element& aEditingHost,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) const {
+ AutoRangeArray rangesToWrapTheLine(mLineRange);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ rangesToWrapTheLine
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ aHTMLEditor, BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ aEditingHost, &aNewContainer);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed");
+ return Err(splitResult.unwrapErr());
+ }
+ EditorDOMPoint pointToPutCaret;
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ nsresult rv = rangesToWrapTheLine.CollectEditTargetNodes(
+ aHTMLEditor, aOutArrayOfContents, EditSubAction::eMergeBlockContents,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eMergeBlockContents, CollectNonEditableNodes::Yes) failed");
+ return Err(rv);
+ }
+ return CaretPoint(pointToPutCaret);
+}
+
+// static
+Element* HTMLEditor::AutoMoveOneLineHandler::
+ GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement(
+ Element& aBlockElement, const Element& aAncestorElement) {
+ MOZ_ASSERT(aBlockElement.IsInclusiveDescendantOf(&aAncestorElement));
+ MOZ_ASSERT(HTMLEditUtils::IsBlockElement(
+ aBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle));
+
+ if (&aBlockElement == &aAncestorElement) {
+ return nullptr;
+ }
+
+ Element* lastBlockAncestor = &aBlockElement;
+ for (Element* element : aBlockElement.InclusiveAncestorsOfType<Element>()) {
+ if (element == &aAncestorElement) {
+ return lastBlockAncestor;
+ }
+ if (HTMLEditUtils::IsBlockElement(
+ *lastBlockAncestor,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ lastBlockAncestor = element;
+ }
+ }
+ return nullptr;
+}
+
+// static
+HTMLEditor::PreserveWhiteSpaceStyle
+HTMLEditor::AutoMoveOneLineHandler::ConsiderWhetherPreserveWhiteSpaceStyle(
+ const nsIContent* aContentInLine,
+ const Element* aInclusiveAncestorBlockOfInsertionPoint) {
+ if (MOZ_UNLIKELY(!aInclusiveAncestorBlockOfInsertionPoint)) {
+ return PreserveWhiteSpaceStyle::No;
+ }
+
+ // If we move content from or to <pre>, we don't need to preserve the
+ // white-space style for compatibility with both our traditional behavior
+ // and the other browsers.
+
+ // TODO: If `white-space` is specified by non-UA stylesheet, we should
+ // preserve it even if the right block is <pre> for compatibility with the
+ // other browsers.
+ const auto IsInclusiveDescendantOfPre = [](const nsIContent& aContent) {
+ // If the content has different `white-space` style from <pre>, we
+ // shouldn't treat it as a descendant of <pre> because web apps or
+ // the user intent to treat the white-spaces in aContent not as `pre`.
+ if (EditorUtils::GetComputedWhiteSpaceStyles(aContent).valueOr(std::pair(
+ StyleWhiteSpaceCollapse::Collapse, StyleTextWrapMode::Wrap)) !=
+ std::pair(StyleWhiteSpaceCollapse::Preserve,
+ StyleTextWrapMode::Nowrap)) {
+ return false;
+ }
+ for (const Element* element :
+ aContent.InclusiveAncestorsOfType<Element>()) {
+ if (element->IsHTMLElement(nsGkAtoms::pre)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ if (IsInclusiveDescendantOfPre(*aInclusiveAncestorBlockOfInsertionPoint) ||
+ MOZ_UNLIKELY(!aContentInLine) ||
+ IsInclusiveDescendantOfPre(*aContentInLine)) {
+ return PreserveWhiteSpaceStyle::No;
+ }
+ return PreserveWhiteSpaceStyle::Yes;
+}
+
+Result<MoveNodeResult, nsresult> HTMLEditor::AutoMoveOneLineHandler::Run(
+ HTMLEditor& aHTMLEditor, const Element& aEditingHost) {
+ EditorDOMPoint pointToInsert(NextInsertionPointRef());
+ MOZ_ASSERT(pointToInsert.IsInContentNode());
+
+ MOZ_LOG(
+ gOneLineMoverLog, LogLevel::Info,
+ ("Run(aHTMLEditor=%p, aEditingHost=%s), pointToInsert=%s", &aHTMLEditor,
+ ToString(aEditingHost).c_str(), ToString(pointToInsert).c_str()));
+
+ EditorDOMPoint pointToPutCaret;
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoTrackDOMPoint tackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+
+ Result<CaretPoint, nsresult> splitAtLineEdgesResult =
+ SplitToMakeTheLineIsolated(
+ aHTMLEditor,
+ MOZ_KnownLive(*pointToInsert.ContainerAs<nsIContent>()),
+ aEditingHost, arrayOfContents);
+ if (MOZ_UNLIKELY(splitAtLineEdgesResult.isErr())) {
+ NS_WARNING("AutoMoveOneLineHandler::SplitToMakeTheLineIsolated() failed");
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: SplitToMakeTheLineIsolated() failed"));
+ return splitAtLineEdgesResult.propagateErr();
+ }
+ splitAtLineEdgesResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Verbose,
+ ("Run: pointToPutCaret=%s", ToString(pointToPutCaret).c_str()));
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ aHTMLEditor.MaybeSplitElementsAtEveryBRElement(
+ arrayOfContents, EditSubAction::eMergeBlockContents);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eMergeBlockContents) failed");
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: MaybeSplitElementsAtEveryBRElement() failed"));
+ return splitAtBRElementsResult.propagateErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Verbose,
+ ("Run: pointToPutCaret=%s", ToString(pointToPutCaret).c_str()));
+ }
+
+ if (!pointToInsert.IsSetAndValid()) {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: Failed because pointToInsert pointed invalid position"));
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (aHTMLEditor.AllowsTransactionsToChangeSelection() &&
+ pointToPutCaret.IsSet()) {
+ nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: Failed because of "
+ "aHTMLEditor.CollapseSelectionTo(pointToPutCaret) failure"));
+ return Err(rv);
+ }
+ }
+
+ if (arrayOfContents.IsEmpty()) {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
+ ("Run: Did nothing because of no content to be moved"));
+ return MoveNodeResult::IgnoredResult(std::move(pointToInsert));
+ }
+
+ // Track the range which contains the moved contents.
+ if (ForceMoveToEndOfContainer()) {
+ pointToInsert = NextInsertionPointRef();
+ }
+ EditorDOMRange movedContentRange(pointToInsert);
+ MoveNodeResult moveContentsInLineResult =
+ MoveNodeResult::IgnoredResult(pointToInsert);
+ for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
+ ("Run: content=%s, pointToInsert=%s, movedContentRange=%s, "
+ "mPointToInsert=%s",
+ ToString(content.ref()).c_str(), ToString(pointToInsert).c_str(),
+ ToString(movedContentRange).c_str(),
+ ToString(mPointToInsert).c_str()));
+ {
+ AutoEditorDOMRangeChildrenInvalidator lockOffsets(movedContentRange);
+ AutoTrackDOMRange trackMovedContentRange(aHTMLEditor.RangeUpdaterRef(),
+ &movedContentRange);
+ // If the content is a block element, move all children of it to the
+ // new container, and then, remove the (probably) empty block element.
+ if (HTMLEditUtils::IsBlockElement(
+ content, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
+ ("Run: Unwrapping children of content because of a block"));
+ Result<MoveNodeResult, nsresult> moveChildrenResult =
+ aHTMLEditor.MoveChildrenWithTransaction(
+ MOZ_KnownLive(*content->AsElement()), pointToInsert,
+ mPreserveWhiteSpaceStyle, RemoveIfCommentNode::Yes);
+ if (MOZ_UNLIKELY(moveChildrenResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveChildrenWithTransaction() failed");
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: MoveChildrenWithTransaction() failed"));
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return moveChildrenResult;
+ }
+ moveContentsInLineResult |= moveChildrenResult.inspect();
+ // MOZ_KnownLive due to bug 1620312
+ nsresult rv =
+ aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(content));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: Aborted because DeleteNodeWithTransaction() caused "
+ "destroying the editor"));
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteNodeWithTransaction() failed, but ignored");
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Warning,
+ ("Run: Failed to delete content but the error was ignored"));
+ }
+ }
+ // If the moving content is a comment node or an empty inline node, we
+ // don't want it to appear in the dist paragraph.
+ else if (content->IsComment() ||
+ (content->IsText() && !content->AsText()->TextDataLength()) ||
+ HTMLEditUtils::IsEmptyInlineContainer(
+ content,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ nsCOMPtr<nsIContent> emptyContent =
+ HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
+ content, BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ &aEditingHost, pointToInsert.ContainerAs<nsIContent>());
+ if (!emptyContent) {
+ emptyContent = content;
+ }
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
+ ("Run: Deleting content because of %s%s",
+ content->IsComment() ? "a comment node"
+ : content->IsText() ? "an empty text node"
+ : "an empty inline container",
+ content != emptyContent
+ ? nsPrintfCString(" (deleting topmost empty ancestor: %s)",
+ ToString(*emptyContent).c_str())
+ .get()
+ : ""));
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*emptyContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: DeleteNodeWithTransaction() failed"));
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return Err(rv);
+ }
+ } else {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info, ("Run: Moving content"));
+ // MOZ_KnownLive due to bug 1620312
+ Result<MoveNodeResult, nsresult> moveNodeOrChildrenResult =
+ aHTMLEditor.MoveNodeOrChildrenWithTransaction(
+ MOZ_KnownLive(content), pointToInsert, mPreserveWhiteSpaceStyle,
+ RemoveIfCommentNode::Yes);
+ if (MOZ_UNLIKELY(moveNodeOrChildrenResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeOrChildrenWithTransaction() failed");
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: MoveNodeOrChildrenWithTransaction() failed"));
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return moveNodeOrChildrenResult;
+ }
+ moveContentsInLineResult |= moveNodeOrChildrenResult.inspect();
+ }
+ }
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
+ ("Run: movedContentRange=%s, mPointToInsert=%s",
+ ToString(movedContentRange).c_str(),
+ ToString(mPointToInsert).c_str()));
+ moveContentsInLineResult.MarkAsHandled();
+ if (NS_WARN_IF(!movedContentRange.IsPositioned())) {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: Failed because movedContentRange was not positioned"));
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // For backward compatibility, we should move contents to end of the
+ // container if the instance is created without specific insertion point.
+ if (ForceMoveToEndOfContainer()) {
+ pointToInsert = NextInsertionPointRef();
+ MOZ_ASSERT(pointToInsert.IsSet());
+ MOZ_ASSERT(movedContentRange.StartRef().EqualsOrIsBefore(pointToInsert));
+ movedContentRange.SetEnd(pointToInsert);
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Debug,
+ ("Run: Updated movedContentRange end to next insertion point"));
+ }
+ // And also if pointToInsert has been made invalid with removing preceding
+ // children, we should move the content to the end of the container.
+ else if (aHTMLEditor.MayHaveMutationEventListeners() &&
+ MOZ_UNLIKELY(!moveContentsInLineResult.NextInsertionPointRef()
+ .IsSetAndValid())) {
+ mPointToInsert.SetToEndOf(mPointToInsert.GetContainer());
+ pointToInsert = NextInsertionPointRef();
+ movedContentRange.SetEnd(pointToInsert);
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Debug,
+ ("Run: Updated mPointToInsert to end of container and updated "
+ "movedContentRange"));
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(
+ moveContentsInLineResult.NextInsertionPointRef().IsSet());
+ mPointToInsert = moveContentsInLineResult.NextInsertionPointRef();
+ pointToInsert = NextInsertionPointRef();
+ if (!aHTMLEditor.MayHaveMutationEventListeners() ||
+ movedContentRange.EndRef().IsBefore(pointToInsert)) {
+ MOZ_ASSERT(pointToInsert.IsSet());
+ MOZ_ASSERT(
+ movedContentRange.StartRef().EqualsOrIsBefore(pointToInsert));
+ movedContentRange.SetEnd(pointToInsert);
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Debug,
+ ("Run: Updated mPointToInsert and updated movedContentRange"));
+ } else {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Debug,
+ ("Run: Updated only mPointToInsert"));
+ }
+ }
+ }
+
+ // Nothing has been moved, we don't need to clean up unnecessary <br> element.
+ // And also if we're not moving content into a block, we can quit right now.
+ if (moveContentsInLineResult.Ignored() ||
+ MOZ_UNLIKELY(!mDestInclusiveAncestorBlock)) {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
+ (moveContentsInLineResult.Ignored()
+ ? "Run: Did nothing for any children"
+ : "Run: Finished (not dest block)"));
+ return moveContentsInLineResult;
+ }
+
+ // If we couldn't track the range to clean up, we should just stop cleaning up
+ // because returning error from here may change the behavior of web apps using
+ // mutation event listeners.
+ if (MOZ_UNLIKELY(!movedContentRange.IsPositioned() ||
+ movedContentRange.Collapsed())) {
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
+ (!movedContentRange.IsPositioned()
+ ? "Run: Finished (Couldn't track moved line)"
+ : "Run: Finished (Moved line was empty)"));
+ return moveContentsInLineResult;
+ }
+
+ nsresult rv = DeleteUnnecessaryTrailingLineBreakInMovedLineEnd(
+ aHTMLEditor, movedContentRange, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoMoveOneLineHandler::"
+ "DeleteUnnecessaryTrailingLineBreakInMovedLineEnd() failed");
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
+ ("Run: DeleteUnnecessaryTrailingLineBreakInMovedLineEnd() failed"));
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return Err(rv);
+ }
+
+ MOZ_LOG(gOneLineMoverLog, LogLevel::Info, ("Run: Finished"));
+ return moveContentsInLineResult;
+}
+
+nsresult HTMLEditor::AutoMoveOneLineHandler::
+ DeleteUnnecessaryTrailingLineBreakInMovedLineEnd(
+ HTMLEditor& aHTMLEditor, const EditorDOMRange& aMovedContentRange,
+ const Element& aEditingHost) const {
+ MOZ_ASSERT(mDestInclusiveAncestorBlock);
+ MOZ_ASSERT(aMovedContentRange.IsPositioned());
+ MOZ_ASSERT(!aMovedContentRange.Collapsed());
+
+ // If we didn't preserve white-space for backward compatibility and
+ // white-space becomes not preformatted, we need to clean it up the last text
+ // node if it ends with a preformatted line break.
+ if (mPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No) {
+ const RefPtr<Text> textNodeEndingWithUnnecessaryLineBreak = [&]() -> Text* {
+ Text* lastTextNode = Text::FromNodeOrNull(
+ mMovingToParentBlock
+ ? HTMLEditUtils::GetPreviousContent(
+ *mTopmostSrcAncestorBlockInDestBlock,
+ {WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ mDestInclusiveAncestorBlock)
+ : HTMLEditUtils::GetLastLeafContent(
+ *mDestInclusiveAncestorBlock,
+ {LeafNodeType::LeafNodeOrNonEditableNode}));
+ if (!lastTextNode ||
+ !HTMLEditUtils::IsSimplyEditableNode(*lastTextNode)) {
+ return nullptr;
+ }
+ const nsTextFragment& textFragment = lastTextNode->TextFragment();
+ const char16_t lastCh =
+ textFragment.GetLength()
+ ? textFragment.CharAt(textFragment.GetLength() - 1u)
+ : 0;
+ return lastCh == HTMLEditUtils::kNewLine &&
+ !EditorUtils::IsNewLinePreformatted(*lastTextNode)
+ ? lastTextNode
+ : nullptr;
+ }();
+ if (textNodeEndingWithUnnecessaryLineBreak) {
+ if (textNodeEndingWithUnnecessaryLineBreak->TextDataLength() == 1u) {
+ const RefPtr<Element> inlineElement =
+ HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
+ *textNodeEndingWithUnnecessaryLineBreak,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ &aEditingHost);
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ inlineElement ? static_cast<nsIContent&>(*inlineElement)
+ : static_cast<nsIContent&>(
+ *textNodeEndingWithUnnecessaryLineBreak));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ } else {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextWithTransaction(
+ *textNodeEndingWithUnnecessaryLineBreak,
+ textNodeEndingWithUnnecessaryLineBreak->TextDataLength() - 1u,
+ 1u);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ }
+ }
+ }
+
+ nsCOMPtr<nsIContent> lastLineBreakContent =
+ mMovingToParentBlock
+ ? HTMLEditUtils::GetUnnecessaryLineBreakContent(
+ *mTopmostSrcAncestorBlockInDestBlock,
+ ScanLineBreak::BeforeBlock)
+ : HTMLEditUtils::GetUnnecessaryLineBreakContent(
+ *mDestInclusiveAncestorBlock, ScanLineBreak::AtEndOfBlock);
+ if (!lastLineBreakContent) {
+ return NS_OK;
+ }
+ EditorRawDOMPoint atUnnecessaryLineBreak(lastLineBreakContent);
+ if (NS_WARN_IF(!atUnnecessaryLineBreak.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ // If the found unnecessary line break is not what we moved above, we
+ // shouldn't remove it. E.g., the web app may have inserted it intentionally.
+ MOZ_ASSERT(aMovedContentRange.StartRef().IsSetAndValid());
+ MOZ_ASSERT(aMovedContentRange.EndRef().IsSetAndValid());
+ if (!aMovedContentRange.Contains(atUnnecessaryLineBreak)) {
+ return NS_OK;
+ }
+
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+ // If it's a text node and ending with a preformatted line break, we should
+ // delete it.
+ if (Text* textNode = Text::FromNode(lastLineBreakContent)) {
+ MOZ_ASSERT(EditorUtils::IsNewLinePreformatted(*textNode));
+ if (textNode->TextDataLength() > 1) {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextWithTransaction(
+ MOZ_KnownLive(*textNode), textNode->TextDataLength() - 1u, 1u);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ // IgnoreCaretPointSuggestion() because of dontChangeMySelection above.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+ }
+ } else {
+ MOZ_ASSERT(lastLineBreakContent->IsHTMLElement(nsGkAtoms::br));
+ }
+ // If last line break content is the only content of its inline parent, we
+ // should remove the parent too.
+ if (const RefPtr<Element> inlineElement =
+ HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
+ *lastLineBreakContent,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ &aEditingHost)) {
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*inlineElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ // Or if the text node has only the preformatted line break or <br> element,
+ // we should remove it.
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*lastLineBreakContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+}
+
+Result<bool, nsresult> HTMLEditor::CanMoveNodeOrChildren(
+ const nsIContent& aContent, const nsINode& aNewContainer) const {
+ if (HTMLEditUtils::CanNodeContain(aNewContainer, aContent)) {
+ return true;
+ }
+ if (aContent.IsElement()) {
+ return CanMoveChildren(*aContent.AsElement(), aNewContainer);
+ }
+ return true;
+}
+
+Result<MoveNodeResult, nsresult> HTMLEditor::MoveNodeOrChildrenWithTransaction(
+ nsIContent& aContentToMove, const EditorDOMPoint& aPointToInsert,
+ PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle,
+ RemoveIfCommentNode aRemoveIfCommentNode) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsert.IsInContentNode());
+
+ const auto destWhiteSpaceStyles =
+ [&]() -> Maybe<std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode>> {
+ if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No ||
+ !aPointToInsert.IsInContentNode()) {
+ return Nothing();
+ }
+ auto styles = EditorUtils::GetComputedWhiteSpaceStyles(
+ *aPointToInsert.ContainerAs<nsIContent>());
+ if (NS_WARN_IF(styles.isSome() &&
+ styles.value().first ==
+ StyleWhiteSpaceCollapse::PreserveSpaces)) {
+ return Nothing();
+ }
+ return styles;
+ }();
+ const auto srcWhiteSpaceStyles =
+ [&]() -> Maybe<std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode>> {
+ if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No) {
+ return Nothing();
+ }
+ auto styles = EditorUtils::GetComputedWhiteSpaceStyles(aContentToMove);
+ if (NS_WARN_IF(styles.isSome() &&
+ styles.value().first ==
+ StyleWhiteSpaceCollapse::PreserveSpaces)) {
+ return Nothing();
+ }
+ return styles;
+ }();
+ // Get the `white-space` shorthand form for the given collapse + mode pair.
+ const auto GetWhiteSpaceStyleValue =
+ [](std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode> aStyles) {
+ if (aStyles.second == StyleTextWrapMode::Wrap) {
+ switch (aStyles.first) {
+ case StyleWhiteSpaceCollapse::Collapse:
+ return u"normal"_ns;
+ case StyleWhiteSpaceCollapse::Preserve:
+ return u"pre-wrap"_ns;
+ case StyleWhiteSpaceCollapse::PreserveBreaks:
+ return u"pre-line"_ns;
+ case StyleWhiteSpaceCollapse::PreserveSpaces:
+ return u"preserve-spaces"_ns;
+ case StyleWhiteSpaceCollapse::BreakSpaces:
+ return u"break-spaces"_ns;
+ }
+ } else {
+ switch (aStyles.first) {
+ case StyleWhiteSpaceCollapse::Collapse:
+ return u"nowrap"_ns;
+ case StyleWhiteSpaceCollapse::Preserve:
+ return u"pre"_ns;
+ case StyleWhiteSpaceCollapse::PreserveBreaks:
+ return u"nowrap preserve-breaks"_ns;
+ case StyleWhiteSpaceCollapse::PreserveSpaces:
+ return u"nowrap preserve-spaces"_ns;
+ case StyleWhiteSpaceCollapse::BreakSpaces:
+ return u"nowrap break-spaces"_ns;
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE("all values should be handled above!");
+ return u"normal"_ns;
+ };
+
+ if (aRemoveIfCommentNode == RemoveIfCommentNode::Yes &&
+ aContentToMove.IsComment()) {
+ EditorDOMPoint pointToInsert(aPointToInsert);
+ {
+ AutoTrackDOMPoint trackPointToInsert(RangeUpdaterRef(), &pointToInsert);
+ nsresult rv = DeleteNodeWithTransaction(aContentToMove);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ if (NS_WARN_IF(!pointToInsert.IsSetAndValid())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ return MoveNodeResult::HandledResult(std::move(pointToInsert));
+ }
+
+ // Check if this node can go into the destination node
+ if (HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(),
+ aContentToMove)) {
+ EditorDOMPoint pointToInsert(aPointToInsert);
+ // Preserve white-space in the new position with using `style` attribute.
+ // This is additional path from point of view of our traditional behavior.
+ // Therefore, ignore errors especially if we got unexpected DOM tree.
+ if (destWhiteSpaceStyles.isSome() && srcWhiteSpaceStyles.isSome() &&
+ destWhiteSpaceStyles.value() != srcWhiteSpaceStyles.value()) {
+ // Set `white-space` with `style` attribute if it's nsStyledElement.
+ if (nsStyledElement* styledElement =
+ nsStyledElement::FromNode(&aContentToMove)) {
+ DebugOnly<nsresult> rvIgnored =
+ CSSEditUtils::SetCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::white_space,
+ GetWhiteSpaceStyleValue(srcWhiteSpaceStyles.value()));
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::SetCSSPropertyWithTransaction("
+ "nsGkAtoms::white_space) failed, but ignored");
+ }
+ // Otherwise, if the dest container can have <span> element and <span>
+ // element can have the moving content node, we should insert it.
+ else if (HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(),
+ *nsGkAtoms::span) &&
+ HTMLEditUtils::CanNodeContain(*nsGkAtoms::span,
+ aContentToMove)) {
+ RefPtr<Element> newSpanElement = CreateHTMLContent(nsGkAtoms::span);
+ if (NS_WARN_IF(!newSpanElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsAutoString styleAttrValue(u"white-space: "_ns);
+ styleAttrValue.Append(
+ GetWhiteSpaceStyleValue(srcWhiteSpaceStyles.value()));
+ IgnoredErrorResult error;
+ newSpanElement->SetAttr(nsGkAtoms::style, styleAttrValue, error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "Element::SetAttr(nsGkAtoms::span) failed");
+ if (MOZ_LIKELY(!error.Failed())) {
+ Result<CreateElementResult, nsresult> insertSpanElementResult =
+ InsertNodeWithTransaction<Element>(*newSpanElement,
+ aPointToInsert);
+ if (MOZ_UNLIKELY(insertSpanElementResult.isErr())) {
+ if (NS_WARN_IF(insertSpanElementResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::InsertNodeWithTransaction() failed, but ignored");
+ } else {
+ // We should move the node into the new <span> to preserve the
+ // style.
+ pointToInsert.Set(newSpanElement, 0u);
+ // We should put caret after aContentToMove after moving it so that
+ // we do not need the suggested caret point here.
+ insertSpanElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+ }
+ }
+ }
+ // If it can, move it there.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeWithTransaction(aContentToMove, pointToInsert);
+ NS_WARNING_ASSERTION(moveNodeResult.isOk(),
+ "HTMLEditor::MoveNodeWithTransaction() failed");
+ // XXX This is odd to override the handled state here, but stopping this
+ // hits an NS_ASSERTION in WhiteSpaceVisibilityKeeper::
+ // MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement.
+ if (moveNodeResult.isOk()) {
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ unwrappedMoveNodeResult.MarkAsHandled();
+ return unwrappedMoveNodeResult;
+ }
+ return moveNodeResult;
+ }
+
+ // If it can't, move its children (if any), and then delete it.
+ auto moveNodeResult =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<MoveNodeResult, nsresult> {
+ if (!aContentToMove.IsElement()) {
+ return MoveNodeResult::HandledResult(aPointToInsert);
+ }
+ Result<MoveNodeResult, nsresult> moveChildrenResult =
+ MoveChildrenWithTransaction(MOZ_KnownLive(*aContentToMove.AsElement()),
+ aPointToInsert, aPreserveWhiteSpaceStyle,
+ aRemoveIfCommentNode);
+ NS_WARNING_ASSERTION(moveChildrenResult.isOk(),
+ "HTMLEditor::MoveChildrenWithTransaction() failed");
+ return moveChildrenResult;
+ }();
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ return moveNodeResult; // Already warned in the lambda.
+ }
+
+ nsresult rv = DeleteNodeWithTransaction(aContentToMove);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ return Err(rv);
+ }
+ if (!MayHaveMutationEventListeners()) {
+ return moveNodeResult;
+ }
+ // Mutation event listener may make `offset` value invalid with
+ // removing some previous children while we call
+ // `DeleteNodeWithTransaction()` so that we should adjust it here.
+ if (moveNodeResult.inspect().NextInsertionPointRef().IsSetAndValid()) {
+ return moveNodeResult;
+ }
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ return MoveNodeResult::HandledResult(
+ EditorDOMPoint::AtEndOf(*aPointToInsert.GetContainer()));
+}
+
+Result<bool, nsresult> HTMLEditor::CanMoveChildren(
+ const Element& aElement, const nsINode& aNewContainer) const {
+ if (NS_WARN_IF(&aElement == &aNewContainer)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ for (nsIContent* childContent = aElement.GetFirstChild(); childContent;
+ childContent = childContent->GetNextSibling()) {
+ Result<bool, nsresult> result =
+ CanMoveNodeOrChildren(*childContent, aNewContainer);
+ if (result.isErr() || result.inspect()) {
+ return result;
+ }
+ }
+ return false;
+}
+
+Result<MoveNodeResult, nsresult> HTMLEditor::MoveChildrenWithTransaction(
+ Element& aElement, const EditorDOMPoint& aPointToInsert,
+ PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle,
+ RemoveIfCommentNode aRemoveIfCommentNode) {
+ MOZ_ASSERT(aPointToInsert.IsSet());
+
+ if (NS_WARN_IF(&aElement == aPointToInsert.GetContainer())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ MoveNodeResult moveChildrenResult =
+ MoveNodeResult::IgnoredResult(aPointToInsert);
+ while (aElement.GetFirstChild()) {
+ Result<MoveNodeResult, nsresult> moveNodeOrChildrenResult =
+ MoveNodeOrChildrenWithTransaction(
+ MOZ_KnownLive(*aElement.GetFirstChild()),
+ moveChildrenResult.NextInsertionPointRef(),
+ aPreserveWhiteSpaceStyle, aRemoveIfCommentNode);
+ if (MOZ_UNLIKELY(moveNodeOrChildrenResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeOrChildrenWithTransaction() failed");
+ moveChildrenResult.IgnoreCaretPointSuggestion();
+ return moveNodeOrChildrenResult;
+ }
+ moveChildrenResult |= moveNodeOrChildrenResult.inspect();
+ }
+ return moveChildrenResult;
+}
+
+nsresult HTMLEditor::MoveAllChildren(nsINode& aContainer,
+ const EditorRawDOMPoint& aPointToInsert) {
+ if (!aContainer.HasChildren()) {
+ return NS_OK;
+ }
+ nsIContent* firstChild = aContainer.GetFirstChild();
+ if (NS_WARN_IF(!firstChild)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsIContent* lastChild = aContainer.GetLastChild();
+ if (NS_WARN_IF(!lastChild)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::MoveChildrenBetween() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::MoveChildrenBetween(
+ nsIContent& aFirstChild, nsIContent& aLastChild,
+ const EditorRawDOMPoint& aPointToInsert) {
+ nsCOMPtr<nsINode> oldContainer = aFirstChild.GetParentNode();
+ if (NS_WARN_IF(oldContainer != aLastChild.GetParentNode()) ||
+ NS_WARN_IF(!aPointToInsert.IsInContentNode()) ||
+ NS_WARN_IF(!aPointToInsert.CanContainerHaveChildren())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // First, store all children which should be moved to the new container.
+ AutoTArray<nsCOMPtr<nsIContent>, 10> children;
+ for (nsIContent* child = &aFirstChild; child;
+ child = child->GetNextSibling()) {
+ children.AppendElement(child);
+ if (child == &aLastChild) {
+ break;
+ }
+ }
+
+ if (NS_WARN_IF(children.LastElement() != &aLastChild)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsIContent> newContainer = aPointToInsert.ContainerAs<nsIContent>();
+ nsCOMPtr<nsIContent> nextNode = aPointToInsert.GetChild();
+ IgnoredErrorResult error;
+ for (size_t i = children.Length(); i > 0; --i) {
+ nsCOMPtr<nsIContent>& child = children[i - 1];
+ if (child->GetParentNode() != oldContainer) {
+ // If the child has been moved to different container, we shouldn't
+ // touch it.
+ continue;
+ }
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*child))) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ oldContainer->RemoveChild(*child, error);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (error.Failed()) {
+ NS_WARNING("nsINode::RemoveChild() failed");
+ return error.StealNSResult();
+ }
+ if (nextNode) {
+ // If we're not appending the children to the new container, we should
+ // check if referring next node of insertion point is still in the new
+ // container.
+ EditorRawDOMPoint pointToInsert(nextNode);
+ if (NS_WARN_IF(!pointToInsert.IsSet()) ||
+ NS_WARN_IF(pointToInsert.GetContainer() != newContainer)) {
+ // The next node of insertion point has been moved by mutation observer.
+ // Let's stop moving the remaining nodes.
+ // XXX Or should we move remaining children after the last moved child?
+ return NS_ERROR_FAILURE;
+ }
+ }
+ if (NS_WARN_IF(
+ !EditorUtils::IsEditableContent(*newContainer, EditorType::HTML))) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ newContainer->InsertBefore(*child, nextNode, error);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (error.Failed()) {
+ NS_WARNING("nsINode::InsertBefore() failed");
+ return error.StealNSResult();
+ }
+ // If the child was inserted or appended properly, the following children
+ // should be inserted before it. Otherwise, keep using current position.
+ if (child->GetParentNode() == newContainer) {
+ nextNode = child;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::MovePreviousSiblings(
+ nsIContent& aChild, const EditorRawDOMPoint& aPointToInsert) {
+ if (NS_WARN_IF(!aChild.GetParentNode())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsIContent* firstChild = aChild.GetParentNode()->GetFirstChild();
+ if (NS_WARN_IF(!firstChild)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsIContent* lastChild =
+ &aChild == firstChild ? firstChild : aChild.GetPreviousSibling();
+ if (NS_WARN_IF(!lastChild)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::MoveChildrenBetween() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::MoveInclusiveNextSiblings(
+ nsIContent& aChild, const EditorRawDOMPoint& aPointToInsert) {
+ if (NS_WARN_IF(!aChild.GetParentNode())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsIContent* lastChild = aChild.GetParentNode()->GetLastChild();
+ if (NS_WARN_IF(!lastChild)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = MoveChildrenBetween(aChild, *lastChild, aPointToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::MoveChildrenBetween() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
+ DeleteContentButKeepTableStructure(HTMLEditor& aHTMLEditor,
+ nsIContent& aContent) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+
+ if (!HTMLEditUtils::IsAnyTableElementButNotTable(&aContent)) {
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+
+ // XXX For performance, this should just call
+ // DeleteContentButKeepTableStructure() while there are children in
+ // aContent. If we need to avoid infinite loop because mutation event
+ // listeners can add unexpected nodes into aContent, we should just loop
+ // only original count of the children.
+ AutoTArray<OwningNonNull<nsIContent>, 10> childList;
+ for (nsIContent* child = aContent.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ childList.AppendElement(*child);
+ }
+
+ for (const auto& child : childList) {
+ // MOZ_KnownLive because 'childList' is guaranteed to
+ // keep it alive.
+ nsresult rv =
+ DeleteContentButKeepTableStructure(aHTMLEditor, MOZ_KnownLive(child));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteContentButKeepTableStructure() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::DeleteMostAncestorMailCiteElementIfEmpty(
+ nsIContent& aContent) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // The element must be `<blockquote type="cite">` or
+ // `<span _moz_quote="true">`.
+ RefPtr<Element> mailCiteElement =
+ GetMostDistantAncestorMailCiteElement(aContent);
+ if (!mailCiteElement) {
+ return NS_OK;
+ }
+ bool seenBR = false;
+ if (!HTMLEditUtils::IsEmptyNode(
+ *mailCiteElement,
+ {EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ &seenBR)) {
+ return NS_OK;
+ }
+ EditorDOMPoint atEmptyMailCiteElement(mailCiteElement);
+ {
+ AutoEditorDOMPointChildInvalidator lockOffset(atEmptyMailCiteElement);
+ nsresult rv = DeleteNodeWithTransaction(*mailCiteElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+
+ if (!atEmptyMailCiteElement.IsSet() || !seenBR) {
+ NS_WARNING_ASSERTION(
+ atEmptyMailCiteElement.IsSet(),
+ "Mutation event listener might changed the DOM tree during "
+ "EditorBase::DeleteNodeWithTransaction(), but ignored");
+ return NS_OK;
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, atEmptyMailCiteElement);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ nsresult rv = CollapseSelectionTo(
+ EditorRawDOMPoint(insertBRElementResult.inspect().GetNewNode()));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::::CollapseSelectionTo() failed, but ignored");
+ return NS_OK;
+}
+
+Element* HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
+ ScanEmptyBlockInclusiveAncestor(const HTMLEditor& aHTMLEditor,
+ nsIContent& aStartContent) {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!mEmptyInclusiveAncestorBlockElement);
+
+ // If we are inside an empty block, delete it.
+ // Note: do NOT delete table elements this way.
+ // Note: do NOT delete non-editable block element.
+ Element* editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement(
+ aStartContent, HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (!editableBlockElement) {
+ return nullptr;
+ }
+ // XXX Perhaps, this is slow loop. If empty blocks are nested, then,
+ // each block checks whether it's empty or not. However, descendant
+ // blocks are checked again and again by IsEmptyNode(). Perhaps, it
+ // should be able to take "known empty element" for avoiding same checks.
+ while (editableBlockElement &&
+ HTMLEditUtils::IsRemovableFromParentNode(*editableBlockElement) &&
+ !HTMLEditUtils::IsAnyTableElement(editableBlockElement) &&
+ HTMLEditUtils::IsEmptyNode(*editableBlockElement)) {
+ // If the removable empty list item is a child of editing host list element,
+ // we should not delete it.
+ if (HTMLEditUtils::IsListItem(editableBlockElement)) {
+ Element* const parentElement = editableBlockElement->GetParentElement();
+ if (parentElement && HTMLEditUtils::IsAnyListElement(parentElement) &&
+ !HTMLEditUtils::IsRemovableFromParentNode(*parentElement) &&
+ HTMLEditUtils::IsEmptyNode(*parentElement)) {
+ break;
+ }
+ }
+ mEmptyInclusiveAncestorBlockElement = editableBlockElement;
+ editableBlockElement = HTMLEditUtils::GetAncestorElement(
+ *mEmptyInclusiveAncestorBlockElement,
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ }
+ if (!mEmptyInclusiveAncestorBlockElement) {
+ return nullptr;
+ }
+
+ // XXX Because of not checking whether found block element is editable
+ // in the above loop, empty ediable block element may be overwritten
+ // with empty non-editable clock element. Therefore, we fail to
+ // remove the found empty nodes.
+ if (NS_WARN_IF(!mEmptyInclusiveAncestorBlockElement->IsEditable()) ||
+ NS_WARN_IF(!mEmptyInclusiveAncestorBlockElement->GetParentElement())) {
+ mEmptyInclusiveAncestorBlockElement = nullptr;
+ }
+ return mEmptyInclusiveAncestorBlockElement;
+}
+
+nsresult HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
+ ComputeTargetRanges(const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount,
+ const Element& aEditingHost,
+ AutoRangeArray& aRangesToDelete) const {
+ MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement);
+
+ // We'll delete `mEmptyInclusiveAncestorBlockElement` node from the tree, but
+ // we should return the range from start/end of next/previous editable content
+ // to end/start of the element for compatiblity with the other browsers.
+ switch (aDirectionAndAmount) {
+ case nsIEditor::eNone:
+ break;
+ case nsIEditor::ePrevious:
+ case nsIEditor::ePreviousWord:
+ case nsIEditor::eToBeginningOfLine: {
+ EditorRawDOMPoint startPoint =
+ HTMLEditUtils::GetPreviousEditablePoint<EditorRawDOMPoint>(
+ *mEmptyInclusiveAncestorBlockElement, &aEditingHost,
+ // In this case, we don't join block elements so that we won't
+ // delete invisible trailing whitespaces in the previous element.
+ InvisibleWhiteSpaces::Preserve,
+ // In this case, we won't join table cells so that we should
+ // get a range which is in a table cell even if it's in a
+ // table.
+ TableBoundary::NoCrossAnyTableElement);
+ if (!startPoint.IsSet()) {
+ NS_WARNING(
+ "HTMLEditUtils::GetPreviousEditablePoint() didn't return a valid "
+ "point");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = aRangesToDelete.SetStartAndEnd(
+ startPoint,
+ EditorRawDOMPoint::AtEndOf(mEmptyInclusiveAncestorBlockElement));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoRangeArray::SetStartAndEnd() failed");
+ return rv;
+ }
+ case nsIEditor::eNext:
+ case nsIEditor::eNextWord:
+ case nsIEditor::eToEndOfLine: {
+ EditorRawDOMPoint endPoint =
+ HTMLEditUtils::GetNextEditablePoint<EditorRawDOMPoint>(
+ *mEmptyInclusiveAncestorBlockElement, &aEditingHost,
+ // In this case, we don't join block elements so that we won't
+ // delete invisible trailing whitespaces in the next element.
+ InvisibleWhiteSpaces::Preserve,
+ // In this case, we won't join table cells so that we should
+ // get a range which is in a table cell even if it's in a
+ // table.
+ TableBoundary::NoCrossAnyTableElement);
+ if (!endPoint.IsSet()) {
+ NS_WARNING(
+ "HTMLEditUtils::GetNextEditablePoint() didn't return a valid "
+ "point");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = aRangesToDelete.SetStartAndEnd(
+ EditorRawDOMPoint(mEmptyInclusiveAncestorBlockElement, 0), endPoint);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "AutoRangeArray::SetStartAndEnd() failed");
+ return rv;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Handle the nsIEditor::EDirection value");
+ break;
+ }
+ // No direction, let's select the element to be deleted.
+ nsresult rv =
+ aRangesToDelete.SelectNode(*mEmptyInclusiveAncestorBlockElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::SelectNode() failed");
+ return rv;
+}
+
+Result<RefPtr<Element>, nsresult>
+HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
+ MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor) {
+ MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement);
+ MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement());
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(mEmptyInclusiveAncestorBlockElement));
+
+ // If the found empty block is a list item element and its grand parent
+ // (i.e., parent of list element) is NOT a list element, insert <br>
+ // element before the list element which has the empty list item.
+ // This odd list structure may occur if `Document.execCommand("indent")`
+ // is performed for list items.
+ // XXX Chrome does not remove empty list elements when last content in
+ // last list item is deleted. We should follow it since current
+ // behavior is annoying when you type new list item with selecting
+ // all list items.
+ if (!HTMLEditUtils::IsFirstChild(*mEmptyInclusiveAncestorBlockElement,
+ {WalkTreeOption::IgnoreNonEditableNode})) {
+ return RefPtr<Element>();
+ }
+
+ EditorDOMPoint atParentOfEmptyListItem(
+ mEmptyInclusiveAncestorBlockElement->GetParentElement());
+ if (NS_WARN_IF(!atParentOfEmptyListItem.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (HTMLEditUtils::IsAnyListElement(atParentOfEmptyListItem.GetContainer())) {
+ return RefPtr<Element>();
+ }
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ aHTMLEditor.InsertBRElement(WithTransaction::Yes,
+ atParentOfEmptyListItem);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ nsresult rv = unwrappedInsertBRElementResult.SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ return unwrappedInsertBRElementResult.UnwrapNewNode();
+}
+
+Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoEmptyBlockAncestorDeleter::GetNewCaretPosition(
+ const HTMLEditor& aHTMLEditor,
+ nsIEditor::EDirection aDirectionAndAmount) const {
+ MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement);
+ MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement());
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+
+ switch (aDirectionAndAmount) {
+ case nsIEditor::eNext:
+ case nsIEditor::eNextWord:
+ case nsIEditor::eToEndOfLine: {
+ // Collapse Selection to next node of after empty block element
+ // if there is. Otherwise, to just after the empty block.
+ auto afterEmptyBlock(
+ EditorDOMPoint::After(mEmptyInclusiveAncestorBlockElement));
+ MOZ_ASSERT(afterEmptyBlock.IsSet());
+ if (nsIContent* nextContentOfEmptyBlock = HTMLEditUtils::GetNextContent(
+ afterEmptyBlock, {}, BlockInlineCheck::Unused,
+ aHTMLEditor.ComputeEditingHost())) {
+ EditorDOMPoint pt = HTMLEditUtils::GetGoodCaretPointFor<EditorDOMPoint>(
+ *nextContentOfEmptyBlock, aDirectionAndAmount);
+ if (!pt.IsSet()) {
+ NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return CaretPoint(std::move(pt));
+ }
+ if (NS_WARN_IF(!afterEmptyBlock.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return CaretPoint(std::move(afterEmptyBlock));
+ }
+ case nsIEditor::ePrevious:
+ case nsIEditor::ePreviousWord:
+ case nsIEditor::eToBeginningOfLine: {
+ // Collapse Selection to previous editable node of the empty block
+ // if there is. Otherwise, to after the empty block.
+ EditorRawDOMPoint atEmptyBlock(mEmptyInclusiveAncestorBlockElement);
+ if (nsIContent* previousContentOfEmptyBlock =
+ HTMLEditUtils::GetPreviousContent(
+ atEmptyBlock, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, aHTMLEditor.ComputeEditingHost())) {
+ EditorDOMPoint pt = HTMLEditUtils::GetGoodCaretPointFor<EditorDOMPoint>(
+ *previousContentOfEmptyBlock, aDirectionAndAmount);
+ if (!pt.IsSet()) {
+ NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return CaretPoint(std::move(pt));
+ }
+ auto afterEmptyBlock =
+ EditorDOMPoint::After(*mEmptyInclusiveAncestorBlockElement);
+ if (NS_WARN_IF(!afterEmptyBlock.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return CaretPoint(std::move(afterEmptyBlock));
+ }
+ case nsIEditor::eNone: {
+ // Collapse selection at the removing block when we are replacing
+ // selected content.
+ EditorDOMPoint atEmptyBlock(mEmptyInclusiveAncestorBlockElement);
+ if (NS_WARN_IF(!atEmptyBlock.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return CaretPoint(std::move(atEmptyBlock));
+ }
+ default:
+ MOZ_CRASH(
+ "AutoEmptyBlockAncestorDeleter doesn't support this action yet");
+ return Err(NS_ERROR_FAILURE);
+ }
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::Run(
+ HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount) {
+ MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement);
+ MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement());
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+
+ {
+ Result<EditActionResult, nsresult> result =
+ MaybeReplaceSubListWithNewListItem(aHTMLEditor);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "AutoEmptyBlockAncestorDeleter::MaybeReplaceSubListWithNewListItem() "
+ "failed");
+ return result;
+ }
+ if (result.inspect().Handled()) {
+ return result;
+ }
+ }
+
+ if (HTMLEditUtils::IsListItem(mEmptyInclusiveAncestorBlockElement)) {
+ Result<RefPtr<Element>, nsresult> result =
+ MaybeInsertBRElementBeforeEmptyListItemElement(aHTMLEditor);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "AutoEmptyBlockAncestorDeleter::"
+ "MaybeInsertBRElementBeforeEmptyListItemElement() failed");
+ return result.propagateErr();
+ }
+ // If a `<br>` element is inserted, caret should be moved to after it.
+ if (RefPtr<Element> brElement = result.unwrap()) {
+ nsresult rv =
+ aHTMLEditor.CollapseSelectionTo(EditorRawDOMPoint(brElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ } else {
+ Result<CaretPoint, nsresult> result =
+ GetNewCaretPosition(aHTMLEditor, aDirectionAndAmount);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("AutoEmptyBlockAncestorDeleter::GetNewCaretPosition() failed");
+ return result.propagateErr();
+ }
+ MOZ_ASSERT(result.inspect().HasCaretPointSuggestion());
+ nsresult rv = result.inspect().SuggestCaretPointTo(aHTMLEditor, {});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ }
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ MOZ_KnownLive(*mEmptyInclusiveAncestorBlockElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
+ AutoEmptyBlockAncestorDeleter::MaybeReplaceSubListWithNewListItem(
+ HTMLEditor& aHTMLEditor) {
+ // If we're deleting sublist element and it's the last list item of its parent
+ // list, we should replace it with a list element.
+ if (!HTMLEditUtils::IsAnyListElement(mEmptyInclusiveAncestorBlockElement)) {
+ return EditActionResult::IgnoredResult();
+ }
+ RefPtr<Element> parentElement =
+ mEmptyInclusiveAncestorBlockElement->GetParentElement();
+ if (!parentElement || !HTMLEditUtils::IsAnyListElement(parentElement) ||
+ !HTMLEditUtils::IsEmptyNode(
+ *parentElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ return EditActionResult::IgnoredResult();
+ }
+
+ nsCOMPtr<nsINode> nextSibling =
+ mEmptyInclusiveAncestorBlockElement->GetNextSibling();
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ MOZ_KnownLive(*mEmptyInclusiveAncestorBlockElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ Result<CreateElementResult, nsresult> insertListItemResult =
+ aHTMLEditor.CreateAndInsertElement(
+ WithTransaction::Yes,
+ parentElement->IsHTMLElement(nsGkAtoms::dl) ? *nsGkAtoms::dd
+ : *nsGkAtoms::li,
+ !nextSibling || nextSibling->GetParentNode() != parentElement
+ ? EditorDOMPoint::AtEndOf(*parentElement)
+ : EditorDOMPoint(nextSibling),
+ [](HTMLEditor& aHTMLEditor, Element& aNewElement,
+ const EditorDOMPoint& aPointToInsert) -> nsresult {
+ RefPtr<Element> brElement =
+ aHTMLEditor.CreateHTMLContent(nsGkAtoms::br);
+ if (MOZ_UNLIKELY(!brElement)) {
+ NS_WARNING(
+ "EditorBase::CreateHTMLContent(nsGkAtoms::br) failed, but "
+ "ignored");
+ return NS_OK; // Just gives up to insert <br>
+ }
+ IgnoredErrorResult error;
+ aNewElement.AppendChild(*brElement, error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "nsINode::AppendChild() failed, but ignored");
+ return NS_OK;
+ });
+ if (MOZ_UNLIKELY(insertListItemResult.isErr())) {
+ NS_WARNING("HTMLEditor::CreateAndInsertElement() failed");
+ return insertListItemResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertListItemResult =
+ insertListItemResult.unwrap();
+ unwrappedInsertListItemResult.IgnoreCaretPointSuggestion();
+ rv = aHTMLEditor.CollapseSelectionTo(
+ EditorRawDOMPoint(unwrappedInsertListItemResult.GetNewNode(), 0u));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+template <typename EditorDOMRangeType>
+Result<EditorRawDOMRange, nsresult>
+HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete(
+ const HTMLEditor& aHTMLEditor, const nsFrameSelection* aFrameSelection,
+ const EditorDOMRangeType& aRangeToDelete) const {
+ MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRangeToDelete.Collapsed());
+ MOZ_ASSERT(aRangeToDelete.IsPositioned());
+
+ const nsIContent* commonAncestor = nsIContent::FromNodeOrNull(
+ nsContentUtils::GetClosestCommonInclusiveAncestor(
+ aRangeToDelete.StartRef().GetContainer(),
+ aRangeToDelete.EndRef().GetContainer()));
+ if (MOZ_UNLIKELY(NS_WARN_IF(!commonAncestor))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Look for the common ancestor's block element. It's fine that we get
+ // non-editable block element which is ancestor of inline editing host
+ // because the following code checks editing host too.
+ const Element* const maybeNonEditableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *commonAncestor, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (NS_WARN_IF(!maybeNonEditableBlockElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Set up for loops and cache our root element
+ RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // If only one list element is selected, and if the list element is empty,
+ // we should delete only the list element. Or if the list element is not
+ // empty, we should make the list has only one empty list item element.
+ if (const Element* maybeListElement =
+ HTMLEditUtils::GetElementIfOnlyOneSelected(aRangeToDelete)) {
+ if (HTMLEditUtils::IsAnyListElement(maybeListElement) &&
+ !HTMLEditUtils::IsEmptyAnyListElement(*maybeListElement)) {
+ EditorRawDOMRange range =
+ HTMLEditUtils::GetRangeSelectingAllContentInAllListItems<
+ EditorRawDOMRange>(*maybeListElement);
+ if (range.IsPositioned()) {
+ if (EditorUtils::IsEditableContent(
+ *range.StartRef().ContainerAs<nsIContent>(),
+ EditorType::HTML) &&
+ EditorUtils::IsEditableContent(
+ *range.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)) {
+ return range;
+ }
+ }
+ // If the first and/or last list item is not editable, we need to do more
+ // complicated things probably, but we just delete the list element with
+ // invisible things around it for now since it must be rare case.
+ }
+ // Otherwise, if the list item is empty, we should delete it with invisible
+ // things around it.
+ }
+
+ // Find previous visible things before start of selection
+ EditorRawDOMRange rangeToDelete(aRangeToDelete);
+ if (rangeToDelete.StartRef().GetContainer() != maybeNonEditableBlockElement &&
+ rangeToDelete.StartRef().GetContainer() != editingHost) {
+ for (;;) {
+ WSScanResult backwardScanFromStartResult =
+ WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
+ editingHost, rangeToDelete.StartRef(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (!backwardScanFromStartResult.ReachedCurrentBlockBoundary()) {
+ break;
+ }
+ MOZ_ASSERT(backwardScanFromStartResult.GetContent() ==
+ WSRunScanner(editingHost, rangeToDelete.StartRef(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)
+ .GetStartReasonContent());
+ // We want to keep looking up. But stop if we are crossing table
+ // element boundaries, or if we hit the root.
+ if (HTMLEditUtils::IsAnyTableElement(
+ backwardScanFromStartResult.GetContent()) ||
+ backwardScanFromStartResult.GetContent() ==
+ maybeNonEditableBlockElement ||
+ backwardScanFromStartResult.GetContent() == editingHost) {
+ break;
+ }
+ // Don't cross list element boundary because we don't want to delete list
+ // element at start position unless it's empty.
+ if (HTMLEditUtils::IsAnyListElement(
+ backwardScanFromStartResult.GetContent()) &&
+ !HTMLEditUtils::IsEmptyAnyListElement(
+ *backwardScanFromStartResult.ElementPtr())) {
+ break;
+ }
+ // Don't cross flex-item/grid-item boundary to make new content inserted
+ // into it.
+ if (StaticPrefs::editor_block_inline_check_use_computed_style() &&
+ backwardScanFromStartResult.ContentIsElement() &&
+ HTMLEditUtils::IsFlexOrGridItem(
+ *backwardScanFromStartResult.ElementPtr())) {
+ break;
+ }
+ rangeToDelete.SetStart(
+ backwardScanFromStartResult.PointAtContent<EditorRawDOMPoint>());
+ }
+ if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint(
+ rangeToDelete.StartRef().GetContainer())) {
+ NS_WARNING("Computed start container was out of selection limiter");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ // Expand selection endpoint only if we don't pass an invisible `<br>`, or if
+ // we really needed to pass that `<br>` (i.e., its block is now totally
+ // selected).
+
+ // Find next visible things after end of selection
+ EditorDOMPoint atFirstInvisibleBRElement;
+ if (rangeToDelete.EndRef().GetContainer() != maybeNonEditableBlockElement &&
+ rangeToDelete.EndRef().GetContainer() != editingHost) {
+ for (;;) {
+ WSRunScanner wsScannerAtEnd(
+ editingHost, rangeToDelete.EndRef(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ WSScanResult forwardScanFromEndResult =
+ wsScannerAtEnd.ScanNextVisibleNodeOrBlockBoundaryFrom(
+ rangeToDelete.EndRef());
+ if (forwardScanFromEndResult.ReachedBRElement()) {
+ // XXX In my understanding, this is odd. The end reason may not be
+ // same as the reached <br> element because the equality is
+ // guaranteed only when ReachedCurrentBlockBoundary() returns true.
+ // However, looks like that this code assumes that
+ // GetEndReasonContent() returns the (or a) <br> element.
+ NS_ASSERTION(wsScannerAtEnd.GetEndReasonContent() ==
+ forwardScanFromEndResult.BRElementPtr(),
+ "End reason is not the reached <br> element");
+ if (HTMLEditUtils::IsVisibleBRElement(
+ *wsScannerAtEnd.GetEndReasonContent())) {
+ break;
+ }
+ if (!atFirstInvisibleBRElement.IsSet()) {
+ atFirstInvisibleBRElement =
+ rangeToDelete.EndRef().To<EditorDOMPoint>();
+ }
+ rangeToDelete.SetEnd(
+ EditorRawDOMPoint::After(*wsScannerAtEnd.GetEndReasonContent()));
+ continue;
+ }
+
+ if (forwardScanFromEndResult.ReachedCurrentBlockBoundary()) {
+ MOZ_ASSERT(forwardScanFromEndResult.GetContent() ==
+ wsScannerAtEnd.GetEndReasonContent());
+ // We want to keep looking up. But stop if we are crossing table
+ // element boundaries, or if we hit the root.
+ if (HTMLEditUtils::IsAnyTableElement(
+ forwardScanFromEndResult.GetContent()) ||
+ forwardScanFromEndResult.GetContent() ==
+ maybeNonEditableBlockElement ||
+ forwardScanFromEndResult.GetContent() == editingHost) {
+ break;
+ }
+ // Don't cross flex-item/grid-item boundary to make new content inserted
+ // into it.
+ if (StaticPrefs::editor_block_inline_check_use_computed_style() &&
+ forwardScanFromEndResult.ContentIsElement() &&
+ HTMLEditUtils::IsFlexOrGridItem(
+ *forwardScanFromEndResult.ElementPtr())) {
+ break;
+ }
+ rangeToDelete.SetEnd(
+ forwardScanFromEndResult.PointAfterContent<EditorRawDOMPoint>());
+ continue;
+ }
+
+ break;
+ }
+
+ if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint(
+ rangeToDelete.EndRef().GetContainer())) {
+ NS_WARNING("Computed end container was out of selection limiter");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ // If range boundaries are in list element, and the positions are very
+ // start/end of first/last list item, we may need to shrink the ranges for
+ // preventing to remove only all list item elements.
+ {
+ EditorRawDOMRange rangeToDeleteListOrLeaveOneEmptyListItem =
+ AutoDeleteRangesHandler::
+ GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements(
+ rangeToDelete);
+ if (rangeToDeleteListOrLeaveOneEmptyListItem.IsPositioned()) {
+ rangeToDelete = std::move(rangeToDeleteListOrLeaveOneEmptyListItem);
+ }
+ }
+
+ if (atFirstInvisibleBRElement.IsInContentNode()) {
+ // Find block node containing invisible `<br>` element.
+ if (const RefPtr<const Element> editableBlockContainingBRElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *atFirstInvisibleBRElement.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ if (rangeToDelete.Contains(
+ EditorRawDOMPoint(editableBlockContainingBRElement))) {
+ return rangeToDelete;
+ }
+ // Otherwise, the new range should end at the invisible `<br>`.
+ if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint(
+ atFirstInvisibleBRElement.GetContainer())) {
+ NS_WARNING(
+ "Computed end container (`<br>` element) was out of selection "
+ "limiter");
+ return Err(NS_ERROR_FAILURE);
+ }
+ rangeToDelete.SetEnd(atFirstInvisibleBRElement);
+ }
+ }
+
+ return rangeToDelete;
+}
+
+// static
+EditorRawDOMRange HTMLEditor::AutoDeleteRangesHandler::
+ GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements(
+ const EditorRawDOMRange& aRangeToDelete) {
+ MOZ_ASSERT(aRangeToDelete.IsPositionedAndValid());
+
+ auto GetDeepestEditableStartPointOfList = [](Element& aListElement) {
+ Element* const firstListItemElement =
+ HTMLEditUtils::GetFirstListItemElement(aListElement);
+ if (MOZ_UNLIKELY(!firstListItemElement)) {
+ return EditorRawDOMPoint();
+ }
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(*firstListItemElement,
+ EditorType::HTML))) {
+ return EditorRawDOMPoint(firstListItemElement);
+ }
+ return HTMLEditUtils::GetDeepestEditableStartPointOf<EditorRawDOMPoint>(
+ *firstListItemElement);
+ };
+
+ auto GetDeepestEditableEndPointOfList = [](Element& aListElement) {
+ Element* const lastListItemElement =
+ HTMLEditUtils::GetLastListItemElement(aListElement);
+ if (MOZ_UNLIKELY(!lastListItemElement)) {
+ return EditorRawDOMPoint();
+ }
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(*lastListItemElement,
+ EditorType::HTML))) {
+ return EditorRawDOMPoint::After(*lastListItemElement);
+ }
+ return HTMLEditUtils::GetDeepestEditableEndPointOf<EditorRawDOMPoint>(
+ *lastListItemElement);
+ };
+
+ Element* const startListElement =
+ aRangeToDelete.StartRef().IsInContentNode()
+ ? HTMLEditUtils::GetClosestInclusiveAncestorAnyListElement(
+ *aRangeToDelete.StartRef().ContainerAs<nsIContent>())
+ : nullptr;
+ Element* const endListElement =
+ aRangeToDelete.EndRef().IsInContentNode()
+ ? HTMLEditUtils::GetClosestInclusiveAncestorAnyListElement(
+ *aRangeToDelete.EndRef().ContainerAs<nsIContent>())
+ : nullptr;
+ if (!startListElement && !endListElement) {
+ return EditorRawDOMRange();
+ }
+
+ // FIXME: If there are invalid children, we cannot handle first/last list item
+ // elements properly. In that case, we should treat list elements and list
+ // item elements as normal block elements.
+ if (startListElement &&
+ NS_WARN_IF(!HTMLEditUtils::IsValidListElement(
+ *startListElement, HTMLEditUtils::TreatSubListElementAs::Valid))) {
+ return EditorRawDOMRange();
+ }
+ if (endListElement && startListElement != endListElement &&
+ NS_WARN_IF(!HTMLEditUtils::IsValidListElement(
+ *endListElement, HTMLEditUtils::TreatSubListElementAs::Valid))) {
+ return EditorRawDOMRange();
+ }
+
+ const bool startListElementIsEmpty =
+ startListElement &&
+ HTMLEditUtils::IsEmptyAnyListElement(*startListElement);
+ const bool endListElementIsEmpty =
+ startListElement == endListElement
+ ? startListElementIsEmpty
+ : endListElement &&
+ HTMLEditUtils::IsEmptyAnyListElement(*endListElement);
+ // If both list elements are empty, we should not shrink the range since
+ // we want to delete the list.
+ if (startListElementIsEmpty && endListElementIsEmpty) {
+ return EditorRawDOMRange();
+ }
+
+ // There may be invisible white-spaces and there are elements in the
+ // list items. Therefore, we need to compare the deepest positions
+ // and range boundaries.
+ EditorRawDOMPoint deepestStartPointOfStartList =
+ startListElement ? GetDeepestEditableStartPointOfList(*startListElement)
+ : EditorRawDOMPoint();
+ EditorRawDOMPoint deepestEndPointOfEndList =
+ endListElement ? GetDeepestEditableEndPointOfList(*endListElement)
+ : EditorRawDOMPoint();
+ if (MOZ_UNLIKELY(!deepestStartPointOfStartList.IsSet() &&
+ !deepestEndPointOfEndList.IsSet())) {
+ // FIXME: This does not work well if there is non-list-item contents in the
+ // list elements. Perhaps, for fixing this invalid cases, we need to wrap
+ // the content into new list item like Chrome.
+ return EditorRawDOMRange();
+ }
+
+ // We don't want to shrink the range into empty sublist.
+ if (deepestStartPointOfStartList.IsSet()) {
+ for (nsIContent* const maybeList :
+ deepestStartPointOfStartList.GetContainer()
+ ->InclusiveAncestorsOfType<nsIContent>()) {
+ if (aRangeToDelete.StartRef().GetContainer() == maybeList) {
+ break;
+ }
+ if (HTMLEditUtils::IsAnyListElement(maybeList) &&
+ HTMLEditUtils::IsEmptyAnyListElement(*maybeList->AsElement())) {
+ deepestStartPointOfStartList.Set(maybeList);
+ }
+ }
+ }
+ if (deepestEndPointOfEndList.IsSet()) {
+ for (nsIContent* const maybeList :
+ deepestEndPointOfEndList.GetContainer()
+ ->InclusiveAncestorsOfType<nsIContent>()) {
+ if (aRangeToDelete.EndRef().GetContainer() == maybeList) {
+ break;
+ }
+ if (HTMLEditUtils::IsAnyListElement(maybeList) &&
+ HTMLEditUtils::IsEmptyAnyListElement(*maybeList->AsElement())) {
+ deepestEndPointOfEndList.SetAfter(maybeList);
+ }
+ }
+ }
+
+ const EditorRawDOMPoint deepestEndPointOfStartList =
+ startListElement ? GetDeepestEditableEndPointOfList(*startListElement)
+ : EditorRawDOMPoint();
+ MOZ_ASSERT_IF(deepestStartPointOfStartList.IsSet(),
+ deepestEndPointOfStartList.IsSet());
+ MOZ_ASSERT_IF(!deepestStartPointOfStartList.IsSet(),
+ !deepestEndPointOfStartList.IsSet());
+
+ const bool rangeStartsFromBeginningOfStartList =
+ deepestStartPointOfStartList.IsSet() &&
+ aRangeToDelete.StartRef().EqualsOrIsBefore(deepestStartPointOfStartList);
+ const bool rangeEndsByEndingOfStartListOrLater =
+ !deepestEndPointOfStartList.IsSet() ||
+ deepestEndPointOfStartList.EqualsOrIsBefore(aRangeToDelete.EndRef());
+ const bool rangeEndsByEndingOfEndList =
+ deepestEndPointOfEndList.IsSet() &&
+ deepestEndPointOfEndList.EqualsOrIsBefore(aRangeToDelete.EndRef());
+
+ EditorRawDOMRange newRangeToDelete;
+ // If all over the list element at start boundary is selected, we should
+ // shrink the range to start from the first list item to avoid to delete
+ // all list items.
+ if (!startListElementIsEmpty && rangeStartsFromBeginningOfStartList &&
+ rangeEndsByEndingOfStartListOrLater) {
+ newRangeToDelete.SetStart(EditorRawDOMPoint(
+ deepestStartPointOfStartList.ContainerAs<nsIContent>(), 0u));
+ }
+ // If all over the list element at end boundary is selected, and...
+ if (!endListElementIsEmpty && rangeEndsByEndingOfEndList) {
+ // If the range starts before the range at end boundary of the range,
+ // we want to delete the list completely, thus, we should extend the
+ // range to contain the list element.
+ if (aRangeToDelete.StartRef().IsBefore(
+ EditorRawDOMPoint(endListElement, 0u))) {
+ newRangeToDelete.SetEnd(EditorRawDOMPoint::After(*endListElement));
+ MOZ_ASSERT_IF(newRangeToDelete.StartRef().IsSet(),
+ newRangeToDelete.IsPositionedAndValid());
+ }
+ // Otherwise, if the range starts in the end list element, we shouldn't
+ // delete the list. Therefore, we should shrink the range to end by end
+ // of the last list item element to avoid to delete all list items.
+ else {
+ newRangeToDelete.SetEnd(EditorRawDOMPoint::AtEndOf(
+ *deepestEndPointOfEndList.ContainerAs<nsIContent>()));
+ MOZ_ASSERT_IF(newRangeToDelete.StartRef().IsSet(),
+ newRangeToDelete.IsPositionedAndValid());
+ }
+ }
+
+ if (!newRangeToDelete.StartRef().IsSet() &&
+ !newRangeToDelete.EndRef().IsSet()) {
+ return EditorRawDOMRange();
+ }
+
+ if (!newRangeToDelete.StartRef().IsSet()) {
+ newRangeToDelete.SetStart(aRangeToDelete.StartRef());
+ MOZ_ASSERT(newRangeToDelete.IsPositionedAndValid());
+ }
+ if (!newRangeToDelete.EndRef().IsSet()) {
+ newRangeToDelete.SetEnd(aRangeToDelete.EndRef());
+ MOZ_ASSERT(newRangeToDelete.IsPositionedAndValid());
+ }
+
+ return newRangeToDelete;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorDocumentCommands.cpp b/editor/libeditor/HTMLEditorDocumentCommands.cpp
new file mode 100644
index 0000000000..ae1af5814b
--- /dev/null
+++ b/editor/libeditor/HTMLEditorDocumentCommands.cpp
@@ -0,0 +1,478 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditorCommands.h"
+
+#include "EditorBase.h" // for EditorBase
+#include "ErrorList.h"
+#include "HTMLEditor.h" // for HTMLEditor
+
+#include "mozilla/BasePrincipal.h" // for nsIPrincipal::IsSystemPrincipal()
+#include "mozilla/dom/Element.h" // for Element
+#include "mozilla/dom/Document.h" // for Document
+#include "mozilla/dom/HTMLInputElement.h" // for HTMLInputElement
+#include "mozilla/dom/HTMLTextAreaElement.h" // for HTMLTextAreaElement
+
+#include "nsCommandParams.h" // for nsCommandParams
+#include "nsIEditingSession.h" // for nsIEditingSession, etc
+#include "nsIPrincipal.h" // for nsIPrincipal
+#include "nsISupportsImpl.h" // for nsPresContext::Release
+#include "nsISupportsUtils.h" // for NS_IF_ADDREF
+#include "nsIURI.h" // for nsIURI
+#include "nsPresContext.h" // for nsPresContext
+
+// defines
+#define STATE_ENABLED "state_enabled"
+#define STATE_ALL "state_all"
+#define STATE_ATTRIBUTE "state_attribute"
+#define STATE_DATA "state_data"
+
+namespace mozilla {
+
+using namespace dom;
+
+/*****************************************************************************
+ * mozilla::SetDocumentStateCommand
+ *
+ * Commands for document state that may be changed via doCommandParams
+ * As of 11/11/02, this is just "cmd_setDocumentModified"
+ * Note that you can use the same command class, SetDocumentStateCommand,
+ * for more than one of this type of command
+ * We check the input command param for different behavior
+ *****************************************************************************/
+
+StaticRefPtr<SetDocumentStateCommand> SetDocumentStateCommand::sInstance;
+
+bool SetDocumentStateCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ switch (aCommand) {
+ case Command::SetDocumentReadOnly:
+ return !!aEditorBase;
+ default:
+ // The other commands are always enabled if given editor is an HTMLEditor.
+ return aEditorBase && aEditorBase->IsHTMLEditor();
+ }
+}
+
+nsresult SetDocumentStateCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult SetDocumentStateCommand::DoCommandParam(
+ Command aCommand, const Maybe<bool>& aBoolParam, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(aBoolParam.isNothing())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (aCommand != Command::SetDocumentReadOnly &&
+ NS_WARN_IF(!aEditorBase.IsHTMLEditor())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ switch (aCommand) {
+ case Command::SetDocumentModified: {
+ if (aBoolParam.value()) {
+ nsresult rv = aEditorBase.IncrementModificationCount(1);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::IncrementModificationCount() failed");
+ return rv;
+ }
+ nsresult rv = aEditorBase.ResetModificationCount();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::ResetModificationCount() failed");
+ return rv;
+ }
+ case Command::SetDocumentReadOnly: {
+ if (aEditorBase.IsTextEditor()) {
+ Element* inputOrTextArea = aEditorBase.GetExposedRoot();
+ if (NS_WARN_IF(!inputOrTextArea)) {
+ return NS_ERROR_FAILURE;
+ }
+ // Perhaps, this legacy command shouldn't work with
+ // `<input type="file">` and `<input type="number">.
+ if (inputOrTextArea->IsInNativeAnonymousSubtree()) {
+ return NS_ERROR_FAILURE;
+ }
+ if (RefPtr<HTMLInputElement> inputElement =
+ HTMLInputElement::FromNode(inputOrTextArea)) {
+ if (inputElement->ReadOnly() == aBoolParam.value()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ ErrorResult error;
+ inputElement->SetReadOnly(aBoolParam.value(), error);
+ return error.StealNSResult();
+ }
+ if (RefPtr<HTMLTextAreaElement> textAreaElement =
+ HTMLTextAreaElement::FromNode(inputOrTextArea)) {
+ if (textAreaElement->ReadOnly() == aBoolParam.value()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ ErrorResult error;
+ textAreaElement->SetReadOnly(aBoolParam.value(), error);
+ return error.StealNSResult();
+ }
+ NS_ASSERTION(
+ false,
+ "Unexpected exposed root element, fallthrough to directly make the "
+ "editor readonly");
+ }
+ ErrorResult error;
+ if (aBoolParam.value()) {
+ nsresult rv = aEditorBase.AddFlags(nsIEditor::eEditorReadonlyMask);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::AddFlags(nsIEditor::eEditorReadonlyMask) failed");
+ return rv;
+ }
+ nsresult rv = aEditorBase.RemoveFlags(nsIEditor::eEditorReadonlyMask);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::RemoveFlags(nsIEditor::eEditorReadonlyMask) failed");
+ return rv;
+ }
+ case Command::SetDocumentUseCSS: {
+ nsresult rv = MOZ_KnownLive(aEditorBase.AsHTMLEditor())
+ ->SetIsCSSEnabled(aBoolParam.value());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetIsCSSEnabled() failed");
+ return rv;
+ }
+ case Command::SetDocumentInsertBROnEnterKeyPress: {
+ nsresult rv =
+ aEditorBase.AsHTMLEditor()->SetReturnInParagraphCreatesNewParagraph(
+ !aBoolParam.value());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::SetReturnInParagraphCreatesNewParagraph() failed");
+ return rv;
+ }
+ case Command::ToggleObjectResizers: {
+ MOZ_KnownLive(aEditorBase.AsHTMLEditor())
+ ->EnableObjectResizer(aBoolParam.value());
+ return NS_OK;
+ }
+ case Command::ToggleInlineTableEditor: {
+ MOZ_KnownLive(aEditorBase.AsHTMLEditor())
+ ->EnableInlineTableEditor(aBoolParam.value());
+ return NS_OK;
+ }
+ case Command::ToggleAbsolutePositionEditor: {
+ MOZ_KnownLive(aEditorBase.AsHTMLEditor())
+ ->EnableAbsolutePositionEditor(aBoolParam.value());
+ return NS_OK;
+ }
+ case Command::EnableCompatibleJoinSplitNodeDirection:
+ // Now we don't support the legacy join/split node direction anymore, but
+ // this result may be used for the feature detection whether Gecko
+ // supports the new direction mode. Therefore, even though we do nothing,
+ // but we should return NS_OK to return `true` from
+ // `Document.execCommand()`.
+ return NS_OK;
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+nsresult SetDocumentStateCommand::DoCommandParam(
+ Command aCommand, const nsACString& aCStringParam, EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ if (NS_WARN_IF(aCStringParam.IsVoid())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!aEditorBase.IsHTMLEditor())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ switch (aCommand) {
+ case Command::SetDocumentDefaultParagraphSeparator: {
+ if (aCStringParam.LowerCaseEqualsLiteral("div")) {
+ aEditorBase.AsHTMLEditor()->SetDefaultParagraphSeparator(
+ ParagraphSeparator::div);
+ return NS_OK;
+ }
+ if (aCStringParam.LowerCaseEqualsLiteral("p")) {
+ aEditorBase.AsHTMLEditor()->SetDefaultParagraphSeparator(
+ ParagraphSeparator::p);
+ return NS_OK;
+ }
+ if (aCStringParam.LowerCaseEqualsLiteral("br")) {
+ // Mozilla extension for backwards compatibility
+ aEditorBase.AsHTMLEditor()->SetDefaultParagraphSeparator(
+ ParagraphSeparator::br);
+ return NS_OK;
+ }
+
+ // This should not be reachable from nsHTMLDocument::ExecCommand
+ // XXX Shouldn't return error in this case because Chrome does not throw
+ // exception in this case.
+ NS_WARNING("Invalid default paragraph separator");
+ return NS_ERROR_UNEXPECTED;
+ }
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+nsresult SetDocumentStateCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ // If the result is set to STATE_ATTRIBUTE as CString value,
+ // queryCommandValue() returns the string value.
+ // Otherwise, ignored.
+
+ // The base editor owns most state info
+ if (NS_WARN_IF(!aEditorBase)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!aEditorBase->IsHTMLEditor())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Always get the enabled state
+ nsresult rv =
+ aParams.SetBool(STATE_ENABLED, IsCommandEnabled(aCommand, aEditorBase));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ switch (aCommand) {
+ case Command::SetDocumentModified: {
+ bool modified;
+ rv = aEditorBase->GetDocumentModified(&modified);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::GetDocumentModified() failed");
+ return rv;
+ }
+ // XXX Nobody refers this result due to wrong type.
+ rv = aParams.SetBool(STATE_ATTRIBUTE, modified);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsCommandParams::SetBool(STATE_ATTRIBUTE) failed");
+ return rv;
+ }
+ case Command::SetDocumentReadOnly: {
+ // XXX Nobody refers this result due to wrong type.
+ rv = aParams.SetBool(STATE_ATTRIBUTE, aEditorBase->IsReadonly());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsCommandParams::SetBool(STATE_ATTRIBUTE) failed");
+ return rv;
+ }
+ case Command::SetDocumentUseCSS: {
+ HTMLEditor* htmlEditor = aEditorBase->GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ rv = aParams.SetBool(STATE_ALL, htmlEditor->IsCSSEnabled());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsCommandParams::SetBool(STATE_ALL) failed");
+ return rv;
+ }
+ case Command::SetDocumentInsertBROnEnterKeyPress: {
+ HTMLEditor* htmlEditor = aEditorBase->GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ bool createPOnReturn;
+ DebugOnly<nsresult> rvIgnored =
+ htmlEditor->GetReturnInParagraphCreatesNewParagraph(&createPOnReturn);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::GetReturnInParagraphCreatesNewParagraph() failed");
+ // XXX Nobody refers this result due to wrong type.
+ rv = aParams.SetBool(STATE_ATTRIBUTE, !createPOnReturn);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsCommandParams::SetBool(STATE_ATTRIBUTE) failed");
+ return rv;
+ }
+ case Command::SetDocumentDefaultParagraphSeparator: {
+ HTMLEditor* htmlEditor = aEditorBase->GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ switch (htmlEditor->GetDefaultParagraphSeparator()) {
+ case ParagraphSeparator::div: {
+ DebugOnly<nsresult> rv =
+ aParams.SetCString(STATE_ATTRIBUTE, "div"_ns);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Failed to set command params to return \"div\"");
+ return NS_OK;
+ }
+ case ParagraphSeparator::p: {
+ DebugOnly<nsresult> rv = aParams.SetCString(STATE_ATTRIBUTE, "p"_ns);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to set command params to return \"p\"");
+ return NS_OK;
+ }
+ case ParagraphSeparator::br: {
+ DebugOnly<nsresult> rv = aParams.SetCString(STATE_ATTRIBUTE, "br"_ns);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to set command params to return \"br\"");
+ return NS_OK;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid paragraph separator value");
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+ case Command::ToggleObjectResizers: {
+ HTMLEditor* htmlEditor = aEditorBase->GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ // We returned the result as STATE_ATTRIBUTE with bool value 60 or
+ // earlier. So, the result was ignored by both
+ // nsHTMLDocument::QueryCommandValue() and
+ // nsHTMLDocument::QueryCommandState().
+ rv = aParams.SetBool(STATE_ALL, htmlEditor->IsObjectResizerEnabled());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsCommandParams::SetBool(STATE_ALL) failed");
+ return rv;
+ }
+ case Command::ToggleInlineTableEditor: {
+ HTMLEditor* htmlEditor = aEditorBase->GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ // We returned the result as STATE_ATTRIBUTE with bool value 60 or
+ // earlier. So, the result was ignored by both
+ // nsHTMLDocument::QueryCommandValue() and
+ // nsHTMLDocument::QueryCommandState().
+ rv = aParams.SetBool(STATE_ALL, htmlEditor->IsInlineTableEditorEnabled());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsCommandParams::SetBool(STATE_ALL) failed");
+ return rv;
+ }
+ case Command::ToggleAbsolutePositionEditor: {
+ HTMLEditor* htmlEditor = aEditorBase->GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ return aParams.SetBool(STATE_ALL,
+ htmlEditor->IsAbsolutePositionEditorEnabled());
+ }
+ case Command::EnableCompatibleJoinSplitNodeDirection: {
+ HTMLEditor* htmlEditor = aEditorBase->GetAsHTMLEditor();
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ // Now we don't support the legacy join/split node direction anymore, but
+ // this result may be used for the feature detection whether Gecko
+ // supports the new direction mode. Therefore, we should return `true`
+ // even though executing the command does nothing.
+ return aParams.SetBool(STATE_ALL, true);
+ }
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+/*****************************************************************************
+ * mozilla::DocumentStateCommand
+ *
+ * Commands just for state notification
+ * As of 11/21/02, possible commands are:
+ * "obs_documentCreated"
+ * "obs_documentWillBeDestroyed"
+ * "obs_documentLocationChanged"
+ * Note that you can use the same command class, DocumentStateCommand
+ * for these or future observer commands.
+ * We check the input command param for different behavior
+ *
+ * How to use:
+ * 1. Get the nsCommandManager for the current editor
+ * 2. Implement an nsIObserve object, e.g:
+ *
+ * void Observe(
+ * in nsISupports aSubject, // The nsCommandManager calling this
+ * // Observer
+ * in string aTopic, // command name, e.g.:"obs_documentCreated"
+ * // or "obs_documentWillBeDestroyed"
+ in wstring aData ); // ignored (set to "command_status_changed")
+ *
+ * 3. Add the observer by:
+ * commandManager.addObserver(observeobject, obs_documentCreated);
+ * 4. In the appropriate location in editorSession, editor, or commands code,
+ * trigger the notification of this observer by something like:
+ *
+ * RefPtr<nsCommandManager> commandManager = mDocShell->GetCommandManager();
+ * commandManager->CommandStatusChanged(obs_documentCreated);
+ *
+ * 5. Use GetCommandStateParams() to obtain state information
+ * e.g., any creation state codes when creating an editor are
+ * supplied for "obs_documentCreated" command in the
+ * "state_data" param's value
+ *****************************************************************************/
+
+StaticRefPtr<DocumentStateCommand> DocumentStateCommand::sInstance;
+
+bool DocumentStateCommand::IsCommandEnabled(Command aCommand,
+ EditorBase* aEditorBase) const {
+ // Always return false to discourage callers from using DoCommand()
+ return false;
+}
+
+nsresult DocumentStateCommand::DoCommand(Command aCommand,
+ EditorBase& aEditorBase,
+ nsIPrincipal* aPrincipal) const {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult DocumentStateCommand::GetCommandStateParams(
+ Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
+ nsIEditingSession* aEditingSession) const {
+ switch (aCommand) {
+ case Command::EditorObserverDocumentCreated: {
+ uint32_t editorStatus = nsIEditingSession::eEditorErrorUnknown;
+ if (aEditingSession) {
+ // Current context is initially set to nsIEditingSession until editor is
+ // successfully created and source doc is loaded. Embedder gets error
+ // status if this fails. If called before startup is finished,
+ // status will be eEditorCreationInProgress.
+ nsresult rv = aEditingSession->GetEditorStatus(&editorStatus);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIEditingSession::GetEditorStatus() failed");
+ return rv;
+ }
+ } else if (aEditorBase) {
+ // If current context is an editor, then everything started up OK!
+ editorStatus = nsIEditingSession::eEditorOK;
+ }
+
+ // Note that if refCon is not-null, but is neither
+ // an nsIEditingSession or nsIEditor, we return "eEditorErrorUnknown"
+ DebugOnly<nsresult> rvIgnored = aParams.SetInt(STATE_DATA, editorStatus);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to set editor status");
+ return NS_OK;
+ }
+ case Command::EditorObserverDocumentLocationChanged: {
+ if (!aEditorBase) {
+ return NS_OK;
+ }
+ Document* document = aEditorBase->GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsIURI* uri = document->GetDocumentURI();
+ if (NS_WARN_IF(!uri)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = aParams.SetISupports(STATE_DATA, uri);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsCOmmandParms::SetISupports(STATE_DATA) failed");
+ return rv;
+ }
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorEventListener.cpp b/editor/libeditor/HTMLEditorEventListener.cpp
new file mode 100644
index 0000000000..dda99bf163
--- /dev/null
+++ b/editor/libeditor/HTMLEditorEventListener.cpp
@@ -0,0 +1,437 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditorEventListener.h"
+
+#include "HTMLEditUtils.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventTarget.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/Selection.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsISupportsImpl.h"
+#include "nsLiteralString.h"
+#include "nsQueryObject.h"
+#include "nsRange.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+nsresult HTMLEditorEventListener::Connect(EditorBase* aEditorBase) {
+ // Guarantee that mEditorBase is always HTMLEditor.
+ HTMLEditor* htmlEditor = HTMLEditor::GetFrom(aEditorBase);
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv = EditorEventListener::Connect(htmlEditor);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::Connect() failed");
+ return rv;
+}
+
+void HTMLEditorEventListener::Disconnect() {
+ if (DetachedFromEditor()) {
+ EditorEventListener::Disconnect();
+ }
+
+ if (mListeningToMouseMoveEventForResizers) {
+ DebugOnly<nsresult> rvIgnored = ListenToMouseMoveEventForResizers(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditorEventListener::ListenToMouseMoveEventForResizers() failed, "
+ "but ignored");
+ }
+ if (mListeningToMouseMoveEventForGrabber) {
+ DebugOnly<nsresult> rvIgnored = ListenToMouseMoveEventForGrabber(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditorEventListener::ListenToMouseMoveEventForGrabber() failed, "
+ "but ignored");
+ }
+ if (mListeningToResizeEvent) {
+ DebugOnly<nsresult> rvIgnored = ListenToWindowResizeEvent(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditorEventListener::ListenToWindowResizeEvent() failed, "
+ "but ignored");
+ }
+
+ EditorEventListener::Disconnect();
+}
+
+NS_IMETHODIMP HTMLEditorEventListener::HandleEvent(Event* aEvent) {
+ switch (aEvent->WidgetEventPtr()->mMessage) {
+ case eMouseMove: {
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ RefPtr<MouseEvent> mouseMoveEvent = aEvent->AsMouseEvent();
+ if (NS_WARN_IF(!aEvent->WidgetEventPtr())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<HTMLEditor> htmlEditor = mEditorBase->AsHTMLEditor();
+ DebugOnly<nsresult> rvIgnored =
+ htmlEditor->UpdateResizerOrGrabberPositionTo(CSSIntPoint(
+ mouseMoveEvent->ClientX(), mouseMoveEvent->ClientY()));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::UpdateResizerOrGrabberPositionTo() failed, but ignored");
+ return NS_OK;
+ }
+ case eResize: {
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ RefPtr<HTMLEditor> htmlEditor = mEditorBase->AsHTMLEditor();
+ nsresult rv = htmlEditor->RefreshResizers();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RefreshResizers() failed");
+ return rv;
+ }
+ default: {
+ nsresult rv = EditorEventListener::HandleEvent(aEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::HandleEvent() failed");
+ return rv;
+ }
+ }
+}
+
+nsresult HTMLEditorEventListener::ListenToMouseMoveEventForResizersOrGrabber(
+ bool aListen, bool aForGrabber) {
+ MOZ_ASSERT(aForGrabber ? mListeningToMouseMoveEventForGrabber != aListen
+ : mListeningToMouseMoveEventForResizers != aListen);
+
+ if (NS_WARN_IF(DetachedFromEditor())) {
+ return aListen ? NS_ERROR_FAILURE : NS_OK;
+ }
+
+ if (aListen) {
+ if (aForGrabber && mListeningToMouseMoveEventForResizers) {
+ // We've already added mousemove event listener for resizers.
+ mListeningToMouseMoveEventForGrabber = true;
+ return NS_OK;
+ }
+ if (!aForGrabber && mListeningToMouseMoveEventForGrabber) {
+ // We've already added mousemove event listener for grabber.
+ mListeningToMouseMoveEventForResizers = true;
+ return NS_OK;
+ }
+ } else {
+ if (aForGrabber && mListeningToMouseMoveEventForResizers) {
+ // We need to keep listening to mousemove event listener for resizers.
+ mListeningToMouseMoveEventForGrabber = false;
+ return NS_OK;
+ }
+ if (!aForGrabber && mListeningToMouseMoveEventForGrabber) {
+ // We need to keep listening to mousemove event listener for grabber.
+ mListeningToMouseMoveEventForResizers = false;
+ return NS_OK;
+ }
+ }
+
+ EventTarget* eventTarget = mEditorBase->AsHTMLEditor()->GetDOMEventTarget();
+ if (NS_WARN_IF(!eventTarget)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Listen to mousemove events in the system group since web apps may stop
+ // propagation before we receive the events.
+ EventListenerManager* eventListenerManager =
+ eventTarget->GetOrCreateListenerManager();
+ if (NS_WARN_IF(!eventListenerManager)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aListen) {
+ eventListenerManager->AddEventListenerByType(
+ this, u"mousemove"_ns, TrustedEventsAtSystemGroupBubble());
+ if (aForGrabber) {
+ mListeningToMouseMoveEventForGrabber = true;
+ } else {
+ mListeningToMouseMoveEventForResizers = true;
+ }
+ return NS_OK;
+ }
+
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"mousemove"_ns, TrustedEventsAtSystemGroupBubble());
+ if (aForGrabber) {
+ mListeningToMouseMoveEventForGrabber = false;
+ } else {
+ mListeningToMouseMoveEventForResizers = false;
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditorEventListener::ListenToWindowResizeEvent(bool aListen) {
+ if (mListeningToResizeEvent == aListen) {
+ return NS_OK;
+ }
+
+ if (DetachedFromEditor()) {
+ return aListen ? NS_ERROR_FAILURE : NS_OK;
+ }
+
+ Document* document = mEditorBase->AsHTMLEditor()->GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Document::GetWindow() may return nullptr when HTMLEditor is destroyed
+ // while the document is being unloaded. If we cannot retrieve window as
+ // expected, let's ignore it.
+ nsPIDOMWindowOuter* window = document->GetWindow();
+ if (!window) {
+ NS_WARNING_ASSERTION(!aListen,
+ "There should be window when adding event listener");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<EventTarget> eventTarget = do_QueryInterface(window);
+ if (NS_WARN_IF(!eventTarget)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Listen to resize events in the system group since web apps may stop
+ // propagation before we receive the events.
+ EventListenerManager* eventListenerManager =
+ eventTarget->GetOrCreateListenerManager();
+ if (NS_WARN_IF(!eventListenerManager)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aListen) {
+ eventListenerManager->AddEventListenerByType(
+ this, u"resize"_ns, TrustedEventsAtSystemGroupBubble());
+ mListeningToResizeEvent = true;
+ return NS_OK;
+ }
+
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"resize"_ns, TrustedEventsAtSystemGroupBubble());
+ mListeningToResizeEvent = false;
+ return NS_OK;
+}
+
+nsresult HTMLEditorEventListener::MouseUp(MouseEvent* aMouseEvent) {
+ MOZ_ASSERT(aMouseEvent);
+ MOZ_ASSERT(aMouseEvent->IsTrusted());
+
+ if (DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ // FYI: We need to notify HTML editor of mouseup even if it's consumed
+ // because HTML editor always needs to release grabbing resizer.
+ RefPtr<HTMLEditor> htmlEditor = mEditorBase->AsHTMLEditor();
+ htmlEditor->PreHandleMouseUp(*aMouseEvent);
+
+ if (NS_WARN_IF(!aMouseEvent->GetTarget())) {
+ return NS_ERROR_FAILURE;
+ }
+ // FYI: The event target must be an element node for conforming to the
+ // UI Events, but it may not be not an element node if it occurs
+ // on native anonymous node like a resizer.
+
+ DebugOnly<nsresult> rvIgnored = htmlEditor->StopDraggingResizerOrGrabberAt(
+ CSSIntPoint(aMouseEvent->ClientX(), aMouseEvent->ClientY()));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::StopDraggingResizerOrGrabberAt() failed, but ignored");
+
+ nsresult rv = EditorEventListener::MouseUp(aMouseEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::MouseUp() failed");
+ return rv;
+}
+
+static bool IsAcceptableMouseEvent(const HTMLEditor& aHTMLEditor,
+ MouseEvent* aMouseEvent) {
+ // Contenteditable should disregard mousedowns outside it.
+ // IsAcceptableInputEvent() checks it for a mouse event.
+ WidgetMouseEvent* mousedownEvent =
+ aMouseEvent->WidgetEventPtr()->AsMouseEvent();
+ MOZ_ASSERT(mousedownEvent);
+ return aHTMLEditor.IsAcceptableInputEvent(mousedownEvent);
+}
+
+nsresult HTMLEditorEventListener::HandlePrimaryMouseButtonDown(
+ HTMLEditor& aHTMLEditor, MouseEvent& aMouseEvent) {
+ RefPtr<EventTarget> eventTarget = aMouseEvent.GetExplicitOriginalTarget();
+ if (NS_WARN_IF(!eventTarget)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsIContent* eventTargetContent = nsIContent::FromEventTarget(eventTarget);
+ if (!eventTargetContent) {
+ return NS_OK;
+ }
+
+ RefPtr<Element> toSelect;
+ bool isElement = eventTargetContent->IsElement();
+ int32_t clickCount = aMouseEvent.Detail();
+ switch (clickCount) {
+ case 1:
+ if (isElement) {
+ OwningNonNull<Element> element(*eventTargetContent->AsElement());
+ DebugOnly<nsresult> rvIgnored =
+ aHTMLEditor.StartToDragResizerOrHandleDragGestureOnGrabber(
+ aMouseEvent, element);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::StartToDragResizerOrHandleDragGestureOnGrabber() "
+ "failed, but ignored");
+ }
+ break;
+ case 2:
+ if (isElement) {
+ toSelect = eventTargetContent->AsElement();
+ }
+ break;
+ case 3:
+ // Triple click selects `<a href>` instead of its container paragraph
+ // as users may want to modify the anchor element.
+ if (!isElement) {
+ toSelect = aHTMLEditor.GetInclusiveAncestorByTagName(
+ *nsGkAtoms::href, *eventTargetContent);
+ }
+ break;
+ }
+ if (toSelect) {
+ DebugOnly<nsresult> rvIgnored = aHTMLEditor.SelectElement(toSelect);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SelectElement() failed, but ignored");
+ aMouseEvent.PreventDefault();
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditorEventListener::HandleSecondaryMouseButtonDown(
+ HTMLEditor& aHTMLEditor, MouseEvent& aMouseEvent) {
+ RefPtr<Selection> selection = aHTMLEditor.GetSelection();
+ if (NS_WARN_IF(!selection)) {
+ return NS_OK;
+ }
+
+ int32_t offset = -1;
+ nsCOMPtr<nsIContent> parentContent =
+ aMouseEvent.GetRangeParentContentAndOffset(&offset);
+ if (NS_WARN_IF(!parentContent) || NS_WARN_IF(offset < 0)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (nsContentUtils::IsPointInSelection(*selection, *parentContent,
+ AssertedCast<uint32_t>(offset))) {
+ return NS_OK;
+ }
+
+ RefPtr<EventTarget> eventTarget = aMouseEvent.GetExplicitOriginalTarget();
+ if (NS_WARN_IF(!eventTarget)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ Element* eventTargetElement = Element::FromEventTarget(eventTarget);
+
+ // Select entire element clicked on if NOT within an existing selection
+ // and not the entire body, or table-related elements
+ if (HTMLEditUtils::IsImage(eventTargetElement)) {
+ // MOZ_KnownLive(eventTargetElement): Guaranteed by eventTarget.
+ DebugOnly<nsresult> rvIgnored =
+ aHTMLEditor.SelectElement(MOZ_KnownLive(eventTargetElement));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SelectElement() failed, but ignored");
+ }
+
+ // HACK !!! Context click places the caret but the context menu consumes
+ // the event; so we need to check resizing state ourselves
+ DebugOnly<nsresult> rvIgnored =
+ aHTMLEditor.CheckSelectionStateForAnonymousButtons();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::CheckSelectionStateForAnonymousButtons() "
+ "failed, but ignored");
+
+ return NS_OK;
+}
+
+nsresult HTMLEditorEventListener::MouseDown(MouseEvent* aMouseEvent) {
+ MOZ_ASSERT(aMouseEvent);
+ MOZ_ASSERT(aMouseEvent->IsTrusted());
+
+ if (NS_WARN_IF(!aMouseEvent) || DetachedFromEditor()) {
+ return NS_OK;
+ }
+
+ // Even if it's not acceptable mousedown event (i.e., when mousedown
+ // event is fired outside of the active editing host), we need to commit
+ // composition because it will be change the selection to the clicked
+ // point. Then, we won't be able to commit the composition.
+ if (!EnsureCommitComposition()) {
+ return NS_OK;
+ }
+
+ RefPtr<HTMLEditor> htmlEditor = mEditorBase->AsHTMLEditor();
+ htmlEditor->PreHandleMouseDown(*aMouseEvent);
+
+ if (!IsAcceptableMouseEvent(*htmlEditor, aMouseEvent)) {
+ return EditorEventListener::MouseDown(aMouseEvent);
+ }
+
+ if (aMouseEvent->Button() == MouseButton::ePrimary) {
+ nsresult rv = HandlePrimaryMouseButtonDown(*htmlEditor, *aMouseEvent);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else if (aMouseEvent->Button() == MouseButton::eSecondary) {
+ nsresult rv = HandleSecondaryMouseButtonDown(*htmlEditor, *aMouseEvent);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ nsresult rv = EditorEventListener::MouseDown(aMouseEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::MouseDown() failed");
+ return rv;
+}
+
+nsresult HTMLEditorEventListener::MouseClick(
+ WidgetMouseEvent* aMouseClickEvent) {
+ if (NS_WARN_IF(DetachedFromEditor())) {
+ return NS_OK;
+ }
+
+ RefPtr<Element> element = Element::FromEventTargetOrNull(
+ aMouseClickEvent->GetOriginalDOMEventTarget());
+ if (NS_WARN_IF(!element)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<HTMLEditor> htmlEditor = mEditorBase->AsHTMLEditor();
+ DebugOnly<nsresult> rvIgnored =
+ htmlEditor->DoInlineTableEditingAction(*element);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::DoInlineTableEditingAction() failed, but ignored");
+ // DoInlineTableEditingAction might cause reframe
+ // Editor is destroyed.
+ if (NS_WARN_IF(htmlEditor->Destroyed())) {
+ return NS_OK;
+ }
+
+ nsresult rv = EditorEventListener::MouseClick(aMouseClickEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorEventListener::MouseClick() failed");
+ return rv;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorEventListener.h b/editor/libeditor/HTMLEditorEventListener.h
new file mode 100644
index 0000000000..21eb115010
--- /dev/null
+++ b/editor/libeditor/HTMLEditorEventListener.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLEditorEventListener_h
+#define HTMLEditorEventListener_h
+
+#include "EditorEventListener.h"
+
+#include "EditorForwards.h"
+#include "HTMLEditor.h"
+
+#include "nscore.h"
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+}
+
+class HTMLEditorEventListener final : public EditorEventListener {
+ public:
+ HTMLEditorEventListener()
+ : mListeningToMouseMoveEventForResizers(false),
+ mListeningToMouseMoveEventForGrabber(false),
+ mListeningToResizeEvent(false) {}
+
+ // nsIDOMEventListener
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(dom::Event* aEvent) override;
+
+ /**
+ * Connect() fails if aEditorBase isn't an HTMLEditor instance.
+ */
+ virtual nsresult Connect(EditorBase* aEditorBase) override;
+
+ virtual void Disconnect() override;
+
+ /**
+ * ListenToMouseMoveEventForResizers() starts to listen to or stop
+ * listening to "mousemove" events for resizers.
+ */
+ nsresult ListenToMouseMoveEventForResizers(bool aListen) {
+ if (aListen == mListeningToMouseMoveEventForResizers) {
+ return NS_OK;
+ }
+ nsresult rv = ListenToMouseMoveEventForResizersOrGrabber(aListen, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditorEventListener::"
+ "ListenToMouseMoveEventForResizersOrGrabber() failed");
+ return rv;
+ }
+
+ /**
+ * ListenToMouseMoveEventForResizers() starts to listen to or stop
+ * listening to "mousemove" events for grabber to move absolutely
+ * positioned element.
+ */
+ nsresult ListenToMouseMoveEventForGrabber(bool aListen) {
+ if (aListen == mListeningToMouseMoveEventForGrabber) {
+ return NS_OK;
+ }
+ nsresult rv = ListenToMouseMoveEventForResizersOrGrabber(aListen, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditorEventListener::"
+ "ListenToMouseMoveEventForResizersOrGrabber() failed");
+ return rv;
+ }
+
+ /**
+ * ListenToWindowResizeEvent() starts to listen to or stop listening to
+ * "resize" events of the document.
+ */
+ nsresult ListenToWindowResizeEvent(bool aListen);
+
+ protected:
+ MOZ_CAN_RUN_SCRIPT virtual nsresult MouseDown(
+ dom::MouseEvent* aMouseEvent) override;
+ MOZ_CAN_RUN_SCRIPT virtual nsresult MouseUp(
+ dom::MouseEvent* aMouseEvent) override;
+ MOZ_CAN_RUN_SCRIPT virtual nsresult MouseClick(
+ WidgetMouseEvent* aMouseClickEvent) override;
+
+ nsresult ListenToMouseMoveEventForResizersOrGrabber(bool aListen,
+ bool aForGrabber);
+
+ MOZ_CAN_RUN_SCRIPT nsresult HandlePrimaryMouseButtonDown(
+ HTMLEditor& aHTMLEditor, dom::MouseEvent& aMouseEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult HandleSecondaryMouseButtonDown(
+ HTMLEditor& aHTMLEditor, dom::MouseEvent& aMouseEvent);
+
+ bool mListeningToMouseMoveEventForResizers;
+ bool mListeningToMouseMoveEventForGrabber;
+ bool mListeningToResizeEvent;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef HTMLEditorEventListener_h
diff --git a/editor/libeditor/HTMLEditorInlines.h b/editor/libeditor/HTMLEditorInlines.h
new file mode 100644
index 0000000000..653b408d75
--- /dev/null
+++ b/editor/libeditor/HTMLEditorInlines.h
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLEditorInlines_h
+#define HTMLEditorInlines_h
+
+#include "HTMLEditor.h"
+
+#include "EditorDOMPoint.h"
+#include "HTMLEditHelpers.h"
+#include "SelectionState.h" // for RangeItem
+
+#include "ErrorList.h" // for nsresult
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Debug.h"
+#include "mozilla/Likely.h"
+#include "mozilla/RefPtr.h"
+
+#include "mozilla/dom/Element.h"
+
+#include "nsAtom.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsRange.h"
+#include "nsString.h"
+
+#include <ostream>
+
+namespace mozilla {
+
+using namespace dom;
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction(
+ Element& aOldContainer, const nsAtom& aTagName) {
+ return ReplaceContainerWithTransactionInternal(
+ aOldContainer, aTagName, *nsGkAtoms::_empty, u""_ns, true);
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::ReplaceContainerWithTransaction(Element& aOldContainer,
+ const nsAtom& aTagName,
+ const nsAtom& aAttribute,
+ const nsAString& aAttributeValue) {
+ return ReplaceContainerWithTransactionInternal(
+ aOldContainer, aTagName, aAttribute, aAttributeValue, false);
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::ReplaceContainerWithTransaction(Element& aOldContainer,
+ const nsAtom& aTagName) {
+ return ReplaceContainerWithTransactionInternal(
+ aOldContainer, aTagName, *nsGkAtoms::_empty, u""_ns, false);
+}
+
+Result<MoveNodeResult, nsresult> HTMLEditor::MoveNodeToEndWithTransaction(
+ nsIContent& aContentToMove, nsINode& aNewContainer) {
+ return MoveNodeWithTransaction(aContentToMove,
+ EditorDOMPoint::AtEndOf(aNewContainer));
+}
+
+Element* HTMLEditor::GetTableCellElementAt(
+ Element& aTableElement, const CellIndexes& aCellIndexes) const {
+ return GetTableCellElementAt(aTableElement, aCellIndexes.mRow,
+ aCellIndexes.mColumn);
+}
+
+already_AddRefed<RangeItem>
+HTMLEditor::GetSelectedRangeItemForTopLevelEditSubAction() const {
+ if (!mSelectedRangeForTopLevelEditSubAction) {
+ mSelectedRangeForTopLevelEditSubAction = new RangeItem();
+ }
+ return do_AddRef(mSelectedRangeForTopLevelEditSubAction);
+}
+
+already_AddRefed<nsRange> HTMLEditor::GetChangedRangeForTopLevelEditSubAction()
+ const {
+ if (!mChangedRangeForTopLevelEditSubAction) {
+ mChangedRangeForTopLevelEditSubAction = nsRange::Create(GetDocument());
+ }
+ return do_AddRef(mChangedRangeForTopLevelEditSubAction);
+}
+
+// static
+template <typename EditorDOMPointType>
+HTMLEditor::CharPointType HTMLEditor::GetPreviousCharPointType(
+ const EditorDOMPointType& aPoint) {
+ MOZ_ASSERT(aPoint.IsInTextNode());
+ if (aPoint.IsStartOfContainer()) {
+ return CharPointType::TextEnd;
+ }
+ if (aPoint.IsPreviousCharPreformattedNewLine()) {
+ return CharPointType::PreformattedLineBreak;
+ }
+ if (EditorUtils::IsWhiteSpacePreformatted(
+ *aPoint.template ContainerAs<Text>())) {
+ return CharPointType::PreformattedChar;
+ }
+ if (aPoint.IsPreviousCharASCIISpace()) {
+ return CharPointType::ASCIIWhiteSpace;
+ }
+ return aPoint.IsPreviousCharNBSP() ? CharPointType::NoBreakingSpace
+ : CharPointType::VisibleChar;
+}
+
+// static
+template <typename EditorDOMPointType>
+HTMLEditor::CharPointType HTMLEditor::GetCharPointType(
+ const EditorDOMPointType& aPoint) {
+ MOZ_ASSERT(aPoint.IsInTextNode());
+ if (aPoint.IsEndOfContainer()) {
+ return CharPointType::TextEnd;
+ }
+ if (aPoint.IsCharPreformattedNewLine()) {
+ return CharPointType::PreformattedLineBreak;
+ }
+ if (EditorUtils::IsWhiteSpacePreformatted(
+ *aPoint.template ContainerAs<Text>())) {
+ return CharPointType::PreformattedChar;
+ }
+ if (aPoint.IsCharASCIISpace()) {
+ return CharPointType::ASCIIWhiteSpace;
+ }
+ return aPoint.IsCharNBSP() ? CharPointType::NoBreakingSpace
+ : CharPointType::VisibleChar;
+}
+
+/******************************************************************************
+ * Logging utils
+ ******************************************************************************/
+
+inline std::ostream& operator<<(
+ std::ostream& aStream,
+ const HTMLEditor::PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle) {
+ aStream << "PreserveWhiteSpaceStyle::"
+ << (aPreserveWhiteSpaceStyle ==
+ HTMLEditor::PreserveWhiteSpaceStyle::No
+ ? "No"
+ : "Yes");
+ return aStream;
+}
+
+} // namespace mozilla
+
+#endif // HTMLEditorInlines_h
diff --git a/editor/libeditor/HTMLEditorNestedClasses.h b/editor/libeditor/HTMLEditorNestedClasses.h
new file mode 100644
index 0000000000..1d5ac2113f
--- /dev/null
+++ b/editor/libeditor/HTMLEditorNestedClasses.h
@@ -0,0 +1,522 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLEditorNestedClasses_h
+#define HTMLEditorNestedClasses_h
+
+#include "EditorDOMPoint.h"
+#include "EditorForwards.h"
+#include "HTMLEditor.h" // for HTMLEditor
+#include "HTMLEditHelpers.h" // for EditorInlineStyleAndValue
+
+#include "mozilla/Attributes.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/Result.h"
+
+namespace mozilla {
+
+/*****************************************************************************
+ * AutoInlineStyleSetter is a temporary class to set an inline style to
+ * specific nodes.
+ ****************************************************************************/
+
+class MOZ_STACK_CLASS HTMLEditor::AutoInlineStyleSetter final
+ : private EditorInlineStyleAndValue {
+ using Element = dom::Element;
+ using Text = dom::Text;
+
+ public:
+ explicit AutoInlineStyleSetter(
+ const EditorInlineStyleAndValue& aStyleAndValue)
+ : EditorInlineStyleAndValue(aStyleAndValue) {}
+
+ void Reset() {
+ mFirstHandledPoint.Clear();
+ mLastHandledPoint.Clear();
+ }
+
+ const EditorDOMPoint& FirstHandledPointRef() const {
+ return mFirstHandledPoint;
+ }
+ const EditorDOMPoint& LastHandledPointRef() const {
+ return mLastHandledPoint;
+ }
+
+ /**
+ * Split aText at aStartOffset and aEndOffset (except when they are start or
+ * end of its data) and wrap the middle text node in an element to apply the
+ * style.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
+ SplitTextNodeAndApplyStyleToMiddleNode(HTMLEditor& aHTMLEditor, Text& aText,
+ uint32_t aStartOffset,
+ uint32_t aEndOffset);
+
+ /**
+ * Remove same style from children and apply the style entire (except
+ * non-editable nodes) aContent.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(HTMLEditor& aHTMLEditor,
+ nsIContent& aContent);
+
+ /**
+ * Invert the style with creating new element or something. This should
+ * be called only when IsInvertibleWithCSS() returns true.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Element& aElement);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
+ InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Text& aTextNode,
+ uint32_t aStartOffset, uint32_t aEndOffset);
+
+ /**
+ * Extend or shrink aRange for applying the style to the range.
+ * See comments in the definition what this does.
+ */
+ Result<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToApplyTheStyle(
+ const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
+ const Element& aEditingHost) const;
+
+ /**
+ * Returns next/previous sibling of aContent or an ancestor of it if it's
+ * editable and does not cross block boundary.
+ */
+ [[nodiscard]] static nsIContent* GetNextEditableInlineContent(
+ const nsIContent& aContent, const nsINode* aLimiter = nullptr);
+ [[nodiscard]] static nsIContent* GetPreviousEditableInlineContent(
+ const nsIContent& aContent, const nsINode* aLimiter = nullptr);
+
+ /**
+ * GetEmptyTextNodeToApplyNewStyle creates new empty text node to insert
+ * a new element which will contain newly inserted text or returns existing
+ * empty text node if aCandidatePointToInsert is around it.
+ *
+ * NOTE: Unfortunately, editor does not want to insert text into empty inline
+ * element in some places (e.g., automatically adjusting caret position to
+ * nearest text node). Therefore, we need to create new empty text node to
+ * prepare new styles for inserting text. This method is designed for the
+ * preparation.
+ *
+ * @param aHTMLEditor The editor.
+ * @param aCandidatePointToInsert The point where the caller wants to
+ * insert new text.
+ * @param aEditingHost The editing host.
+ * @return If this creates new empty text node returns it.
+ * If this couldn't create new empty text node due to
+ * the point or aEditingHost cannot have text node,
+ * returns nullptr.
+ * Otherwise, returns error.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<RefPtr<Text>, nsresult>
+ GetEmptyTextNodeToApplyNewStyle(HTMLEditor& aHTMLEditor,
+ const EditorDOMPoint& aCandidatePointToInsert,
+ const Element& aEditingHost);
+
+ private:
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> ApplyStyle(
+ HTMLEditor& aHTMLEditor, nsIContent& aContent);
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ ApplyCSSTextDecoration(HTMLEditor& aHTMLEditor, nsIContent& aContent);
+
+ /**
+ * Returns true if aStyledElement is a good element to set `style` attribute.
+ */
+ [[nodiscard]] bool ElementIsGoodContainerToSetStyle(
+ nsStyledElement& aStyledElement) const;
+
+ /**
+ * ElementIsGoodContainerForTheStyle() returns true if aElement is a
+ * good container for applying the style to a node. I.e., if this returns
+ * true, moving nodes into aElement is enough to apply the style to them.
+ * Otherwise, you need to create new element for the style.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<bool, nsresult>
+ ElementIsGoodContainerForTheStyle(HTMLEditor& aHTMLEditor,
+ Element& aElement) const;
+
+ /**
+ * Return true if the node is an element node and it represents the style or
+ * sets the style (including when setting different value) with `style`
+ * attribute.
+ */
+ [[nodiscard]] bool ContentIsElementSettingTheStyle(
+ const HTMLEditor& aHTMLEditor, nsIContent& aContent) const;
+
+ /**
+ * Helper methods to shrink range to apply the style.
+ */
+ [[nodiscard]] EditorRawDOMPoint GetShrunkenRangeStart(
+ const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
+ const nsINode& aCommonAncestorOfRange,
+ const nsIContent* aFirstEntirelySelectedContentNodeInRange) const;
+ [[nodiscard]] EditorRawDOMPoint GetShrunkenRangeEnd(
+ const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
+ const nsINode& aCommonAncestorOfRange,
+ const nsIContent* aLastEntirelySelectedContentNodeInRange) const;
+
+ /**
+ * Helper methods to extend the range to apply the style.
+ */
+ [[nodiscard]] EditorRawDOMPoint
+ GetExtendedRangeStartToWrapAncestorApplyingSameStyle(
+ const HTMLEditor& aHTMLEditor,
+ const EditorRawDOMPoint& aStartPoint) const;
+ [[nodiscard]] EditorRawDOMPoint
+ GetExtendedRangeEndToWrapAncestorApplyingSameStyle(
+ const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aEndPoint) const;
+ [[nodiscard]] EditorRawDOMRange
+ GetExtendedRangeToMinimizeTheNumberOfNewElements(
+ const HTMLEditor& aHTMLEditor, const nsINode& aCommonAncestor,
+ EditorRawDOMPoint&& aStartPoint, EditorRawDOMPoint&& aEndPoint) const;
+
+ /**
+ * OnHandled() are called when this class creates new element to apply the
+ * style, applies new style to existing element or ignores to apply the style
+ * due to already set.
+ */
+ void OnHandled(const EditorDOMPoint& aStartPoint,
+ const EditorDOMPoint& aEndPoint) {
+ if (!mFirstHandledPoint.IsSet()) {
+ mFirstHandledPoint = aStartPoint;
+ }
+ mLastHandledPoint = aEndPoint;
+ }
+ void OnHandled(nsIContent& aContent) {
+ if (!mFirstHandledPoint.IsSet()) {
+ mFirstHandledPoint.Set(&aContent, 0u);
+ }
+ mLastHandledPoint = EditorDOMPoint::AtEndOf(aContent);
+ }
+
+ // mFirstHandledPoint and mLastHandledPoint store the first and last points
+ // which are newly created or apply the new style, or just ignored at trying
+ // to split a text node.
+ EditorDOMPoint mFirstHandledPoint;
+ EditorDOMPoint mLastHandledPoint;
+};
+
+/**
+ * AutoMoveOneLineHandler moves the content in a line (between line breaks/block
+ * boundaries) to specific point or end of a container element.
+ */
+class MOZ_STACK_CLASS HTMLEditor::AutoMoveOneLineHandler final {
+ public:
+ /**
+ * Use this constructor when you want a line to move specific point.
+ */
+ explicit AutoMoveOneLineHandler(const EditorDOMPoint& aPointToInsert)
+ : mPointToInsert(aPointToInsert),
+ mMoveToEndOfContainer(MoveToEndOfContainer::No) {
+ MOZ_ASSERT(mPointToInsert.IsSetAndValid());
+ MOZ_ASSERT(mPointToInsert.IsInContentNode());
+ }
+ /**
+ * Use this constructor when you want a line to move end of
+ * aNewContainerElement.
+ */
+ explicit AutoMoveOneLineHandler(Element& aNewContainerElement)
+ : mPointToInsert(&aNewContainerElement, 0),
+ mMoveToEndOfContainer(MoveToEndOfContainer::Yes) {
+ MOZ_ASSERT(mPointToInsert.IsSetAndValid());
+ }
+
+ /**
+ * Must be called before calling Run().
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aPointInHardLine A point in a line which you want to move.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] nsresult Prepare(HTMLEditor& aHTMLEditor,
+ const EditorDOMPoint& aPointInHardLine,
+ const Element& aEditingHost);
+ /**
+ * Must be called if Prepare() returned NS_OK.
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<MoveNodeResult, nsresult> Run(
+ HTMLEditor& aHTMLEditor, const Element& aEditingHost);
+
+ /**
+ * Returns true if there are some content nodes which can be moved to another
+ * place or deleted in the line containing aPointInHardLine. Note that if
+ * there is only a padding <br> element in an empty block element, this
+ * returns false even though it may be deleted.
+ */
+ static Result<bool, nsresult> CanMoveOrDeleteSomethingInLine(
+ const EditorDOMPoint& aPointInHardLine, const Element& aEditingHost);
+
+ AutoMoveOneLineHandler(const AutoMoveOneLineHandler& aOther) = delete;
+ AutoMoveOneLineHandler(AutoMoveOneLineHandler&& aOther) = delete;
+
+ private:
+ [[nodiscard]] bool ForceMoveToEndOfContainer() const {
+ return mMoveToEndOfContainer == MoveToEndOfContainer::Yes;
+ }
+ [[nodiscard]] EditorDOMPoint& NextInsertionPointRef() {
+ if (ForceMoveToEndOfContainer()) {
+ mPointToInsert.SetToEndOf(mPointToInsert.GetContainer());
+ }
+ return mPointToInsert;
+ }
+
+ /**
+ * Consider whether Run() should preserve or does not preserve white-space
+ * style of moving content.
+ *
+ * @param aContentInLine Specify a content node in the moving line.
+ * Typically, container of aPointInHardLine of
+ * Prepare().
+ * @param aInclusiveAncestorBlockOfInsertionPoint
+ * Inclusive ancestor block element of insertion
+ * point. Typically, computed
+ * mDestInclusiveAncestorBlock.
+ */
+ [[nodiscard]] static PreserveWhiteSpaceStyle
+ ConsiderWhetherPreserveWhiteSpaceStyle(
+ const nsIContent* aContentInLine,
+ const Element* aInclusiveAncestorBlockOfInsertionPoint);
+
+ /**
+ * Look for inclusive ancestor block element of aBlockElement and a descendant
+ * of aAncestorElement. If aBlockElement and aAncestorElement are same one,
+ * this returns nullptr.
+ *
+ * @param aBlockElement A block element which is a descendant of
+ * aAncestorElement.
+ * @param aAncestorElement An inclusive ancestor block element of
+ * aBlockElement.
+ */
+ [[nodiscard]] static Element*
+ GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement(
+ Element& aBlockElement, const Element& aAncestorElement);
+
+ /**
+ * Split ancestors at the line range boundaries and collect array of contents
+ * in the line to aOutArrayOfContents. Specify aNewContainer to the container
+ * of insertion point to avoid splitting the destination.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
+ SplitToMakeTheLineIsolated(
+ HTMLEditor& aHTMLEditor, const nsIContent& aNewContainer,
+ const Element& aEditingHost,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) const;
+
+ /**
+ * Delete unnecessary trailing line break in aMovedContentRange if there is.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DeleteUnnecessaryTrailingLineBreakInMovedLineEnd(
+ HTMLEditor& aHTMLEditor, const EditorDOMRange& aMovedContentRange,
+ const Element& aEditingHost) const;
+
+ // Range of selected line.
+ EditorDOMRange mLineRange;
+ // Next insertion point. If mMoveToEndOfContainer is `Yes`, this is
+ // recomputed with its container in NextInsertionPointRef. Therefore, this
+ // should not be referred directly.
+ EditorDOMPoint mPointToInsert;
+ // An inclusive ancestor block element of the moving line.
+ RefPtr<Element> mSrcInclusiveAncestorBlock;
+ // An inclusive ancestor block element of the insertion point.
+ RefPtr<Element> mDestInclusiveAncestorBlock;
+ // nullptr if mMovingToParentBlock is false.
+ // Must be non-nullptr if mMovingToParentBlock is true. The topmost ancestor
+ // block element which contains mSrcInclusiveAncestorBlock and a descendant of
+ // mDestInclusiveAncestorBlock. I.e., this may be same as
+ // mSrcInclusiveAncestorBlock, but never same as mDestInclusiveAncestorBlock.
+ RefPtr<Element> mTopmostSrcAncestorBlockInDestBlock;
+ enum class MoveToEndOfContainer { No, Yes };
+ MoveToEndOfContainer mMoveToEndOfContainer;
+ PreserveWhiteSpaceStyle mPreserveWhiteSpaceStyle =
+ PreserveWhiteSpaceStyle::No;
+ // true if mDestInclusiveAncestorBlock is an ancestor of
+ // mSrcInclusiveAncestorBlock.
+ bool mMovingToParentBlock = false;
+};
+
+/**
+ * Convert contents around aRanges of Run() to specified list element. If there
+ * are some different type of list elements, this method converts them to
+ * specified list items too. Basically, each line will be wrapped in a list
+ * item element. However, only when <p> element is selected, its child <br>
+ * elements won't be treated as line separators. Perhaps, this is a bug.
+ */
+class MOZ_STACK_CLASS HTMLEditor::AutoListElementCreator final {
+ public:
+ /**
+ * @param aListElementTagName The new list element tag name.
+ * @param aListItemElementTagName The new list item element tag name.
+ * @param aBulletType If this is not empty string, it's set
+ * to `type` attribute of new list item
+ * elements. Otherwise, existing `type`
+ * attributes will be removed.
+ */
+ AutoListElementCreator(const nsStaticAtom& aListElementTagName,
+ const nsStaticAtom& aListItemElementTagName,
+ const nsAString& aBulletType)
+ // Needs const_cast hack here because the struct users may want
+ // non-const nsStaticAtom pointer due to bug 1794954
+ : mListTagName(const_cast<nsStaticAtom&>(aListElementTagName)),
+ mListItemTagName(const_cast<nsStaticAtom&>(aListItemElementTagName)),
+ mBulletType(aBulletType) {
+ MOZ_ASSERT(&mListTagName == nsGkAtoms::ul ||
+ &mListTagName == nsGkAtoms::ol ||
+ &mListTagName == nsGkAtoms::dl);
+ MOZ_ASSERT_IF(
+ &mListTagName == nsGkAtoms::ul || &mListTagName == nsGkAtoms::ol,
+ &mListItemTagName == nsGkAtoms::li);
+ MOZ_ASSERT_IF(&mListTagName == nsGkAtoms::dl,
+ &mListItemTagName == nsGkAtoms::dt ||
+ &mListItemTagName == nsGkAtoms::dd);
+ }
+
+ /**
+ * @param aHTMLEditor The HTML editor.
+ * @param aRanges [in/out] The ranges which will be converted to list.
+ * The instance must not have saved ranges because it'll
+ * be used in this method.
+ * If succeeded, this will have selection ranges which
+ * should be applied to `Selection`.
+ * If failed, this keeps storing original selection
+ * ranges.
+ * @param aSelectAllOfCurrentList Yes if this should treat all of
+ * ancestor list element at selection.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
+ HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges,
+ HTMLEditor::SelectAllOfCurrentList aSelectAllOfCurrentList,
+ const Element& aEditingHost) const;
+
+ private:
+ using ContentNodeArray = nsTArray<OwningNonNull<nsIContent>>;
+ using AutoContentNodeArray = AutoTArray<OwningNonNull<nsIContent>, 64>;
+
+ /**
+ * If aSelectAllOfCurrentList is "Yes" and aRanges is in a list element,
+ * returns the list element.
+ * Otherwise, extend aRanges to select start and end lines selected by it and
+ * correct all topmost content nodes in the extended ranges with splitting
+ * ancestors at range edges.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList(
+ HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges,
+ SelectAllOfCurrentList aSelectAllOfCurrentList,
+ const Element& aEditingHost, ContentNodeArray& aOutArrayOfContents) const;
+
+ /**
+ * Return true if aArrayOfContents has only <br> elements or empty inline
+ * container elements. I.e., it means that aArrayOfContents represents
+ * only empty line(s) if this returns true.
+ */
+ [[nodiscard]] static bool
+ IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements(
+ const ContentNodeArray& aArrayOfContents);
+
+ /**
+ * Delete all content nodes ina ArrayOfContents, and if we can put new list
+ * element at start of the first range of aRanges, insert new list element
+ * there.
+ *
+ * @return The empty list item element in new list element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
+ ReplaceContentNodesWithEmptyNewList(
+ HTMLEditor& aHTMLEditor, const AutoRangeArray& aRanges,
+ const AutoContentNodeArray& aArrayOfContents,
+ const Element& aEditingHost) const;
+
+ /**
+ * Creat new list elements or use existing list elements and move
+ * aArrayOfContents into list item elements.
+ *
+ * @return A list or list item element which should have caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
+ WrapContentNodesIntoNewListElements(HTMLEditor& aHTMLEditor,
+ AutoRangeArray& aRanges,
+ AutoContentNodeArray& aArrayOfContents,
+ const Element& aEditingHost) const;
+
+ struct MOZ_STACK_CLASS AutoHandlingState final {
+ // Current list element which is a good container to create new list item
+ // element.
+ RefPtr<Element> mCurrentListElement;
+ // Previously handled list item element.
+ RefPtr<Element> mPreviousListItemElement;
+ // List or list item element which should have caret after handling all
+ // contents.
+ RefPtr<Element> mListOrListItemElementToPutCaret;
+ // Replacing block element. This is typically already removed from the DOM
+ // tree.
+ RefPtr<Element> mReplacingBlockElement;
+ // Once id attribute of mReplacingBlockElement copied, the id attribute
+ // shouldn't be copied again.
+ bool mMaybeCopiedReplacingBlockElementId = false;
+ };
+
+ /**
+ * Helper methods of WrapContentNodesIntoNewListElements. They are called for
+ * handling one content node of aArrayOfContents. It's set to aHandling*.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildContent(
+ HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
+ AutoHandlingState& aState, const Element& aEditingHost) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ HandleChildListElement(HTMLEditor& aHTMLEditor, Element& aHandlingListElement,
+ AutoHandlingState& aState) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListItemElement(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
+ AutoHandlingState& aState) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ HandleChildListItemInDifferentTypeList(HTMLEditor& aHTMLEditor,
+ Element& aHandlingListItemElement,
+ AutoHandlingState& aState) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListItemInSameTypeList(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
+ AutoHandlingState& aState) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildDivOrParagraphElement(
+ HTMLEditor& aHTMLEditor, Element& aHandlingDivOrParagraphElement,
+ AutoHandlingState& aState, const Element& aEditingHost) const;
+ enum class EmptyListItem { NotCreate, Create };
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult CreateAndUpdateCurrentListElement(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert,
+ EmptyListItem aEmptyListItem, AutoHandlingState& aState,
+ const Element& aEditingHost) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
+ AppendListItemElement(HTMLEditor& aHTMLEditor, const Element& aListElement,
+ AutoHandlingState& aState) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
+ MaybeCloneAttributesToNewListItem(HTMLEditor& aHTMLEditor,
+ Element& aListItemElement,
+ AutoHandlingState& aState);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildInlineContent(
+ HTMLEditor& aHTMLEditor, nsIContent& aHandlingInlineContent,
+ AutoHandlingState& aState) const;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult WrapContentIntoNewListItemElement(
+ HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
+ AutoHandlingState& aState) const;
+
+ /**
+ * If aRanges is collapsed outside aListItemOrListToPutCaret, this collapse
+ * aRanges in aListItemOrListToPutCaret again.
+ */
+ nsresult EnsureCollapsedRangeIsInListItemOrListElement(
+ Element& aListItemOrListToPutCaret, AutoRangeArray& aRanges) const;
+
+ MOZ_KNOWN_LIVE nsStaticAtom& mListTagName;
+ MOZ_KNOWN_LIVE nsStaticAtom& mListItemTagName;
+ const nsAutoString mBulletType;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef HTMLEditorNestedClasses_h
diff --git a/editor/libeditor/HTMLEditorObjectResizer.cpp b/editor/libeditor/HTMLEditorObjectResizer.cpp
new file mode 100644
index 0000000000..8a11c8487c
--- /dev/null
+++ b/editor/libeditor/HTMLEditorObjectResizer.cpp
@@ -0,0 +1,1493 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ErrorList.h"
+#include "HTMLEditor.h"
+
+#include "CSSEditUtils.h"
+#include "HTMLEditorEventListener.h"
+#include "HTMLEditUtils.h"
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/EventTarget.h"
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsAtom.h"
+#include "nsIContent.h"
+#include "nsID.h"
+#include "mozilla/dom/Document.h"
+#include "nsISupportsUtils.h"
+#include "nsPIDOMWindow.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStyledElement.h"
+#include "nsTextNode.h"
+#include "nscore.h"
+#include <algorithm>
+
+#define kTopLeft u"nw"_ns
+#define kTop u"n"_ns
+#define kTopRight u"ne"_ns
+#define kLeft u"w"_ns
+#define kRight u"e"_ns
+#define kBottomLeft u"sw"_ns
+#define kBottom u"s"_ns
+#define kBottomRight u"se"_ns
+
+namespace mozilla {
+
+using namespace dom;
+
+/******************************************************************************
+ * mozilla::HTMLEditor
+ ******************************************************************************/
+
+ManualNACPtr HTMLEditor::CreateResizer(int16_t aLocation,
+ nsIContent& aParentContent) {
+ ManualNACPtr resizer = CreateAnonymousElement(nsGkAtoms::span, aParentContent,
+ u"mozResizer"_ns, false);
+ if (!resizer) {
+ NS_WARNING(
+ "HTMLEditor::CreateAnonymousElement(nsGkAtoms::span, mozResizer) "
+ "failed");
+ return nullptr;
+ }
+
+ // add the mouse listener so we can detect a click on a resizer
+ DebugOnly<nsresult> rvIgnored =
+ resizer->AddEventListener(u"mousedown"_ns, mEventListener, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EventTarget::AddEventListener(mousedown) failed, but ignored");
+
+ nsAutoString locationStr;
+ switch (aLocation) {
+ case nsIHTMLObjectResizer::eTopLeft:
+ locationStr = kTopLeft;
+ break;
+ case nsIHTMLObjectResizer::eTop:
+ locationStr = kTop;
+ break;
+ case nsIHTMLObjectResizer::eTopRight:
+ locationStr = kTopRight;
+ break;
+
+ case nsIHTMLObjectResizer::eLeft:
+ locationStr = kLeft;
+ break;
+ case nsIHTMLObjectResizer::eRight:
+ locationStr = kRight;
+ break;
+
+ case nsIHTMLObjectResizer::eBottomLeft:
+ locationStr = kBottomLeft;
+ break;
+ case nsIHTMLObjectResizer::eBottom:
+ locationStr = kBottom;
+ break;
+ case nsIHTMLObjectResizer::eBottomRight:
+ locationStr = kBottomRight;
+ break;
+ }
+
+ if (NS_FAILED(resizer->SetAttr(kNameSpaceID_None, nsGkAtoms::anonlocation,
+ locationStr, true))) {
+ NS_WARNING("Element::SetAttr(nsGkAtoms::anonlocation) failed");
+ return nullptr;
+ }
+ return resizer;
+}
+
+ManualNACPtr HTMLEditor::CreateShadow(nsIContent& aParentContent,
+ Element& aOriginalObject) {
+ // let's create an image through the element factory
+ RefPtr<nsAtom> name;
+ if (HTMLEditUtils::IsImage(&aOriginalObject)) {
+ name = nsGkAtoms::img;
+ } else {
+ name = nsGkAtoms::span;
+ }
+
+ return CreateAnonymousElement(name, aParentContent, u"mozResizingShadow"_ns,
+ true);
+}
+
+ManualNACPtr HTMLEditor::CreateResizingInfo(nsIContent& aParentContent) {
+ // let's create an info box through the element factory
+ return CreateAnonymousElement(nsGkAtoms::span, aParentContent,
+ u"mozResizingInfo"_ns, true);
+}
+
+nsresult HTMLEditor::SetAllResizersPosition() {
+ if (NS_WARN_IF(!mTopLeftHandle)) {
+ return NS_ERROR_FAILURE; // There are no resizers.
+ }
+
+ int32_t x = mResizedObjectX;
+ int32_t y = mResizedObjectY;
+ int32_t w = mResizedObjectWidth;
+ int32_t h = mResizedObjectHeight;
+
+ nsAutoString value;
+ float resizerWidth, resizerHeight;
+ RefPtr<nsAtom> dummyUnit;
+ DebugOnly<nsresult> rvIgnored = NS_OK;
+ OwningNonNull<Element> topLeftHandle = *mTopLeftHandle.get();
+ // XXX Do we really need to computed value rather than specified value?
+ // Because it's an anonymous node.
+ rvIgnored = CSSEditUtils::GetComputedProperty(*topLeftHandle,
+ *nsGkAtoms::width, value);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_WARN_IF(topLeftHandle != mTopLeftHandle)) {
+ return NS_ERROR_FAILURE;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::width) "
+ "failed, but ignored");
+ rvIgnored = CSSEditUtils::GetComputedProperty(*topLeftHandle,
+ *nsGkAtoms::height, value);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_WARN_IF(topLeftHandle != mTopLeftHandle)) {
+ return NS_ERROR_FAILURE;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::height) "
+ "failed, but ignored");
+ CSSEditUtils::ParseLength(value, &resizerWidth, getter_AddRefs(dummyUnit));
+ CSSEditUtils::ParseLength(value, &resizerHeight, getter_AddRefs(dummyUnit));
+
+ int32_t rw = static_cast<int32_t>((resizerWidth + 1) / 2);
+ int32_t rh = static_cast<int32_t>((resizerHeight + 1) / 2);
+
+ // While moving each resizer, mutation event listener may hide the resizers.
+ // And in worst case, new resizers may be recreated. So, we need to store
+ // all resizers here, and then, if we detect a resizer is removed or replaced,
+ // we should do nothing anymore.
+ // FYI: Note that only checking if mTopLeftHandle is replaced is enough.
+ // We're may be in hot path if user resizes an element a lot. So,
+ // we should just add-ref mTopLeftHandle.
+ auto setHandlePosition =
+ [this](ManualNACPtr& aHandleElement, int32_t aNewX, int32_t aNewY)
+ MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> nsresult {
+ RefPtr<nsStyledElement> handleStyledElement =
+ nsStyledElement::FromNodeOrNull(aHandleElement.get());
+ if (!handleStyledElement) {
+ return NS_OK;
+ }
+ nsresult rv = SetAnonymousElementPositionWithoutTransaction(
+ *handleStyledElement, aNewX, aNewY);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::SetAnonymousElementPositionWithoutTransaction() "
+ "failed");
+ return rv;
+ }
+ return NS_WARN_IF(handleStyledElement != aHandleElement.get())
+ ? NS_ERROR_FAILURE
+ : NS_OK;
+ };
+ nsresult rv;
+ rv = setHandlePosition(mTopLeftHandle, x - rw, y - rh);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set top-left handle position");
+ return rv;
+ }
+ rv = setHandlePosition(mTopHandle, x + w / 2 - rw, y - rh);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set top handle position");
+ return rv;
+ }
+ rv = setHandlePosition(mTopRightHandle, x + w - rw - 1, y - rh);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set top-right handle position");
+ return rv;
+ }
+
+ rv = setHandlePosition(mLeftHandle, x - rw, y + h / 2 - rh);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set left handle position");
+ return rv;
+ }
+ rv = setHandlePosition(mRightHandle, x + w - rw - 1, y + h / 2 - rh);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set right handle position");
+ return rv;
+ }
+
+ rv = setHandlePosition(mBottomLeftHandle, x - rw, y + h - rh - 1);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set bottom-left handle position");
+ return rv;
+ }
+ rv = setHandlePosition(mBottomHandle, x + w / 2 - rw, y + h - rh - 1);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set bottom handle position");
+ return rv;
+ }
+ rv = setHandlePosition(mBottomRightHandle, x + w - rw - 1, y + h - rh - 1);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set bottom-right handle position");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::RefreshResizers() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = RefreshResizersInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::RefreshResizersInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::RefreshResizersInternal() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Don't warn even if resizers are not visible since script cannot check
+ // if they are visible and this is non-virtual method. So, the cost of
+ // calling this can be ignored.
+ if (!mResizedObject) {
+ return NS_OK;
+ }
+
+ OwningNonNull<Element> resizedObject = *mResizedObject;
+ nsresult rv = GetPositionAndDimensions(
+ resizedObject, mResizedObjectX, mResizedObjectY, mResizedObjectWidth,
+ mResizedObjectHeight, mResizedObjectBorderLeft, mResizedObjectBorderTop,
+ mResizedObjectMarginLeft, mResizedObjectMarginTop);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetPositionAndDimensions() failed");
+ return rv;
+ }
+ if (NS_WARN_IF(resizedObject != mResizedObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = SetAllResizersPosition();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetAllResizersPosition() failed");
+ return rv;
+ }
+ if (NS_WARN_IF(resizedObject != mResizedObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(
+ mResizingShadow,
+ "SetAllResizersPosition() should return error if resizers are hidden");
+ RefPtr<Element> resizingShadow = mResizingShadow.get();
+ rv = SetShadowPosition(*resizingShadow, resizedObject, mResizedObjectX,
+ mResizedObjectY);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetShadowPosition() failed");
+ return rv;
+ }
+ if (NS_WARN_IF(resizedObject != mResizedObject)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::ShowResizersInternal(Element& aResizedElement) {
+ // When we have visible resizers, we cannot show new resizers.
+ // So, the caller should call HideResizersInternal() first if this
+ // returns error.
+ if (NS_WARN_IF(mResizedObject)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCOMPtr<nsIContent> parentContent = aResizedElement.GetParent();
+ if (NS_WARN_IF(!parentContent)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost) ||
+ NS_WARN_IF(!aResizedElement.IsInclusiveDescendantOf(editingHost))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // Let's create and setup resizers. If we failed something, we should
+ // cancel everything which we do in this method.
+ do {
+ mResizedObject = &aResizedElement;
+
+ // The resizers and the shadow will be anonymous siblings of the element.
+ // Note that creating a resizer or shadow may causes calling
+ // HideRisizersInternal() via a mutation event listener. So, we should
+ // store new resizer to a local variable, then, check:
+ // - whether creating resizer is already set to the member or not
+ // - whether resizing element is changed to another element
+ // If showing resizers are canceled, we hit the latter check.
+ // If resizers for another element is shown during this, we hit the latter
+ // check too.
+ // If resizers are just shown again for same element, we hit the former
+ // check.
+ ManualNACPtr newResizer =
+ CreateResizer(nsIHTMLObjectResizer::eTopLeft, *parentContent);
+ if (!newResizer) {
+ NS_WARNING("HTMLEditor::CreateResizer(eTopLeft) failed");
+ break;
+ }
+ if (NS_WARN_IF(mTopLeftHandle) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ // Don't hide current resizers in this case because they are not what
+ // we're creating.
+ return NS_ERROR_FAILURE;
+ }
+ mTopLeftHandle = std::move(newResizer);
+ newResizer = CreateResizer(nsIHTMLObjectResizer::eTop, *parentContent);
+ if (!newResizer) {
+ NS_WARNING("HTMLEditor::CreateResizer(eTop) failed");
+ break;
+ }
+ if (NS_WARN_IF(mTopHandle) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mTopHandle = std::move(newResizer);
+ newResizer = CreateResizer(nsIHTMLObjectResizer::eTopRight, *parentContent);
+ if (!newResizer) {
+ NS_WARNING("HTMLEditor::CreateResizer(eTopRight) failed");
+ break;
+ }
+ if (NS_WARN_IF(mTopRightHandle) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mTopRightHandle = std::move(newResizer);
+
+ newResizer = CreateResizer(nsIHTMLObjectResizer::eLeft, *parentContent);
+ if (!newResizer) {
+ NS_WARNING("HTMLEditor::CreateResizer(eLeft) failed");
+ break;
+ }
+ if (NS_WARN_IF(mLeftHandle) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mLeftHandle = std::move(newResizer);
+ newResizer = CreateResizer(nsIHTMLObjectResizer::eRight, *parentContent);
+ if (!newResizer) {
+ NS_WARNING("HTMLEditor::CreateResizer(eRight) failed");
+ break;
+ }
+ if (NS_WARN_IF(mRightHandle) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mRightHandle = std::move(newResizer);
+
+ newResizer =
+ CreateResizer(nsIHTMLObjectResizer::eBottomLeft, *parentContent);
+ if (!newResizer) {
+ NS_WARNING("HTMLEditor::CreateResizer(eBottomLeft) failed");
+ break;
+ }
+ if (NS_WARN_IF(mBottomLeftHandle) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mBottomLeftHandle = std::move(newResizer);
+ newResizer = CreateResizer(nsIHTMLObjectResizer::eBottom, *parentContent);
+ if (!newResizer) {
+ NS_WARNING("HTMLEditor::CreateResizer(eBottom) failed");
+ break;
+ }
+ if (NS_WARN_IF(mBottomHandle) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mBottomHandle = std::move(newResizer);
+ newResizer =
+ CreateResizer(nsIHTMLObjectResizer::eBottomRight, *parentContent);
+ if (!newResizer) {
+ NS_WARNING("HTMLEditor::CreateResizer(eBottomRight) failed");
+ break;
+ }
+ if (NS_WARN_IF(mBottomRightHandle) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mBottomRightHandle = std::move(newResizer);
+
+ // Store the last resizer which we created. This is useful when we
+ // need to check whether our resizers are hiddedn and recreated another
+ // set of resizers or not.
+ RefPtr<Element> createdBottomRightHandle = mBottomRightHandle.get();
+
+ nsresult rv = GetPositionAndDimensions(
+ aResizedElement, mResizedObjectX, mResizedObjectY, mResizedObjectWidth,
+ mResizedObjectHeight, mResizedObjectBorderLeft, mResizedObjectBorderTop,
+ mResizedObjectMarginLeft, mResizedObjectMarginTop);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetPositionAndDimensions() failed");
+ break;
+ }
+
+ // and let's set their absolute positions in the document
+ rv = SetAllResizersPosition();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetAllResizersPosition() failed");
+ if (NS_WARN_IF(mBottomRightHandle.get() != createdBottomRightHandle)) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+ }
+
+ // now, let's create the resizing shadow
+ ManualNACPtr newShadow = CreateShadow(*parentContent, aResizedElement);
+ if (!newShadow) {
+ NS_WARNING("HTMLEditor::CreateShadow() failed");
+ break;
+ }
+ if (NS_WARN_IF(mResizingShadow) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mResizingShadow = std::move(newShadow);
+
+ // and set its position
+ RefPtr<Element> resizingShadow = mResizingShadow.get();
+ rv = SetShadowPosition(*resizingShadow, aResizedElement, mResizedObjectX,
+ mResizedObjectY);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetShadowPosition() failed");
+ if (NS_WARN_IF(mBottomRightHandle.get() != createdBottomRightHandle)) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+ }
+
+ // and then the resizing info tooltip
+ ManualNACPtr newResizingInfo = CreateResizingInfo(*parentContent);
+ if (!newResizingInfo) {
+ NS_WARNING("HTMLEditor::CreateResizingInfo() failed");
+ break;
+ }
+ if (NS_WARN_IF(mResizingInfo) ||
+ NS_WARN_IF(mResizedObject != &aResizedElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mResizingInfo = std::move(newResizingInfo);
+
+ // and listen to the "resize" event on the window first, get the
+ // window from the document...
+ if (NS_WARN_IF(!mEventListener)) {
+ break;
+ }
+
+ rv = static_cast<HTMLEditorEventListener*>(mEventListener.get())
+ ->ListenToWindowResizeEvent(true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditorEventListener::ListenToWindowResizeEvent(true) failed");
+ break;
+ }
+
+ MOZ_ASSERT(mResizedObject == &aResizedElement);
+
+ // XXX Even when it failed to add event listener, should we need to set
+ // _moz_resizing attribute?
+ DebugOnly<nsresult> rvIgnored = aResizedElement.SetAttr(
+ kNameSpaceID_None, nsGkAtoms::_moz_resizing, u"true"_ns, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::_moz_resizing, true) failed, but ignored");
+ return NS_OK;
+ } while (true);
+
+ DebugOnly<nsresult> rv = HideResizersInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::HideResizersInternal() failed to clean up");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP HTMLEditor::HideResizers() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = HideResizersInternal();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::HideResizersInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::HideResizersInternal() {
+ // Don't warn even if resizers are visible since script cannot check
+ // if they are visible and this is non-virtual method. So, the cost of
+ // calling this can be ignored.
+ if (!mResizedObject) {
+ return NS_OK;
+ }
+
+ // get the presshell's document observer interface.
+ RefPtr<PresShell> presShell = GetPresShell();
+ NS_WARNING_ASSERTION(presShell, "There is no presShell");
+ // We allow the pres shell to be null; when it is, we presume there
+ // are no document observers to notify, but we still want to
+ // UnbindFromTree.
+
+ constexpr auto mousedown = u"mousedown"_ns;
+
+ // HTMLEditor should forget all members related to resizers first since
+ // removing a part of UI may cause showing the resizers again. In such
+ // case, the members may be overwritten by ShowResizers() and this will
+ // lose the chance to release the old resizers.
+ ManualNACPtr topLeftHandle(std::move(mTopLeftHandle));
+ ManualNACPtr topHandle(std::move(mTopHandle));
+ ManualNACPtr topRightHandle(std::move(mTopRightHandle));
+ ManualNACPtr leftHandle(std::move(mLeftHandle));
+ ManualNACPtr rightHandle(std::move(mRightHandle));
+ ManualNACPtr bottomLeftHandle(std::move(mBottomLeftHandle));
+ ManualNACPtr bottomHandle(std::move(mBottomHandle));
+ ManualNACPtr bottomRightHandle(std::move(mBottomRightHandle));
+ ManualNACPtr resizingShadow(std::move(mResizingShadow));
+ ManualNACPtr resizingInfo(std::move(mResizingInfo));
+ RefPtr<Element> activatedHandle(std::move(mActivatedHandle));
+ RefPtr<Element> resizedObject(std::move(mResizedObject));
+
+ // Remvoe all handles.
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(topLeftHandle), presShell);
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(topHandle), presShell);
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(topRightHandle), presShell);
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(leftHandle), presShell);
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(rightHandle), presShell);
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(bottomLeftHandle), presShell);
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(bottomHandle), presShell);
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(bottomRightHandle), presShell);
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(resizingShadow), presShell);
+
+ RemoveListenerAndDeleteRef(mousedown, mEventListener, true,
+ std::move(resizingInfo), presShell);
+
+ // Remove active state of a resizer.
+ if (activatedHandle) {
+ DebugOnly<nsresult> rvIgnored = activatedHandle->UnsetAttr(
+ kNameSpaceID_None, nsGkAtoms::_moz_activated, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::UnsetAttr(nsGkAtoms::_moz_activated) failed, but ignored");
+ }
+
+ // Remove resizing state of the target element.
+ DebugOnly<nsresult> rvIgnored = resizedObject->UnsetAttr(
+ kNameSpaceID_None, nsGkAtoms::_moz_resizing, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::UnsetAttr(nsGkAtoms::_moz_resizing) failed, but ignored");
+
+ if (!mEventListener) {
+ return NS_OK;
+ }
+
+ nsresult rv = static_cast<HTMLEditorEventListener*>(mEventListener.get())
+ ->ListenToMouseMoveEventForResizers(false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditorEventListener::ListenToMouseMoveEventForResizers(false) "
+ "failed");
+ return rv;
+ }
+
+ // Remove resize event listener from the window.
+ if (!mEventListener) {
+ return NS_OK;
+ }
+
+ rv = static_cast<HTMLEditorEventListener*>(mEventListener.get())
+ ->ListenToWindowResizeEvent(false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditorEventListener::ListenToWindowResizeEvent(false) failed");
+ return rv;
+}
+
+void HTMLEditor::HideShadowAndInfo() {
+ if (mResizingShadow) {
+ DebugOnly<nsresult> rvIgnored = mResizingShadow->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::_class, u"hidden"_ns, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::_class, hidden) failed, but ignored");
+ }
+ if (mResizingInfo) {
+ DebugOnly<nsresult> rvIgnored = mResizingInfo->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::_class, u"hidden"_ns, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::_class, hidden) failed, but ignored");
+ }
+}
+
+nsresult HTMLEditor::StartResizing(Element& aHandleElement) {
+ mIsResizing = true;
+ mActivatedHandle = &aHandleElement;
+ DebugOnly<nsresult> rvIgnored = mActivatedHandle->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::_moz_activated, u"true"_ns, true);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::_moz_activated, true) failed");
+
+ // do we want to preserve ratio or not?
+ const bool preserveRatio = HTMLEditUtils::IsImage(mResizedObject);
+
+ // the way we change the position/size of the shadow depends on
+ // the handle
+ nsAutoString locationStr;
+ mActivatedHandle->GetAttr(nsGkAtoms::anonlocation, locationStr);
+ if (locationStr.Equals(kTopLeft)) {
+ SetResizeIncrements(1, 1, -1, -1, preserveRatio);
+ } else if (locationStr.Equals(kTop)) {
+ SetResizeIncrements(0, 1, 0, -1, false);
+ } else if (locationStr.Equals(kTopRight)) {
+ SetResizeIncrements(0, 1, 1, -1, preserveRatio);
+ } else if (locationStr.Equals(kLeft)) {
+ SetResizeIncrements(1, 0, -1, 0, false);
+ } else if (locationStr.Equals(kRight)) {
+ SetResizeIncrements(0, 0, 1, 0, false);
+ } else if (locationStr.Equals(kBottomLeft)) {
+ SetResizeIncrements(1, 0, -1, 1, preserveRatio);
+ } else if (locationStr.Equals(kBottom)) {
+ SetResizeIncrements(0, 0, 0, 1, false);
+ } else if (locationStr.Equals(kBottomRight)) {
+ SetResizeIncrements(0, 0, 1, 1, preserveRatio);
+ }
+
+ // make the shadow appear
+ rvIgnored =
+ mResizingShadow->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Element::UnsetAttr(nsGkAtoms::_class) failed");
+
+ // position it
+ if (RefPtr<nsStyledElement> resizingShadowStyledElement =
+ nsStyledElement::FromNodeOrNull(mResizingShadow.get())) {
+ nsresult rv;
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *resizingShadowStyledElement, *nsGkAtoms::width, mResizedObjectWidth);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction("
+ "nsGkAtoms::width) destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction("
+ "nsGkAtoms::width) failed");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *resizingShadowStyledElement, *nsGkAtoms::height, mResizedObjectHeight);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction("
+ "nsGkAtoms::height) destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction("
+ "nsGkAtoms::height) failed");
+ }
+
+ // add a mouse move listener to the editor
+ if (NS_WARN_IF(!mEventListener)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv = static_cast<HTMLEditorEventListener*>(mEventListener.get())
+ ->ListenToMouseMoveEventForResizers(true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditorEventListener::"
+ "ListenToMouseMoveEventForResizers(true) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::StartToDragResizerOrHandleDragGestureOnGrabber(
+ MouseEvent& aMouseDownEvent, Element& aEventTargetElement) {
+ MOZ_ASSERT(aMouseDownEvent.GetExplicitOriginalTarget() ==
+ &aEventTargetElement);
+ MOZ_ASSERT(!aMouseDownEvent.DefaultPrevented());
+ MOZ_ASSERT(aMouseDownEvent.WidgetEventPtr()->mMessage == eMouseDown);
+
+ nsAutoString anonclass;
+ aEventTargetElement.GetAttr(nsGkAtoms::_moz_anonclass, anonclass);
+
+ if (anonclass.EqualsLiteral("mozResizer")) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eResizingElement);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // If we have an anonymous element and that element is a resizer,
+ // let's start resizing!
+ aMouseDownEvent.PreventDefault();
+ mOriginalX = aMouseDownEvent.ClientX();
+ mOriginalY = aMouseDownEvent.ClientY();
+ nsresult rv = StartResizing(aEventTargetElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::StartResizing() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (anonclass.EqualsLiteral("mozGrabber")) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eMovingElement);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // If we have an anonymous element and that element is a grabber,
+ // let's start moving the element!
+ mOriginalX = aMouseDownEvent.ClientX();
+ mOriginalY = aMouseDownEvent.ClientY();
+ nsresult rv = GrabberClicked();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GrabberClicked() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::StopDraggingResizerOrGrabberAt(
+ const CSSIntPoint& aClientPoint) {
+ if (mIsResizing) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eResizeElement);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // we are resizing and release the mouse button, so let's
+ // end the resizing process
+ mIsResizing = false;
+ HideShadowAndInfo();
+
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "EditorBase::MaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = SetFinalSizeWithTransaction(aClientPoint.x, aClientPoint.y);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "HTMLEditor::SetFinalSizeWithTransaction() destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::SetFinalSizeWithTransaction() failed, but ignored");
+ return NS_OK;
+ }
+
+ if (mIsMoving || mGrabberClicked) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eMoveElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (rv != NS_ERROR_EDITOR_ACTION_CANCELED && NS_WARN_IF(NS_FAILED(rv))) {
+ NS_WARNING("CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (mIsMoving) {
+ DebugOnly<nsresult> rvIgnored = mPositioningShadow->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::_class, u"hidden"_ns, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::_class, hidden) failed");
+ if (rv != NS_ERROR_EDITOR_ACTION_CANCELED) {
+ SetFinalPosition(aClientPoint.x, aClientPoint.y);
+ }
+ }
+ if (mGrabberClicked) {
+ DebugOnly<nsresult> rvIgnored = EndMoving();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::EndMoving() failed");
+ }
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ return NS_OK;
+}
+
+void HTMLEditor::SetResizeIncrements(int32_t aX, int32_t aY, int32_t aW,
+ int32_t aH, bool aPreserveRatio) {
+ mXIncrementFactor = aX;
+ mYIncrementFactor = aY;
+ mWidthIncrementFactor = aW;
+ mHeightIncrementFactor = aH;
+ mPreserveRatio = aPreserveRatio;
+}
+
+nsresult HTMLEditor::SetResizingInfoPosition(int32_t aX, int32_t aY, int32_t aW,
+ int32_t aH) {
+ // Determine the position of the resizing info box based upon the new
+ // position and size of the element (aX, aY, aW, aH), and which
+ // resizer is the "activated handle". For example, place the resizing
+ // info box at the bottom-right corner of the new element, if the element
+ // is being resized by the bottom-right resizer.
+ int32_t infoXPosition;
+ int32_t infoYPosition;
+
+ if (mActivatedHandle == mTopLeftHandle || mActivatedHandle == mLeftHandle ||
+ mActivatedHandle == mBottomLeftHandle) {
+ infoXPosition = aX;
+ } else if (mActivatedHandle == mTopHandle ||
+ mActivatedHandle == mBottomHandle) {
+ infoXPosition = aX + (aW / 2);
+ } else {
+ // should only occur when mActivatedHandle is one of the 3 right-side
+ // handles, but this is a reasonable default if it isn't any of them (?)
+ infoXPosition = aX + aW;
+ }
+
+ if (mActivatedHandle == mTopLeftHandle || mActivatedHandle == mTopHandle ||
+ mActivatedHandle == mTopRightHandle) {
+ infoYPosition = aY;
+ } else if (mActivatedHandle == mLeftHandle ||
+ mActivatedHandle == mRightHandle) {
+ infoYPosition = aY + (aH / 2);
+ } else {
+ // should only occur when mActivatedHandle is one of the 3 bottom-side
+ // handles, but this is a reasonable default if it isn't any of them (?)
+ infoYPosition = aY + aH;
+ }
+
+ // Offset info box by 20 so it's not directly under the mouse cursor.
+ const int mouseCursorOffset = 20;
+ if (RefPtr<nsStyledElement> resizingInfoStyledElement =
+ nsStyledElement::FromNodeOrNull(mResizingInfo.get())) {
+ nsresult rv;
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *resizingInfoStyledElement, *nsGkAtoms::left,
+ infoXPosition + mouseCursorOffset);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left) "
+ "failed, but ignored");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *resizingInfoStyledElement, *nsGkAtoms::top,
+ infoYPosition + mouseCursorOffset);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "failed, but ignored");
+ }
+
+ nsCOMPtr<nsIContent> textInfo = mResizingInfo->GetFirstChild();
+ ErrorResult error;
+ if (textInfo) {
+ mResizingInfo->RemoveChild(*textInfo, error);
+ if (error.Failed()) {
+ NS_WARNING("nsINode::RemoveChild() failed");
+ return error.StealNSResult();
+ }
+ textInfo = nullptr;
+ }
+
+ nsAutoString widthStr, heightStr, diffWidthStr, diffHeightStr;
+ widthStr.AppendInt(aW);
+ heightStr.AppendInt(aH);
+ int32_t diffWidth = aW - mResizedObjectWidth;
+ int32_t diffHeight = aH - mResizedObjectHeight;
+ if (diffWidth > 0) {
+ diffWidthStr.Assign('+');
+ }
+ if (diffHeight > 0) {
+ diffHeightStr.Assign('+');
+ }
+ diffWidthStr.AppendInt(diffWidth);
+ diffHeightStr.AppendInt(diffHeight);
+
+ nsAutoString info(widthStr + u" x "_ns + heightStr + u" ("_ns + diffWidthStr +
+ u", "_ns + diffHeightStr + u")"_ns);
+
+ RefPtr<Document> document = GetDocument();
+ textInfo = document->CreateTextNode(info);
+ if (!textInfo) {
+ NS_WARNING("Document::CreateTextNode() failed");
+ return NS_ERROR_FAILURE;
+ }
+ mResizingInfo->AppendChild(*textInfo, error);
+ if (error.Failed()) {
+ NS_WARNING("nsINode::AppendChild() failed");
+ return error.StealNSResult();
+ }
+
+ nsresult rv =
+ mResizingInfo->UnsetAttr(kNameSpaceID_None, nsGkAtoms::_class, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Element::UnsetAttr(nsGkAtoms::_class) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SetShadowPosition(Element& aShadowElement,
+ Element& aElement, int32_t aElementX,
+ int32_t aElementY) {
+ MOZ_ASSERT(&aShadowElement == mResizingShadow ||
+ &aShadowElement == mPositioningShadow);
+ RefPtr<Element> handlingShadowElement = &aShadowElement == mResizingShadow
+ ? mResizingShadow.get()
+ : mPositioningShadow.get();
+
+ if (nsStyledElement* styledShadowElement =
+ nsStyledElement::FromNode(&aShadowElement)) {
+ // MOZ_KnownLive(*styledShadowElement): It's aShadowElement whose lifetime
+ // must be guaranteed by caller because of MOZ_CAN_RUN_SCRIPT method.
+ nsresult rv = SetAnonymousElementPositionWithoutTransaction(
+ MOZ_KnownLive(*styledShadowElement), aElementX, aElementY);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::SetAnonymousElementPositionWithoutTransaction() "
+ "failed");
+ return rv;
+ }
+ }
+
+ if (!HTMLEditUtils::IsImage(&aElement)) {
+ return NS_OK;
+ }
+
+ nsAutoString imageSource;
+ aElement.GetAttr(nsGkAtoms::src, imageSource);
+ nsresult rv = aShadowElement.SetAttr(kNameSpaceID_None, nsGkAtoms::src,
+ imageSource, true);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Element::SetAttr(nsGkAtoms::src) failed");
+ return NS_ERROR_FAILURE;
+ }
+ return NS_WARN_IF(&aShadowElement != handlingShadowElement) ? NS_ERROR_FAILURE
+ : NS_OK;
+}
+
+int32_t HTMLEditor::GetNewResizingIncrement(int32_t aX, int32_t aY,
+ ResizeAt aResizeAt) const {
+ int32_t result = 0;
+ if (!mPreserveRatio) {
+ switch (aResizeAt) {
+ case ResizeAt::eX:
+ case ResizeAt::eWidth:
+ result = aX - mOriginalX;
+ break;
+ case ResizeAt::eY:
+ case ResizeAt::eHeight:
+ result = aY - mOriginalY;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid resizing request");
+ }
+ return result;
+ }
+
+ int32_t xi = (aX - mOriginalX) * mWidthIncrementFactor;
+ int32_t yi = (aY - mOriginalY) * mHeightIncrementFactor;
+ float objectSizeRatio =
+ ((float)mResizedObjectWidth) / ((float)mResizedObjectHeight);
+ result = (xi > yi) ? xi : yi;
+ switch (aResizeAt) {
+ case ResizeAt::eX:
+ case ResizeAt::eWidth:
+ if (result == yi) result = (int32_t)(((float)result) * objectSizeRatio);
+ result = (int32_t)(((float)result) * mWidthIncrementFactor);
+ break;
+ case ResizeAt::eY:
+ case ResizeAt::eHeight:
+ if (result == xi) result = (int32_t)(((float)result) / objectSizeRatio);
+ result = (int32_t)(((float)result) * mHeightIncrementFactor);
+ break;
+ }
+ return result;
+}
+
+int32_t HTMLEditor::GetNewResizingX(int32_t aX, int32_t aY) {
+ int32_t resized =
+ mResizedObjectX +
+ GetNewResizingIncrement(aX, aY, ResizeAt::eX) * mXIncrementFactor;
+ int32_t max = mResizedObjectX + mResizedObjectWidth;
+ return std::min(resized, max);
+}
+
+int32_t HTMLEditor::GetNewResizingY(int32_t aX, int32_t aY) {
+ int32_t resized =
+ mResizedObjectY +
+ GetNewResizingIncrement(aX, aY, ResizeAt::eY) * mYIncrementFactor;
+ int32_t max = mResizedObjectY + mResizedObjectHeight;
+ return std::min(resized, max);
+}
+
+int32_t HTMLEditor::GetNewResizingWidth(int32_t aX, int32_t aY) {
+ int32_t resized =
+ mResizedObjectWidth +
+ GetNewResizingIncrement(aX, aY, ResizeAt::eWidth) * mWidthIncrementFactor;
+ return std::max(resized, 1);
+}
+
+int32_t HTMLEditor::GetNewResizingHeight(int32_t aX, int32_t aY) {
+ int32_t resized = mResizedObjectHeight +
+ GetNewResizingIncrement(aX, aY, ResizeAt::eHeight) *
+ mHeightIncrementFactor;
+ return std::max(resized, 1);
+}
+
+nsresult HTMLEditor::UpdateResizerOrGrabberPositionTo(
+ const CSSIntPoint& aClientPoint) {
+ if (mIsResizing) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eResizingElement);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // we are resizing and the mouse pointer's position has changed
+ // we have to resdisplay the shadow
+ const int32_t newX = GetNewResizingX(aClientPoint.x, aClientPoint.y);
+ const int32_t newY = GetNewResizingY(aClientPoint.x, aClientPoint.y);
+ const int32_t newWidth =
+ GetNewResizingWidth(aClientPoint.x, aClientPoint.y);
+ const int32_t newHeight =
+ GetNewResizingHeight(aClientPoint.x, aClientPoint.y);
+
+ if (RefPtr<nsStyledElement> resizingShadowStyledElement =
+ nsStyledElement::FromNodeOrNull(mResizingShadow.get())) {
+ nsresult rv;
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *resizingShadowStyledElement, *nsGkAtoms::left, newX);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left)"
+ " destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left) "
+ "failed, but ignored");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *resizingShadowStyledElement, *nsGkAtoms::top, newY);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top)"
+ " destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "failed, but ignored");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *resizingShadowStyledElement, *nsGkAtoms::width, newWidth);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "width) destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::width) "
+ "failed, but ignored");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *resizingShadowStyledElement, *nsGkAtoms::height, newHeight);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "height) destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::height)"
+ " failed, but ignored");
+ }
+
+ nsresult rv = SetResizingInfoPosition(newX, newY, newWidth, newHeight);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetResizingInfoPosition() failed");
+ return rv;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eMovingElement);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (mGrabberClicked) {
+ int32_t xThreshold =
+ LookAndFeel::GetInt(LookAndFeel::IntID::DragThresholdX, 1);
+ int32_t yThreshold =
+ LookAndFeel::GetInt(LookAndFeel::IntID::DragThresholdY, 1);
+
+ if (DeprecatedAbs(aClientPoint.x - mOriginalX) * 2 >= xThreshold ||
+ DeprecatedAbs(aClientPoint.y - mOriginalY) * 2 >= yThreshold) {
+ mGrabberClicked = false;
+ DebugOnly<nsresult> rvIgnored = StartMoving();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::StartMoving() failed, but ignored");
+ }
+ }
+ if (mIsMoving) {
+ int32_t newX = mPositionedObjectX + aClientPoint.x - mOriginalX;
+ int32_t newY = mPositionedObjectY + aClientPoint.y - mOriginalY;
+
+ // Maybe align newX and newY to the grid.
+ SnapToGrid(newX, newY);
+
+ if (RefPtr<nsStyledElement> positioningShadowStyledElement =
+ nsStyledElement::FromNodeOrNull(mPositioningShadow.get())) {
+ nsresult rv;
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *positioningShadowStyledElement, *nsGkAtoms::left, newX);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left)"
+ " destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left) "
+ "failed, but ignored");
+ rv = CSSEditUtils::SetCSSPropertyPixelsWithoutTransaction(
+ *positioningShadowStyledElement, *nsGkAtoms::top, newY);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "failed, but ignored");
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::SetFinalSizeWithTransaction(int32_t aX, int32_t aY) {
+ if (!mResizedObject) {
+ // paranoia
+ return NS_OK;
+ }
+
+ if (mActivatedHandle) {
+ DebugOnly<nsresult> rvIgnored = mActivatedHandle->UnsetAttr(
+ kNameSpaceID_None, nsGkAtoms::_moz_activated, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::UnsetAttr(nsGkAtoms::_moz_activated) failed, but ignored");
+ mActivatedHandle = nullptr;
+ }
+
+ // we have now to set the new width and height of the resized object
+ // we don't set the x and y position because we don't control that in
+ // a normal HTML layout
+ int32_t left = GetNewResizingX(aX, aY);
+ int32_t top = GetNewResizingY(aX, aY);
+ int32_t width = GetNewResizingWidth(aX, aY);
+ int32_t height = GetNewResizingHeight(aX, aY);
+ bool setWidth =
+ !mResizedObjectIsAbsolutelyPositioned || (width != mResizedObjectWidth);
+ bool setHeight =
+ !mResizedObjectIsAbsolutelyPositioned || (height != mResizedObjectHeight);
+
+ int32_t x, y;
+ x = left - ((mResizedObjectIsAbsolutelyPositioned)
+ ? mResizedObjectBorderLeft + mResizedObjectMarginLeft
+ : 0);
+ y = top - ((mResizedObjectIsAbsolutelyPositioned)
+ ? mResizedObjectBorderTop + mResizedObjectMarginTop
+ : 0);
+
+ // we want one transaction only from a user's point of view
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ RefPtr<Element> resizedElement(mResizedObject);
+ RefPtr<nsStyledElement> resizedStyleElement =
+ nsStyledElement::FromNodeOrNull(mResizedObject);
+
+ if (mResizedObjectIsAbsolutelyPositioned && resizedStyleElement) {
+ if (setHeight) {
+ nsresult rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(
+ *this, *resizedStyleElement, *nsGkAtoms::top, y);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "top) destoyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::top) "
+ "failed, but ignored");
+ }
+ if (setWidth) {
+ nsresult rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(
+ *this, *resizedStyleElement, *nsGkAtoms::left, x);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "left) destoyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::left) "
+ "failed, but ignored");
+ }
+ }
+ if (IsCSSEnabled() || mResizedObjectIsAbsolutelyPositioned) {
+ if (setWidth && resizedElement->HasAttr(nsGkAtoms::width)) {
+ nsresult rv =
+ RemoveAttributeWithTransaction(*resizedElement, *nsGkAtoms::width);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::width) "
+ "failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::width) "
+ "failed, but ignored");
+ }
+
+ if (setHeight && resizedElement->HasAttr(nsGkAtoms::height)) {
+ nsresult rv =
+ RemoveAttributeWithTransaction(*resizedElement, *nsGkAtoms::height);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::height) "
+ "failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::height) "
+ "failed, but ignored");
+ }
+
+ if (resizedStyleElement) {
+ if (setWidth) {
+ nsresult rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(
+ *this, *resizedStyleElement, *nsGkAtoms::width, width);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "width) destoyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixels(nsGkAtoms::width) "
+ "failed, but ignored");
+ }
+ if (setHeight) {
+ nsresult rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(
+ *this, *resizedStyleElement, *nsGkAtoms::height, height);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "height) destoyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixels(nsGkAtoms::height) "
+ "failed, but ignored");
+ }
+ }
+ } else {
+ // we use HTML size and remove all equivalent CSS properties
+
+ // we set the CSS width and height to remove it later,
+ // triggering an immediate reflow; otherwise, we have problems
+ // with asynchronous reflow
+ if (resizedStyleElement) {
+ if (setWidth) {
+ nsresult rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(
+ *this, *resizedStyleElement, *nsGkAtoms::width, width);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "width) destoyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "width) failed, but ignored");
+ }
+ if (setHeight) {
+ nsresult rv = CSSEditUtils::SetCSSPropertyPixelsWithTransaction(
+ *this, *resizedStyleElement, *nsGkAtoms::height, height);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "height) destoyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyPixelsWithTransaction(nsGkAtoms::"
+ "height) failed, but ignored");
+ }
+ }
+ if (setWidth) {
+ nsAutoString w;
+ w.AppendInt(width);
+ DebugOnly<nsresult> rvIgnored =
+ SetAttributeWithTransaction(*resizedElement, *nsGkAtoms::width, w);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::width) "
+ "failed, but ignored");
+ }
+ if (setHeight) {
+ nsAutoString h;
+ h.AppendInt(height);
+ DebugOnly<nsresult> rvIgnored =
+ SetAttributeWithTransaction(*resizedElement, *nsGkAtoms::height, h);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::height) "
+ "failed, but ignored");
+ }
+
+ if (resizedStyleElement) {
+ if (setWidth) {
+ nsresult rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, *resizedStyleElement, *nsGkAtoms::width, u""_ns);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::width)"
+ " destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::width) "
+ "failed, but ignored");
+ }
+ if (setHeight) {
+ nsresult rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, *resizedStyleElement, *nsGkAtoms::height, u""_ns);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::"
+ "height) destroyed the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction(nsGkAtoms::height) "
+ "failed, but ignored");
+ }
+ }
+ }
+
+ // keep track of that size
+ mResizedObjectWidth = width;
+ mResizedObjectHeight = height;
+
+ nsresult rv = RefreshResizersInternal();
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::RefreshResizersInternal() failed, but ignored");
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetObjectResizingEnabled(
+ bool* aIsObjectResizingEnabled) {
+ *aIsObjectResizingEnabled = IsObjectResizerEnabled();
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::SetObjectResizingEnabled(
+ bool aObjectResizingEnabled) {
+ EnableObjectResizer(aObjectResizingEnabled);
+ return NS_OK;
+}
+
+#undef kTopLeft
+#undef kTop
+#undef kTopRight
+#undef kLeft
+#undef kRight
+#undef kBottomLeft
+#undef kBottom
+#undef kBottomRight
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLEditorState.cpp b/editor/libeditor/HTMLEditorState.cpp
new file mode 100644
index 0000000000..bfd496add5
--- /dev/null
+++ b/editor/libeditor/HTMLEditorState.cpp
@@ -0,0 +1,705 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditor.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "AutoRangeArray.h"
+#include "CSSEditUtils.h"
+#include "EditAction.h"
+#include "EditorUtils.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditUtils.h"
+#include "WSRunObject.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Selection.h"
+
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsAtom.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsRange.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+// NOTE: This file was split from:
+// https://searchfox.org/mozilla-central/rev/c409dd9235c133ab41eba635f906aa16e050c197/editor/libeditor/HTMLEditSubActionHandler.cpp
+
+namespace mozilla {
+
+using EditorType = EditorUtils::EditorType;
+using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
+
+/*****************************************************************************
+ * ListElementSelectionState
+ ****************************************************************************/
+
+ListElementSelectionState::ListElementSelectionState(HTMLEditor& aHTMLEditor,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(!aRv.Failed());
+
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ // XXX Should we create another constructor which won't create
+ // AutoEditActionDataSetter? Or should we create another
+ // AutoEditActionDataSetter which won't nest edit action?
+ EditorBase::AutoEditActionDataSetter editActionData(aHTMLEditor,
+ EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ Element* editingHostOrRoot = aHTMLEditor.ComputeEditingHost();
+ if (!editingHostOrRoot) {
+ // This is not a handler of editing command so that if there is no active
+ // editing host, let's use the <body> or document element instead.
+ editingHostOrRoot = aHTMLEditor.GetRoot();
+ if (!editingHostOrRoot) {
+ return;
+ }
+ }
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedSelectionRanges(aHTMLEditor.SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ EditSubAction::eCreateOrChangeList,
+ BlockInlineCheck::UseHTMLDefaultStyle, *editingHostOrRoot);
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ aHTMLEditor, arrayOfContents, EditSubAction::eCreateOrChangeList,
+ AutoRangeArray::CollectNonEditableNodes::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
+ aRv = EditorBase::ToGenericNSResult(rv);
+ return;
+ }
+ }
+
+ // Examine list type for nodes in selection.
+ for (const auto& content : arrayOfContents) {
+ if (!content->IsElement()) {
+ mIsOtherContentSelected = true;
+ } else if (content->IsHTMLElement(nsGkAtoms::ul)) {
+ mIsULElementSelected = true;
+ } else if (content->IsHTMLElement(nsGkAtoms::ol)) {
+ mIsOLElementSelected = true;
+ } else if (content->IsHTMLElement(nsGkAtoms::li)) {
+ if (dom::Element* parent = content->GetParentElement()) {
+ if (parent->IsHTMLElement(nsGkAtoms::ul)) {
+ mIsULElementSelected = true;
+ } else if (parent->IsHTMLElement(nsGkAtoms::ol)) {
+ mIsOLElementSelected = true;
+ }
+ }
+ } else if (content->IsAnyOfHTMLElements(nsGkAtoms::dl, nsGkAtoms::dt,
+ nsGkAtoms::dd)) {
+ mIsDLElementSelected = true;
+ } else {
+ mIsOtherContentSelected = true;
+ }
+
+ if (mIsULElementSelected && mIsOLElementSelected && mIsDLElementSelected &&
+ mIsOtherContentSelected) {
+ break;
+ }
+ }
+}
+
+/*****************************************************************************
+ * ListItemElementSelectionState
+ ****************************************************************************/
+
+ListItemElementSelectionState::ListItemElementSelectionState(
+ HTMLEditor& aHTMLEditor, ErrorResult& aRv) {
+ MOZ_ASSERT(!aRv.Failed());
+
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ // XXX Should we create another constructor which won't create
+ // AutoEditActionDataSetter? Or should we create another
+ // AutoEditActionDataSetter which won't nest edit action?
+ EditorBase::AutoEditActionDataSetter editActionData(aHTMLEditor,
+ EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ Element* editingHostOrRoot = aHTMLEditor.ComputeEditingHost();
+ if (!editingHostOrRoot) {
+ // This is not a handler of editing command so that if there is no active
+ // editing host, let's use the <body> or document element instead.
+ editingHostOrRoot = aHTMLEditor.GetRoot();
+ if (!editingHostOrRoot) {
+ return;
+ }
+ }
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedSelectionRanges(aHTMLEditor.SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ EditSubAction::eCreateOrChangeList,
+ BlockInlineCheck::UseHTMLDefaultStyle, *editingHostOrRoot);
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ aHTMLEditor, arrayOfContents, EditSubAction::eCreateOrChangeList,
+ AutoRangeArray::CollectNonEditableNodes::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
+ aRv = EditorBase::ToGenericNSResult(rv);
+ return;
+ }
+ }
+
+ // examine list type for nodes in selection
+ for (const auto& content : arrayOfContents) {
+ if (!content->IsElement()) {
+ mIsOtherElementSelected = true;
+ } else if (content->IsAnyOfHTMLElements(nsGkAtoms::ul, nsGkAtoms::ol,
+ nsGkAtoms::li)) {
+ mIsLIElementSelected = true;
+ } else if (content->IsHTMLElement(nsGkAtoms::dt)) {
+ mIsDTElementSelected = true;
+ } else if (content->IsHTMLElement(nsGkAtoms::dd)) {
+ mIsDDElementSelected = true;
+ } else if (content->IsHTMLElement(nsGkAtoms::dl)) {
+ if (mIsDTElementSelected && mIsDDElementSelected) {
+ continue;
+ }
+ // need to look inside dl and see which types of items it has
+ DefinitionListItemScanner scanner(*content->AsElement());
+ mIsDTElementSelected |= scanner.DTElementFound();
+ mIsDDElementSelected |= scanner.DDElementFound();
+ } else {
+ mIsOtherElementSelected = true;
+ }
+
+ if (mIsLIElementSelected && mIsDTElementSelected && mIsDDElementSelected &&
+ mIsOtherElementSelected) {
+ break;
+ }
+ }
+}
+
+/*****************************************************************************
+ * AlignStateAtSelection
+ ****************************************************************************/
+
+AlignStateAtSelection::AlignStateAtSelection(HTMLEditor& aHTMLEditor,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(!aRv.Failed());
+
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ // XXX Should we create another constructor which won't create
+ // AutoEditActionDataSetter? Or should we create another
+ // AutoEditActionDataSetter which won't nest edit action?
+ EditorBase::AutoEditActionDataSetter editActionData(aHTMLEditor,
+ EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ if (aHTMLEditor.IsSelectionRangeContainerNotContent()) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return;
+ }
+
+ // For now, just return first alignment. We don't check if it's mixed.
+ // This is for efficiency given that our current UI doesn't care if it's
+ // mixed.
+ // cmanske: NOT TRUE! We would like to pay attention to mixed state in
+ // [Format] -> [Align] submenu!
+
+ // This routine assumes that alignment is done ONLY by `<div>` elements
+ // if aHTMLEditor is not in CSS mode.
+
+ if (NS_WARN_IF(!aHTMLEditor.GetRoot())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ OwningNonNull<dom::Element> bodyOrDocumentElement = *aHTMLEditor.GetRoot();
+ EditorRawDOMPoint atBodyOrDocumentElement(bodyOrDocumentElement);
+
+ const nsRange* firstRange = aHTMLEditor.SelectionRef().GetRangeAt(0);
+ mFoundSelectionRanges = !!firstRange;
+ if (!mFoundSelectionRanges) {
+ NS_WARNING("There was no selection range");
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ EditorRawDOMPoint atStartOfSelection(firstRange->StartRef());
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
+
+ nsIContent* editTargetContent = nullptr;
+ // If selection is collapsed or in a text node, take the container.
+ if (aHTMLEditor.SelectionRef().IsCollapsed() ||
+ atStartOfSelection.IsInTextNode()) {
+ editTargetContent = atStartOfSelection.GetContainerAs<nsIContent>();
+ if (NS_WARN_IF(!editTargetContent)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+ // If selection container is the `<body>` element which is set to
+ // `HTMLDocument.body`, take first editable node in it.
+ // XXX Why don't we just compare `atStartOfSelection.GetChild()` and
+ // `bodyOrDocumentElement`? Then, we can avoid computing the
+ // offset.
+ else if (atStartOfSelection.IsContainerHTMLElement(nsGkAtoms::html) &&
+ atBodyOrDocumentElement.IsSet() &&
+ atStartOfSelection.Offset() == atBodyOrDocumentElement.Offset()) {
+ editTargetContent = HTMLEditUtils::GetNextContent(
+ atStartOfSelection, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, aHTMLEditor.ComputeEditingHost());
+ if (NS_WARN_IF(!editTargetContent)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+ // Otherwise, use first selected node.
+ // XXX Only for retrieving it, the following block treats all selected
+ // ranges. `HTMLEditor` should have
+ // `GetFirstSelectionRangeExtendedToHardLineStartAndEnd()`.
+ else {
+ Element* editingHostOrRoot = aHTMLEditor.ComputeEditingHost();
+ if (!editingHostOrRoot) {
+ // This is not a handler of editing command so that if there is no active
+ // editing host, let's use the <body> or document element instead.
+ editingHostOrRoot = aHTMLEditor.GetRoot();
+ if (!editingHostOrRoot) {
+ return;
+ }
+ }
+ AutoRangeArray extendedSelectionRanges(aHTMLEditor.SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ EditSubAction::eSetOrClearAlignment,
+ BlockInlineCheck::UseHTMLDefaultStyle, *editingHostOrRoot);
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ aHTMLEditor, arrayOfContents, EditSubAction::eSetOrClearAlignment,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(eSetOrClearAlignment, "
+ "CollectNonEditableNodes::Yes) failed");
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ if (arrayOfContents.IsEmpty()) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(eSetOrClearAlignment, "
+ "CollectNonEditableNodes::Yes) returned no contents");
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ editTargetContent = arrayOfContents[0];
+ }
+
+ const RefPtr<dom::Element> maybeNonEditableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *editTargetContent, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseHTMLDefaultStyle);
+ if (NS_WARN_IF(!maybeNonEditableBlockElement)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ if (aHTMLEditor.IsCSSEnabled() && EditorElementStyle::Align().IsCSSSettable(
+ *maybeNonEditableBlockElement)) {
+ // We are in CSS mode and we know how to align this element with CSS
+ nsAutoString value;
+ // Let's get the value(s) of text-align or margin-left/margin-right
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetComputedCSSEquivalentTo(
+ *maybeNonEditableBlockElement, EditorElementStyle::Align(), value);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetComputedCSSEquivalentTo("
+ "EditorElementStyle::Align()) failed, but ignored");
+ if (value.EqualsLiteral(u"center") || value.EqualsLiteral(u"-moz-center") ||
+ value.EqualsLiteral(u"auto auto")) {
+ mFirstAlign = nsIHTMLEditor::eCenter;
+ return;
+ }
+ if (value.EqualsLiteral(u"right") || value.EqualsLiteral(u"-moz-right") ||
+ value.EqualsLiteral(u"auto 0px")) {
+ mFirstAlign = nsIHTMLEditor::eRight;
+ return;
+ }
+ if (value.EqualsLiteral(u"justify")) {
+ mFirstAlign = nsIHTMLEditor::eJustify;
+ return;
+ }
+ // XXX In RTL document, is this expected?
+ mFirstAlign = nsIHTMLEditor::eLeft;
+ return;
+ }
+
+ for (Element* const containerElement :
+ editTargetContent->InclusiveAncestorsOfType<Element>()) {
+ // If the node is a parent `<table>` element of edit target, let's break
+ // here to materialize the 'inline-block' behaviour of html tables
+ // regarding to text alignment.
+ if (containerElement != editTargetContent &&
+ containerElement->IsHTMLElement(nsGkAtoms::table)) {
+ return;
+ }
+
+ if (EditorElementStyle::Align().IsCSSSettable(*containerElement)) {
+ nsAutoString value;
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetSpecifiedProperty(
+ *containerElement, *nsGkAtoms::textAlign, value);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetSpecifiedProperty(nsGkAtoms::"
+ "textAlign) failed, but ignored");
+ if (!value.IsEmpty()) {
+ if (value.EqualsLiteral("center")) {
+ mFirstAlign = nsIHTMLEditor::eCenter;
+ return;
+ }
+ if (value.EqualsLiteral("right")) {
+ mFirstAlign = nsIHTMLEditor::eRight;
+ return;
+ }
+ if (value.EqualsLiteral("justify")) {
+ mFirstAlign = nsIHTMLEditor::eJustify;
+ return;
+ }
+ if (value.EqualsLiteral("left")) {
+ mFirstAlign = nsIHTMLEditor::eLeft;
+ return;
+ }
+ // XXX
+ // text-align: start and end aren't supported yet
+ }
+ }
+
+ if (!HTMLEditUtils::SupportsAlignAttr(*containerElement)) {
+ continue;
+ }
+
+ nsAutoString alignAttributeValue;
+ containerElement->GetAttr(nsGkAtoms::align, alignAttributeValue);
+ if (alignAttributeValue.IsEmpty()) {
+ continue;
+ }
+
+ if (alignAttributeValue.LowerCaseEqualsASCII("center")) {
+ mFirstAlign = nsIHTMLEditor::eCenter;
+ return;
+ }
+ if (alignAttributeValue.LowerCaseEqualsASCII("right")) {
+ mFirstAlign = nsIHTMLEditor::eRight;
+ return;
+ }
+ // XXX This is odd case. `<div align="justify">` is not in any standards.
+ if (alignAttributeValue.LowerCaseEqualsASCII("justify")) {
+ mFirstAlign = nsIHTMLEditor::eJustify;
+ return;
+ }
+ // XXX In RTL document, is this expected?
+ mFirstAlign = nsIHTMLEditor::eLeft;
+ return;
+ }
+}
+
+/*****************************************************************************
+ * ParagraphStateAtSelection
+ ****************************************************************************/
+
+ParagraphStateAtSelection::ParagraphStateAtSelection(
+ HTMLEditor& aHTMLEditor, FormatBlockMode aFormatBlockMode,
+ ErrorResult& aRv) {
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ // XXX Should we create another constructor which won't create
+ // AutoEditActionDataSetter? Or should we create another
+ // AutoEditActionDataSetter which won't nest edit action?
+ EditorBase::AutoEditActionDataSetter editActionData(aHTMLEditor,
+ EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ if (aHTMLEditor.IsSelectionRangeContainerNotContent()) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return;
+ }
+
+ if (MOZ_UNLIKELY(!aHTMLEditor.SelectionRef().RangeCount())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ const Element* const editingHostOrBodyOrDocumentElement = [&]() -> Element* {
+ if (Element* editingHost = aHTMLEditor.ComputeEditingHost()) {
+ return editingHost;
+ }
+ return aHTMLEditor.GetRoot();
+ }();
+ if (!editingHostOrBodyOrDocumentElement ||
+ !HTMLEditUtils::IsSimplyEditableNode(
+ *editingHostOrBodyOrDocumentElement)) {
+ return;
+ }
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ nsresult rv = CollectEditableFormatNodesInSelection(
+ aHTMLEditor, aFormatBlockMode, *editingHostOrBodyOrDocumentElement,
+ arrayOfContents);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "ParagraphStateAtSelection::CollectEditableFormatNodesInSelection() "
+ "failed");
+ aRv.Throw(rv);
+ return;
+ }
+
+ // We need to append descendant format block if block nodes are not format
+ // block. This is so we only have to look "up" the hierarchy to find
+ // format nodes, instead of both up and down.
+ for (size_t index : Reversed(IntegerRange(arrayOfContents.Length()))) {
+ OwningNonNull<nsIContent>& content = arrayOfContents[index];
+ if (HTMLEditUtils::IsBlockElement(content,
+ BlockInlineCheck::UseHTMLDefaultStyle) &&
+ !HTMLEditor::IsFormatElement(aFormatBlockMode, content)) {
+ // XXX This RemoveObject() call has already been commented out and
+ // the above comment explained we're trying to replace non-format
+ // block nodes in the array. According to the following blocks and
+ // `AppendDescendantFormatNodesAndFirstInlineNode()`, replacing
+ // non-format block with descendants format blocks makes sense.
+ // arrayOfContents.RemoveObject(node);
+ ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
+ arrayOfContents, aFormatBlockMode, *content->AsElement());
+ }
+ }
+
+ // We might have an empty node list. if so, find selection parent
+ // and put that on the list
+ if (arrayOfContents.IsEmpty()) {
+ const auto atCaret =
+ aHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!atCaret.IsInContentNode())) {
+ MOZ_ASSERT(false,
+ "We've already checked whether there is a selection range, "
+ "but we have no range right now.");
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ arrayOfContents.AppendElement(*atCaret.ContainerAs<nsIContent>());
+ }
+
+ for (auto& content : Reversed(arrayOfContents)) {
+ const Element* formatElement = nullptr;
+ if (HTMLEditor::IsFormatElement(aFormatBlockMode, content)) {
+ formatElement = content->AsElement();
+ }
+ // Ignore inline contents since its children have been appended
+ // the list above so that we'll handle this descendants later.
+ // XXX: It's odd to ignore block children to consider the mixed state.
+ else if (HTMLEditUtils::IsBlockElement(
+ content, BlockInlineCheck::UseHTMLDefaultStyle)) {
+ continue;
+ }
+ // If we meet an inline node, let's get its parent format.
+ else {
+ for (Element* parentElement : content->AncestorsOfType<Element>()) {
+ // If we reach `HTMLDocument.body` or `Document.documentElement`,
+ // there is no format.
+ if (parentElement == editingHostOrBodyOrDocumentElement) {
+ break;
+ }
+ if (HTMLEditor::IsFormatElement(aFormatBlockMode, *parentElement)) {
+ MOZ_ASSERT(parentElement->NodeInfo()->NameAtom());
+ formatElement = parentElement;
+ break;
+ }
+ }
+ }
+
+ auto FormatElementIsInclusiveDescendantOfFormatDLElement = [&]() {
+ if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand) {
+ return false;
+ }
+ if (!formatElement) {
+ return false;
+ }
+ for (const Element* const element :
+ formatElement->InclusiveAncestorsOfType<Element>()) {
+ if (element->IsHTMLElement(nsGkAtoms::dl)) {
+ return true;
+ }
+ if (element->IsAnyOfHTMLElements(nsGkAtoms::dd, nsGkAtoms::dt)) {
+ continue;
+ }
+ if (HTMLEditUtils::IsFormatElementForFormatBlockCommand(
+ *formatElement)) {
+ return false;
+ }
+ }
+ return false;
+ };
+
+ // if this is the first node, we've found, remember it as the format
+ if (!mFirstParagraphState) {
+ mFirstParagraphState = formatElement
+ ? formatElement->NodeInfo()->NameAtom()
+ : nsGkAtoms::_empty;
+ mIsInDLElement = FormatElementIsInclusiveDescendantOfFormatDLElement();
+ continue;
+ }
+ mIsInDLElement &= FormatElementIsInclusiveDescendantOfFormatDLElement();
+ // else make sure it matches previously found format
+ if ((!formatElement && mFirstParagraphState != nsGkAtoms::_empty) ||
+ (formatElement &&
+ !formatElement->IsHTMLElement(mFirstParagraphState))) {
+ mIsMixed = true;
+ break;
+ }
+ }
+}
+
+// static
+void ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ FormatBlockMode aFormatBlockMode, dom::Element& aNonFormatBlockElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsBlockElement(
+ aNonFormatBlockElement, BlockInlineCheck::UseHTMLDefaultStyle));
+ MOZ_ASSERT(
+ !HTMLEditor::IsFormatElement(aFormatBlockMode, aNonFormatBlockElement));
+
+ // We only need to place any one inline inside this node onto
+ // the list. They are all the same for purposes of determining
+ // paragraph style. We use foundInline to track this as we are
+ // going through the children in the loop below.
+ bool foundInline = false;
+ for (nsIContent* childContent = aNonFormatBlockElement.GetFirstChild();
+ childContent; childContent = childContent->GetNextSibling()) {
+ const bool isBlock = HTMLEditUtils::IsBlockElement(
+ *childContent, BlockInlineCheck::UseHTMLDefaultStyle);
+ const bool isFormat =
+ HTMLEditor::IsFormatElement(aFormatBlockMode, *childContent);
+ // If the child is a non-format block element, let's check its children
+ // recursively.
+ if (isBlock && !isFormat) {
+ ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
+ aArrayOfContents, aFormatBlockMode, *childContent->AsElement());
+ continue;
+ }
+
+ // If it's a format block, append it.
+ if (isFormat) {
+ aArrayOfContents.AppendElement(*childContent);
+ continue;
+ }
+
+ MOZ_ASSERT(!isBlock);
+
+ // If we haven't found inline node, append only this first inline node.
+ // XXX I think that this makes sense if caller of this removes
+ // aNonFormatBlockElement from aArrayOfContents because the last loop
+ // of the constructor can check parent format block with
+ // aNonFormatBlockElement.
+ if (!foundInline) {
+ foundInline = true;
+ aArrayOfContents.AppendElement(*childContent);
+ continue;
+ }
+ }
+}
+
+// static
+nsresult ParagraphStateAtSelection::CollectEditableFormatNodesInSelection(
+ HTMLEditor& aHTMLEditor, FormatBlockMode aFormatBlockMode,
+ const Element& aEditingHost,
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents) {
+ {
+ AutoRangeArray extendedSelectionRanges(aHTMLEditor.SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
+ ? EditSubAction::eFormatBlockForHTMLCommand
+ : EditSubAction::eCreateOrRemoveBlock,
+ BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ aHTMLEditor, aArrayOfContents,
+ aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
+ ? EditSubAction::eFormatBlockForHTMLCommand
+ : EditSubAction::eCreateOrRemoveBlock,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes("
+ "CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+
+ // Pre-process our list of nodes
+ for (size_t index : Reversed(IntegerRange(aArrayOfContents.Length()))) {
+ OwningNonNull<nsIContent> content = aArrayOfContents[index];
+
+ // Remove all non-editable nodes. Leave them be.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ aArrayOfContents.RemoveElementAt(index);
+ continue;
+ }
+
+ // Scan for table elements. If we find table elements other than table,
+ // replace it with a list of any editable non-table content. Ditto for
+ // list elements.
+ if (HTMLEditUtils::IsAnyTableElement(content) ||
+ HTMLEditUtils::IsAnyListElement(content) ||
+ HTMLEditUtils::IsListItem(content)) {
+ aArrayOfContents.RemoveElementAt(index);
+ HTMLEditUtils::CollectChildren(
+ content, aArrayOfContents, index,
+ {CollectChildrenOption::CollectListChildren,
+ CollectChildrenOption::CollectTableChildren});
+ }
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLInlineTableEditor.cpp b/editor/libeditor/HTMLInlineTableEditor.cpp
new file mode 100644
index 0000000000..271a60bf12
--- /dev/null
+++ b/editor/libeditor/HTMLInlineTableEditor.cpp
@@ -0,0 +1,493 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/HTMLEditor.h"
+
+#include "EditorEventListener.h"
+#include "HTMLEditUtils.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Element.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGenericHTMLElement.h"
+#include "nsIContent.h"
+#include "nsLiteralString.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+NS_IMETHODIMP HTMLEditor::SetInlineTableEditingEnabled(bool aIsEnabled) {
+ EnableInlineTableEditor(aIsEnabled);
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetInlineTableEditingEnabled(bool* aIsEnabled) {
+ *aIsEnabled = IsInlineTableEditorEnabled();
+ return NS_OK;
+}
+
+nsresult HTMLEditor::ShowInlineTableEditingUIInternal(Element& aCellElement) {
+ if (NS_WARN_IF(!HTMLEditUtils::IsTableCell(&aCellElement))) {
+ return NS_OK;
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost) ||
+ NS_WARN_IF(!aCellElement.IsInclusiveDescendantOf(editingHost))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(mInlineEditedCell)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mInlineEditedCell = &aCellElement;
+
+ // the resizers and the shadow will be anonymous children of the body
+ RefPtr<Element> rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ do {
+ // The buttons of inline table editor will be children of the <body>
+ // element. Creating the anonymous elements may cause calling
+ // HideInlineTableEditingUIInternal() via a mutation event listener.
+ // So, we should store new button to a local variable, then, check:
+ // - whether creating a button is already set to the member or not
+ // - whether already created buttons are changed to another set
+ // If creating the buttons are canceled, we hit the latter check.
+ // If buttons for another table are created during this, we hit the latter
+ // check too.
+ // If buttons are just created again for same element, we hit the former
+ // check.
+ ManualNACPtr addColumnBeforeButton = CreateAnonymousElement(
+ nsGkAtoms::a, *rootElement, u"mozTableAddColumnBefore"_ns, false);
+ if (NS_WARN_IF(!addColumnBeforeButton)) {
+ NS_WARNING(
+ "HTMLEditor::CreateAnonymousElement(nsGkAtoms::a, "
+ "mozTableAddColumnBefore) failed");
+ break; // Hide unnecessary buttons created above.
+ }
+ if (NS_WARN_IF(mAddColumnBeforeButton) ||
+ NS_WARN_IF(mInlineEditedCell != &aCellElement)) {
+ return NS_ERROR_FAILURE; // Don't hide another set of buttons.
+ }
+ mAddColumnBeforeButton = std::move(addColumnBeforeButton);
+
+ ManualNACPtr removeColumnButton = CreateAnonymousElement(
+ nsGkAtoms::a, *rootElement, u"mozTableRemoveColumn"_ns, false);
+ if (!removeColumnButton) {
+ NS_WARNING(
+ "HTMLEditor::CreateAnonymousElement(nsGkAtoms::a, "
+ "mozTableRemoveColumn) failed");
+ break;
+ }
+ if (NS_WARN_IF(mRemoveColumnButton) ||
+ NS_WARN_IF(mInlineEditedCell != &aCellElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mRemoveColumnButton = std::move(removeColumnButton);
+
+ ManualNACPtr addColumnAfterButton = CreateAnonymousElement(
+ nsGkAtoms::a, *rootElement, u"mozTableAddColumnAfter"_ns, false);
+ if (!addColumnAfterButton) {
+ NS_WARNING(
+ "HTMLEditor::CreateAnonymousElement(nsGkAtoms::a, "
+ "mozTableAddColumnAfter) failed");
+ break;
+ }
+ if (NS_WARN_IF(mAddColumnAfterButton) ||
+ NS_WARN_IF(mInlineEditedCell != &aCellElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mAddColumnAfterButton = std::move(addColumnAfterButton);
+
+ ManualNACPtr addRowBeforeButton = CreateAnonymousElement(
+ nsGkAtoms::a, *rootElement, u"mozTableAddRowBefore"_ns, false);
+ if (!addRowBeforeButton) {
+ NS_WARNING(
+ "HTMLEditor::CreateAnonymousElement(nsGkAtoms::a, "
+ "mozTableAddRowBefore) failed");
+ break;
+ }
+ if (NS_WARN_IF(mAddRowBeforeButton) ||
+ NS_WARN_IF(mInlineEditedCell != &aCellElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mAddRowBeforeButton = std::move(addRowBeforeButton);
+
+ ManualNACPtr removeRowButton = CreateAnonymousElement(
+ nsGkAtoms::a, *rootElement, u"mozTableRemoveRow"_ns, false);
+ if (!removeRowButton) {
+ NS_WARNING(
+ "HTMLEditor::CreateAnonymousElement(nsGkAtoms::a, "
+ "mozTableRemoveRow) failed");
+ break;
+ }
+ if (NS_WARN_IF(mRemoveRowButton) ||
+ NS_WARN_IF(mInlineEditedCell != &aCellElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mRemoveRowButton = std::move(removeRowButton);
+
+ ManualNACPtr addRowAfterButton = CreateAnonymousElement(
+ nsGkAtoms::a, *rootElement, u"mozTableAddRowAfter"_ns, false);
+ if (!addRowAfterButton) {
+ NS_WARNING(
+ "HTMLEditor::CreateAnonymousElement(nsGkAtoms::a, "
+ "mozTableAddRowAfter) failed");
+ break;
+ }
+ if (NS_WARN_IF(mAddRowAfterButton) ||
+ NS_WARN_IF(mInlineEditedCell != &aCellElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ mAddRowAfterButton = std::move(addRowAfterButton);
+
+ AddMouseClickListener(mAddColumnBeforeButton);
+ AddMouseClickListener(mRemoveColumnButton);
+ AddMouseClickListener(mAddColumnAfterButton);
+ AddMouseClickListener(mAddRowBeforeButton);
+ AddMouseClickListener(mRemoveRowButton);
+ AddMouseClickListener(mAddRowAfterButton);
+
+ nsresult rv = RefreshInlineTableEditingUIInternal();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::RefreshInlineTableEditingUIInternal() failed");
+ return rv;
+ } while (true);
+
+ HideInlineTableEditingUIInternal();
+ return NS_ERROR_FAILURE;
+}
+
+void HTMLEditor::HideInlineTableEditingUIInternal() {
+ mInlineEditedCell = nullptr;
+
+ RemoveMouseClickListener(mAddColumnBeforeButton);
+ RemoveMouseClickListener(mRemoveColumnButton);
+ RemoveMouseClickListener(mAddColumnAfterButton);
+ RemoveMouseClickListener(mAddRowBeforeButton);
+ RemoveMouseClickListener(mRemoveRowButton);
+ RemoveMouseClickListener(mAddRowAfterButton);
+
+ // get the presshell's document observer interface.
+ RefPtr<PresShell> presShell = GetPresShell();
+ // We allow the pres shell to be null; when it is, we presume there
+ // are no document observers to notify, but we still want to
+ // UnbindFromTree.
+
+ // Calling DeleteRefToAnonymousNode() may cause showing the UI again.
+ // Therefore, we should forget all anonymous contents first.
+ // Otherwise, we could leak the old content because of overwritten by
+ // ShowInlineTableEditingUIInternal().
+ ManualNACPtr addColumnBeforeButton(std::move(mAddColumnBeforeButton));
+ ManualNACPtr removeColumnButton(std::move(mRemoveColumnButton));
+ ManualNACPtr addColumnAfterButton(std::move(mAddColumnAfterButton));
+ ManualNACPtr addRowBeforeButton(std::move(mAddRowBeforeButton));
+ ManualNACPtr removeRowButton(std::move(mRemoveRowButton));
+ ManualNACPtr addRowAfterButton(std::move(mAddRowAfterButton));
+
+ DeleteRefToAnonymousNode(std::move(addColumnBeforeButton), presShell);
+ DeleteRefToAnonymousNode(std::move(removeColumnButton), presShell);
+ DeleteRefToAnonymousNode(std::move(addColumnAfterButton), presShell);
+ DeleteRefToAnonymousNode(std::move(addRowBeforeButton), presShell);
+ DeleteRefToAnonymousNode(std::move(removeRowButton), presShell);
+ DeleteRefToAnonymousNode(std::move(addRowAfterButton), presShell);
+}
+
+nsresult HTMLEditor::DoInlineTableEditingAction(const Element& aElement) {
+ nsAutoString anonclass;
+ aElement.GetAttr(nsGkAtoms::_moz_anonclass, anonclass);
+
+ if (!StringBeginsWith(anonclass, u"mozTable"_ns)) {
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!mInlineEditedCell) ||
+ NS_WARN_IF(!mInlineEditedCell->IsInComposedDoc()) ||
+ NS_WARN_IF(
+ !HTMLEditUtils::IsTableRow(mInlineEditedCell->GetParentNode()))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ RefPtr<Element> tableElement =
+ HTMLEditUtils::GetClosestAncestorTableElement(*mInlineEditedCell);
+ if (!tableElement) {
+ NS_WARNING("HTMLEditor::GetClosestAncestorTableElement() returned nullptr");
+ return NS_ERROR_FAILURE;
+ }
+ int32_t rowCount, colCount;
+ nsresult rv = GetTableSize(tableElement, &rowCount, &colCount);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetTableSize() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ bool hideUI = false;
+ bool hideResizersWithInlineTableUI = (mResizedObject == tableElement);
+
+ if (anonclass.EqualsLiteral("mozTableAddColumnBefore")) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertTableColumn);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ InsertTableColumnsWithTransaction(EditorDOMPoint(mInlineEditedCell), 1);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::InsertTableColumnsWithTransaction("
+ "EditorDOMPoint(mInlineEditedCell), 1) failed, but ignored");
+ } else if (anonclass.EqualsLiteral("mozTableAddColumnAfter")) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertTableColumn);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ Element* nextCellElement = nullptr;
+ for (nsIContent* maybeNextCellElement = mInlineEditedCell->GetNextSibling();
+ maybeNextCellElement;
+ maybeNextCellElement = maybeNextCellElement->GetNextSibling()) {
+ if (HTMLEditUtils::IsTableCell(maybeNextCellElement)) {
+ nextCellElement = maybeNextCellElement->AsElement();
+ break;
+ }
+ }
+ DebugOnly<nsresult> rvIgnored = InsertTableColumnsWithTransaction(
+ nextCellElement
+ ? EditorDOMPoint(nextCellElement)
+ : EditorDOMPoint::AtEndOf(*mInlineEditedCell->GetParentElement()),
+ 1);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::InsertTableColumnsWithTransaction("
+ "EditorDOMPoint(nextCellElement), 1) failed, but ignored");
+ } else if (anonclass.EqualsLiteral("mozTableAddRowBefore")) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertTableRowElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ OwningNonNull<Element> targetCellElement(*mInlineEditedCell);
+ DebugOnly<nsresult> rvIgnored = InsertTableRowsWithTransaction(
+ targetCellElement, 1, InsertPosition::eBeforeSelectedCell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::InsertTableRowsWithTransaction(targetCellElement, 1, "
+ "InsertPosition::eBeforeSelectedCell) failed, but ignored");
+ } else if (anonclass.EqualsLiteral("mozTableAddRowAfter")) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertTableRowElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ OwningNonNull<Element> targetCellElement(*mInlineEditedCell);
+ DebugOnly<nsresult> rvIgnored = InsertTableRowsWithTransaction(
+ targetCellElement, 1, InsertPosition::eAfterSelectedCell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::InsertTableRowsWithTransaction(targetCellElement, 1, "
+ "InsertPosition::eAfterSelectedCell) failed, but ignored");
+ } else if (anonclass.EqualsLiteral("mozTableRemoveColumn")) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eRemoveTableColumn);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ DebugOnly<nsresult> rvIgnored =
+ DeleteSelectedTableColumnsWithTransaction(1);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::DeleteSelectedTableColumnsWithTransaction(1) failed, but "
+ "ignored");
+ hideUI = (colCount == 1);
+ } else if (anonclass.EqualsLiteral("mozTableRemoveRow")) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eRemoveTableRowElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ DebugOnly<nsresult> rvIgnored = DeleteSelectedTableRowsWithTransaction(1);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::DeleteSelectedTableRowsWithTransaction(1)"
+ " failed, but ignored");
+ hideUI = (rowCount == 1);
+ } else {
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_OK;
+ }
+
+ if (hideUI) {
+ HideInlineTableEditingUIInternal();
+ if (hideResizersWithInlineTableUI) {
+ DebugOnly<nsresult> rvIgnored = HideResizersInternal();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::HideResizersInternal() failed, but ignored");
+ }
+ }
+
+ return NS_OK;
+}
+
+void HTMLEditor::AddMouseClickListener(Element* aElement) {
+ if (NS_WARN_IF(!aElement)) {
+ return;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ aElement->AddEventListener(u"click"_ns, mEventListener, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EventTarget::AddEventListener(click) failed, but ignored");
+}
+
+void HTMLEditor::RemoveMouseClickListener(Element* aElement) {
+ if (NS_WARN_IF(!aElement)) {
+ return;
+ }
+ aElement->RemoveEventListener(u"click"_ns, mEventListener, true);
+}
+
+nsresult HTMLEditor::RefreshInlineTableEditingUIInternal() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!mInlineEditedCell) {
+ return NS_OK;
+ }
+
+ RefPtr<nsGenericHTMLElement> inlineEditingCellElement =
+ nsGenericHTMLElement::FromNode(mInlineEditedCell);
+ if (NS_WARN_IF(!inlineEditingCellElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t cellX = 0, cellY = 0;
+ DebugOnly<nsresult> rvIgnored =
+ GetElementOrigin(*mInlineEditedCell, cellX, cellY);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::GetElementOrigin() failed, but ignored");
+
+ int32_t cellWidth = inlineEditingCellElement->OffsetWidth();
+ int32_t cellHeight = inlineEditingCellElement->OffsetHeight();
+
+ int32_t centerOfCellX = cellX + cellWidth / 2;
+ int32_t centerOfCellY = cellY + cellHeight / 2;
+
+ RefPtr<Element> tableElement =
+ HTMLEditUtils::GetClosestAncestorTableElement(*mInlineEditedCell);
+ if (!tableElement) {
+ NS_WARNING("HTMLEditor::GetClosestAncestorTableElement() returned nullptr");
+ return NS_ERROR_FAILURE;
+ }
+ int32_t rowCount = 0, colCount = 0;
+ nsresult rv = GetTableSize(tableElement, &rowCount, &colCount);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetTableSize() failed");
+ return rv;
+ }
+
+ auto setInlineTableEditButtonPosition =
+ [this](ManualNACPtr& aButtonElement, int32_t aNewX, int32_t aNewY)
+ MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> nsresult {
+ RefPtr<nsStyledElement> buttonStyledElement =
+ nsStyledElement::FromNodeOrNull(aButtonElement.get());
+ if (!buttonStyledElement) {
+ return NS_OK;
+ }
+ nsresult rv = SetAnonymousElementPositionWithoutTransaction(
+ *buttonStyledElement, aNewX, aNewY);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::SetAnonymousElementPositionWithoutTransaction() "
+ "failed");
+ return rv;
+ }
+ return NS_WARN_IF(buttonStyledElement != aButtonElement.get())
+ ? NS_ERROR_FAILURE
+ : NS_OK;
+ };
+
+ // clang-format off
+ rv = setInlineTableEditButtonPosition(mAddColumnBeforeButton,
+ centerOfCellX - 10, cellY - 7);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to move button for add-column-before");
+ return rv;
+ }
+ rv = setInlineTableEditButtonPosition(mRemoveColumnButton,
+ centerOfCellX - 4, cellY - 7);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to move button for remove-column");
+ return rv;
+ }
+ rv = setInlineTableEditButtonPosition(mAddColumnAfterButton,
+ centerOfCellX + 6, cellY - 7);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to move button for add-column-after");
+ return rv;
+ }
+ rv = setInlineTableEditButtonPosition(mAddRowBeforeButton,
+ cellX - 7, centerOfCellY - 10);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to move button for add-row-before");
+ return rv;
+ }
+ rv = setInlineTableEditButtonPosition(mRemoveRowButton,
+ cellX - 7, centerOfCellY - 4);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to move button for remove-row");
+ return rv;
+ }
+ rv = setInlineTableEditButtonPosition(mAddRowAfterButton,
+ cellX - 7, centerOfCellY + 6);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to move button for add-row-after");
+ return rv;
+ }
+ // clang-format on
+
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLStyleEditor.cpp b/editor/libeditor/HTMLStyleEditor.cpp
new file mode 100644
index 0000000000..2ca7b06c29
--- /dev/null
+++ b/editor/libeditor/HTMLStyleEditor.cpp
@@ -0,0 +1,4452 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ErrorList.h"
+#include "HTMLEditor.h"
+#include "HTMLEditorInlines.h"
+#include "HTMLEditorNestedClasses.h"
+
+#include "AutoRangeArray.h"
+#include "CSSEditUtils.h"
+#include "EditAction.h"
+#include "EditorUtils.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditUtils.h"
+#include "PendingStyles.h"
+#include "SelectionState.h"
+#include "WSRunObject.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/SelectionState.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/NameSpaceConstants.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Text.h"
+
+#include "nsAString.h"
+#include "nsAtom.h"
+#include "nsAttrName.h"
+#include "nsAttrValue.h"
+#include "nsCaseTreatment.h"
+#include "nsColor.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsIPrincipal.h"
+#include "nsISupportsImpl.h"
+#include "nsLiteralString.h"
+#include "nsNameSpaceManager.h"
+#include "nsRange.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStyledElement.h"
+#include "nsTArray.h"
+#include "nsTextNode.h"
+#include "nsUnicharUtils.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
+using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
+
+template nsresult HTMLEditor::SetInlinePropertiesAsSubAction(
+ const AutoTArray<EditorInlineStyleAndValue, 1>& aStylesToSet);
+template nsresult HTMLEditor::SetInlinePropertiesAsSubAction(
+ const AutoTArray<EditorInlineStyleAndValue, 32>& aStylesToSet);
+
+template nsresult HTMLEditor::SetInlinePropertiesAroundRanges(
+ AutoRangeArray& aRanges,
+ const AutoTArray<EditorInlineStyleAndValue, 1>& aStylesToSet,
+ const Element& aEditingHost);
+template nsresult HTMLEditor::SetInlinePropertiesAroundRanges(
+ AutoRangeArray& aRanges,
+ const AutoTArray<EditorInlineStyleAndValue, 32>& aStylesToSet,
+ const Element& aEditingHost);
+
+nsresult HTMLEditor::SetInlinePropertyAsAction(nsStaticAtom& aProperty,
+ nsStaticAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(
+ *this,
+ HTMLEditUtils::GetEditActionForFormatText(aProperty, aAttribute, true),
+ aPrincipal);
+ switch (editActionData.GetEditAction()) {
+ case EditAction::eSetFontFamilyProperty:
+ MOZ_ASSERT(!aValue.IsVoid());
+ // XXX Should we trim unnecessary white-spaces?
+ editActionData.SetData(aValue);
+ break;
+ case EditAction::eSetColorProperty:
+ case EditAction::eSetBackgroundColorPropertyInline:
+ editActionData.SetColorData(aValue);
+ break;
+ default:
+ break;
+ }
+
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // XXX Due to bug 1659276 and bug 1659924, we should not scroll selection
+ // into view after setting the new style.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, ScrollSelectionIntoView::No,
+ __FUNCTION__);
+
+ nsStaticAtom* property = &aProperty;
+ nsStaticAtom* attribute = aAttribute;
+ nsString value(aValue);
+ if (attribute == nsGkAtoms::color || attribute == nsGkAtoms::bgcolor) {
+ if (!IsCSSEnabled()) {
+ // We allow CSS style color value even in the HTML mode. In the cases,
+ // we will apply the style with CSS. For considering it in the value
+ // as-is if it's a known CSS keyboard, `rgb()` or `rgba()` style.
+ // NOTE: It may be later that we set the color into the DOM tree and at
+ // that time, IsCSSEnabled() may return true. E.g., setting color value
+ // to collapsed selection, then, change the CSS enabled, finally, user
+ // types text there.
+ if (!HTMLEditUtils::MaybeCSSSpecificColorValue(value)) {
+ HTMLEditUtils::GetNormalizedHTMLColorValue(value, value);
+ }
+ } else {
+ HTMLEditUtils::GetNormalizedCSSColorValue(
+ value, HTMLEditUtils::ZeroAlphaColor::RGBAValue, value);
+ }
+ }
+
+ AutoTArray<EditorInlineStyle, 1> stylesToRemove;
+ if (&aProperty == nsGkAtoms::sup) {
+ // Superscript and Subscript styles are mutually exclusive.
+ stylesToRemove.AppendElement(EditorInlineStyle(*nsGkAtoms::sub));
+ } else if (&aProperty == nsGkAtoms::sub) {
+ // Superscript and Subscript styles are mutually exclusive.
+ stylesToRemove.AppendElement(EditorInlineStyle(*nsGkAtoms::sup));
+ }
+ // Handling `<tt>` element code was implemented for composer (bug 115922).
+ // This shouldn't work with `Document.execCommand()`. Currently, aPrincipal
+ // is set only when the root caller is Document::ExecCommand() so that
+ // we should handle `<tt>` element only when aPrincipal is nullptr that
+ // must be only when XUL command is executed on composer.
+ else if (!aPrincipal) {
+ if (&aProperty == nsGkAtoms::tt) {
+ stylesToRemove.AppendElement(
+ EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::face));
+ } else if (&aProperty == nsGkAtoms::font && aAttribute == nsGkAtoms::face) {
+ if (!value.LowerCaseEqualsASCII("tt")) {
+ stylesToRemove.AppendElement(EditorInlineStyle(*nsGkAtoms::tt));
+ } else {
+ stylesToRemove.AppendElement(
+ EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::face));
+ // Override property, attribute and value if the new font face value is
+ // "tt".
+ property = nsGkAtoms::tt;
+ attribute = nullptr;
+ value.Truncate();
+ }
+ }
+ }
+
+ if (!stylesToRemove.IsEmpty()) {
+ nsresult rv = RemoveInlinePropertiesAsSubAction(stylesToRemove);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::RemoveInlinePropertiesAsSubAction() failed");
+ return rv;
+ }
+ }
+
+ AutoTArray<EditorInlineStyleAndValue, 1> styleToSet;
+ styleToSet.AppendElement(
+ attribute
+ ? EditorInlineStyleAndValue(*property, *attribute, std::move(value))
+ : EditorInlineStyleAndValue(*property));
+ rv = SetInlinePropertiesAsSubAction(styleToSet);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetInlinePropertiesAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::SetInlineProperty(const nsAString& aProperty,
+ const nsAString& aAttribute,
+ const nsAString& aValue) {
+ nsStaticAtom* property = NS_GetStaticAtom(aProperty);
+ if (NS_WARN_IF(!property)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute);
+ AutoEditActionDataSetter editActionData(
+ *this,
+ HTMLEditUtils::GetEditActionForFormatText(*property, attribute, true));
+ switch (editActionData.GetEditAction()) {
+ case EditAction::eSetFontFamilyProperty:
+ MOZ_ASSERT(!aValue.IsVoid());
+ // XXX Should we trim unnecessary white-spaces?
+ editActionData.SetData(aValue);
+ break;
+ case EditAction::eSetColorProperty:
+ case EditAction::eSetBackgroundColorPropertyInline:
+ editActionData.SetColorData(aValue);
+ break;
+ default:
+ break;
+ }
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoTArray<EditorInlineStyleAndValue, 1> styleToSet;
+ styleToSet.AppendElement(
+ attribute ? EditorInlineStyleAndValue(*property, *attribute, aValue)
+ : EditorInlineStyleAndValue(*property));
+ rv = SetInlinePropertiesAsSubAction(styleToSet);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetInlinePropertiesAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+template <size_t N>
+nsresult HTMLEditor::SetInlinePropertiesAsSubAction(
+ const AutoTArray<EditorInlineStyleAndValue, N>& aStylesToSet) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aStylesToSet.IsEmpty());
+
+ DebugOnly<nsresult> rvIgnored = CommitComposition();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CommitComposition() failed, but ignored");
+
+ if (SelectionRef().IsCollapsed()) {
+ // Manipulating text attributes on a collapsed selection only sets state
+ // for the next text insertion
+ mPendingStylesToApplyToNewContent->PreserveStyles(aStylesToSet);
+ return NS_OK;
+ }
+
+ // XXX Shouldn't we return before calling `CommitComposition()`?
+ if (IsPlaintextMailComposer()) {
+ return NS_OK;
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ RefPtr<Element> const editingHost =
+ ComputeEditingHost(LimitInBodyElement::No);
+ if (NS_WARN_IF(!editingHost)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertElement, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ AutoRangeArray selectionRanges(SelectionRef());
+ nsresult rv = SetInlinePropertiesAroundRanges(selectionRanges, aStylesToSet,
+ *editingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetInlinePropertiesAroundRanges() failed");
+ return rv;
+ }
+ MOZ_ASSERT(!selectionRanges.HasSavedRanges());
+ rv = selectionRanges.ApplyTo(SelectionRef());
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed");
+ return rv;
+}
+
+template <size_t N>
+nsresult HTMLEditor::SetInlinePropertiesAroundRanges(
+ AutoRangeArray& aRanges,
+ const AutoTArray<EditorInlineStyleAndValue, N>& aStylesToSet,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(!aRanges.HasSavedRanges());
+ for (const EditorInlineStyleAndValue& styleToSet : aStylesToSet) {
+ AutoInlineStyleSetter inlineStyleSetter(styleToSet);
+ for (OwningNonNull<nsRange>& domRange : aRanges.Ranges()) {
+ inlineStyleSetter.Reset();
+ auto rangeOrError =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMRange, nsresult> {
+ EditorDOMRange range(domRange);
+ // If we're setting <font>, we want to remove ancestors which set
+ // `font-size` or <font size="..."> recursively. Therefore, for
+ // extending the ranges to contain all ancestors in the range, we need
+ // to split ancestors first.
+ // XXX: Blink and WebKit inserts <font> elements to inner most
+ // elements, however, we cannot do it under current design because
+ // once we contain ancestors which have `font-size` or are
+ // <font size="...">, we lost the original ranges which we wanted to
+ // apply the style. For fixing this, we need to manage both ranges, but
+ // it's too expensive especially we allow to run script when we touch
+ // the DOM tree. Additionally, font-size value affects the height
+ // of the element, but does not affect the height of ancestor inline
+ // elements. Therefore, following the behavior may cause similar issue
+ // as bug 1808906. So at least for now, we should not do this big work.
+ if (styleToSet.IsStyleOfFontElement()) {
+ Result<SplitRangeOffResult, nsresult> splitAncestorsResult =
+ SplitAncestorStyledInlineElementsAtRangeEdges(
+ range, styleToSet, SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitAncestorsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges() "
+ "failed");
+ return splitAncestorsResult.propagateErr();
+ }
+ SplitRangeOffResult unwrappedResult = splitAncestorsResult.unwrap();
+ unwrappedResult.IgnoreCaretPointSuggestion();
+ range = unwrappedResult.RangeRef();
+ if (NS_WARN_IF(!range.IsPositionedAndValid())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+ Result<EditorRawDOMRange, nsresult> rangeOrError =
+ inlineStyleSetter.ExtendOrShrinkRangeToApplyTheStyle(*this, range,
+ aEditingHost);
+ if (MOZ_UNLIKELY(rangeOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::ExtendOrShrinkRangeToApplyTheStyle() failed, but "
+ "ignored");
+ return EditorDOMRange();
+ }
+ return EditorDOMRange(rangeOrError.unwrap());
+ }();
+ if (MOZ_UNLIKELY(rangeOrError.isErr())) {
+ return rangeOrError.unwrapErr();
+ }
+
+ const EditorDOMRange range = rangeOrError.unwrap();
+ if (!range.IsPositioned()) {
+ continue;
+ }
+
+ // If the range is collapsed, we should insert new element there.
+ if (range.Collapsed()) {
+ Result<RefPtr<Text>, nsresult> emptyTextNodeOrError =
+ AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle(
+ *this, range.StartRef(), aEditingHost);
+ if (MOZ_UNLIKELY(emptyTextNodeOrError.isErr())) {
+ NS_WARNING(
+ "AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle() "
+ "failed");
+ return emptyTextNodeOrError.unwrapErr();
+ }
+ if (MOZ_UNLIKELY(!emptyTextNodeOrError.inspect())) {
+ continue; // Couldn't insert text node there
+ }
+ RefPtr<Text> emptyTextNode = emptyTextNodeOrError.unwrap();
+ Result<CaretPoint, nsresult> caretPointOrError =
+ inlineStyleSetter
+ .ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
+ *this, *emptyTextNode);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "AutoInlineStyleSetter::"
+ "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ DebugOnly<nsresult> rvIgnored = domRange->CollapseTo(emptyTextNode, 0);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsRange::CollapseTo() failed, but ignored");
+ continue;
+ }
+
+ // Use const_cast hack here for preventing the others to update the range.
+ AutoTrackDOMRange trackRange(RangeUpdaterRef(),
+ const_cast<EditorDOMRange*>(&range));
+ auto UpdateSelectionRange = [&]() MOZ_CAN_RUN_SCRIPT {
+ // If inlineStyleSetter creates elements or setting styles, we should
+ // select between start of first element and end of last element.
+ if (inlineStyleSetter.FirstHandledPointRef().IsInContentNode()) {
+ MOZ_ASSERT(inlineStyleSetter.LastHandledPointRef().IsInContentNode());
+ const auto startPoint =
+ !inlineStyleSetter.FirstHandledPointRef().IsStartOfContainer()
+ ? inlineStyleSetter.FirstHandledPointRef()
+ .To<EditorRawDOMPoint>()
+ : HTMLEditUtils::GetDeepestEditableStartPointOf<
+ EditorRawDOMPoint>(
+ *inlineStyleSetter.FirstHandledPointRef()
+ .ContainerAs<nsIContent>());
+ const auto endPoint =
+ !inlineStyleSetter.LastHandledPointRef().IsEndOfContainer()
+ ? inlineStyleSetter.LastHandledPointRef()
+ .To<EditorRawDOMPoint>()
+ : HTMLEditUtils::GetDeepestEditableEndPointOf<
+ EditorRawDOMPoint>(
+ *inlineStyleSetter.LastHandledPointRef()
+ .ContainerAs<nsIContent>());
+ nsresult rv = domRange->SetStartAndEnd(
+ startPoint.ToRawRangeBoundary(), endPoint.ToRawRangeBoundary());
+ if (NS_SUCCEEDED(rv)) {
+ trackRange.StopTracking();
+ return;
+ }
+ }
+ // Otherwise, use the range computed with the tracking original range.
+ trackRange.FlushAndStopTracking();
+ domRange->SetStartAndEnd(range.StartRef().ToRawRangeBoundary(),
+ range.EndRef().ToRawRangeBoundary());
+ };
+
+ // If range is in a text node, apply new style simply.
+ if (range.InSameContainer() && range.StartRef().IsInTextNode()) {
+ // MOZ_KnownLive(...ContainerAs<Text>()) because of grabbed by `range`.
+ // MOZ_KnownLive(styleToSet.*) due to bug 1622253.
+ Result<SplitRangeOffFromNodeResult, nsresult>
+ wrapTextInStyledElementResult =
+ inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode(
+ *this, MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()),
+ range.StartRef().Offset(), range.EndRef().Offset());
+ if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed");
+ return wrapTextInStyledElementResult.unwrapErr();
+ }
+ // The caller should handle the ranges as Selection if necessary, and we
+ // don't want to update aRanges with this result.
+ wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion();
+ UpdateSelectionRange();
+ continue;
+ }
+
+ // Collect editable nodes which are entirely contained in the range.
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContentsAroundRange;
+ {
+ ContentSubtreeIterator subtreeIter;
+ // If there is no node which is entirely in the range,
+ // `ContentSubtreeIterator::Init()` fails, but this is possible case,
+ // don't warn it.
+ if (NS_SUCCEEDED(
+ subtreeIter.Init(range.StartRef().ToRawRangeBoundary(),
+ range.EndRef().ToRawRangeBoundary()))) {
+ for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
+ nsINode* node = subtreeIter.GetCurrentNode();
+ if (NS_WARN_IF(!node)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (MOZ_UNLIKELY(!node->IsContent())) {
+ continue;
+ }
+ // We don't need to wrap non-editable node in new inline element
+ // nor shouldn't modify `style` attribute of non-editable element.
+ if (!EditorUtils::IsEditableContent(*node->AsContent(),
+ EditorType::HTML)) {
+ continue;
+ }
+ // We shouldn't wrap invisible text node in new inline element.
+ if (node->IsText() &&
+ !HTMLEditUtils::IsVisibleTextNode(*node->AsText())) {
+ continue;
+ }
+ arrayOfContentsAroundRange.AppendElement(*node->AsContent());
+ }
+ }
+ }
+
+ // If start node is a text node, apply new style to a part of it.
+ if (range.StartRef().IsInTextNode() &&
+ EditorUtils::IsEditableContent(*range.StartRef().ContainerAs<Text>(),
+ EditorType::HTML)) {
+ // MOZ_KnownLive(...ContainerAs<Text>()) because of grabbed by `range`.
+ // MOZ_KnownLive(styleToSet.*) due to bug 1622253.
+ Result<SplitRangeOffFromNodeResult, nsresult>
+ wrapTextInStyledElementResult =
+ inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode(
+ *this, MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()),
+ range.StartRef().Offset(),
+ range.StartRef().ContainerAs<Text>()->TextDataLength());
+ if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed");
+ return wrapTextInStyledElementResult.unwrapErr();
+ }
+ // The caller should handle the ranges as Selection if necessary, and we
+ // don't want to update aRanges with this result.
+ wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ // Then, apply new style to all nodes in the range entirely.
+ for (auto& content : arrayOfContentsAroundRange) {
+ // MOZ_KnownLive due to bug 1622253.
+ Result<CaretPoint, nsresult> pointToPutCaretOrError =
+ inlineStyleSetter
+ .ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
+ *this, MOZ_KnownLive(*content));
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "AutoInlineStyleSetter::"
+ "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ // The caller should handle the ranges as Selection if necessary, and we
+ // don't want to update aRanges with this result.
+ pointToPutCaretOrError.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ // Finally, if end node is a text node, apply new style to a part of it.
+ if (range.EndRef().IsInTextNode() &&
+ EditorUtils::IsEditableContent(*range.EndRef().ContainerAs<Text>(),
+ EditorType::HTML)) {
+ // MOZ_KnownLive(...ContainerAs<Text>()) because of grabbed by `range`.
+ // MOZ_KnownLive(styleToSet.mAttribute) due to bug 1622253.
+ Result<SplitRangeOffFromNodeResult, nsresult>
+ wrapTextInStyledElementResult =
+ inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode(
+ *this, MOZ_KnownLive(*range.EndRef().ContainerAs<Text>()),
+ 0, range.EndRef().Offset());
+ if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed");
+ return wrapTextInStyledElementResult.unwrapErr();
+ }
+ // The caller should handle the ranges as Selection if necessary, and we
+ // don't want to update aRanges with this result.
+ wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+ UpdateSelectionRange();
+ }
+ }
+ return NS_OK;
+}
+
+// static
+Result<RefPtr<Text>, nsresult>
+HTMLEditor::AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCandidatePointToInsert,
+ const Element& aEditingHost) {
+ auto pointToInsertNewText =
+ HTMLEditUtils::GetBetterCaretPositionToInsertText<EditorDOMPoint>(
+ aCandidatePointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(!pointToInsertNewText.IsSet())) {
+ return RefPtr<Text>(); // cannot insert text there
+ }
+ auto pointToInsertNewStyleOrError =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMPoint, nsresult> {
+ if (!pointToInsertNewText.IsInTextNode()) {
+ return pointToInsertNewText;
+ }
+ if (!pointToInsertNewText.ContainerAs<Text>()->TextDataLength()) {
+ return pointToInsertNewText; // Use it
+ }
+ if (pointToInsertNewText.IsStartOfContainer()) {
+ return pointToInsertNewText.ParentPoint();
+ }
+ if (pointToInsertNewText.IsEndOfContainer()) {
+ return EditorDOMPoint::After(*pointToInsertNewText.ContainerAs<Text>());
+ }
+ Result<SplitNodeResult, nsresult> splitTextNodeResult =
+ aHTMLEditor.SplitNodeWithTransaction(pointToInsertNewText);
+ if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitTextNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitTextNodeResult = splitTextNodeResult.unwrap();
+ unwrappedSplitTextNodeResult.IgnoreCaretPointSuggestion();
+ return unwrappedSplitTextNodeResult.AtSplitPoint<EditorDOMPoint>();
+ }();
+ if (MOZ_UNLIKELY(pointToInsertNewStyleOrError.isErr())) {
+ return pointToInsertNewStyleOrError.propagateErr();
+ }
+
+ // If we already have empty text node which is available for placeholder in
+ // new styled element, let's use it.
+ if (pointToInsertNewStyleOrError.inspect().IsInTextNode()) {
+ return RefPtr<Text>(
+ pointToInsertNewStyleOrError.inspect().ContainerAs<Text>());
+ }
+
+ // Otherwise, we need an empty text node to create new inline style.
+ RefPtr<Text> newEmptyTextNode = aHTMLEditor.CreateTextNode(u""_ns);
+ if (MOZ_UNLIKELY(!newEmptyTextNode)) {
+ NS_WARNING("EditorBase::CreateTextNode() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<CreateTextResult, nsresult> insertNewTextNodeResult =
+ aHTMLEditor.InsertNodeWithTransaction<Text>(
+ *newEmptyTextNode, pointToInsertNewStyleOrError.inspect());
+ if (MOZ_UNLIKELY(insertNewTextNodeResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertNewTextNodeResult.propagateErr();
+ }
+ insertNewTextNodeResult.inspect().IgnoreCaretPointSuggestion();
+ return newEmptyTextNode;
+}
+
+Result<bool, nsresult>
+HTMLEditor::AutoInlineStyleSetter::ElementIsGoodContainerForTheStyle(
+ HTMLEditor& aHTMLEditor, Element& aElement) const {
+ // If the editor is in the CSS mode and the style can be specified with CSS,
+ // we should not use existing HTML element as a new container.
+ const bool isCSSEditable = IsCSSSettable(aElement);
+ if (!aHTMLEditor.IsCSSEnabled() || !isCSSEditable) {
+ // First check for <b>, <i>, etc.
+ if (aElement.IsHTMLElement(&HTMLPropertyRef()) &&
+ !HTMLEditUtils::ElementHasAttribute(aElement) && !mAttribute) {
+ return true;
+ }
+
+ // Now look for things like <font>
+ if (mAttribute) {
+ nsString attrValue;
+ if (aElement.IsHTMLElement(&HTMLPropertyRef()) &&
+ !HTMLEditUtils::ElementHasAttributeExcept(aElement, *mAttribute) &&
+ aElement.GetAttr(mAttribute, attrValue)) {
+ if (attrValue.Equals(mAttributeValue,
+ nsCaseInsensitiveStringComparator)) {
+ return true;
+ }
+ if (mAttribute == nsGkAtoms::color ||
+ mAttribute == nsGkAtoms::bgcolor) {
+ if (aHTMLEditor.IsCSSEnabled()) {
+ if (HTMLEditUtils::IsSameCSSColorValue(mAttributeValue,
+ attrValue)) {
+ return true;
+ }
+ } else if (HTMLEditUtils::IsSameHTMLColorValue(
+ mAttributeValue, attrValue,
+ HTMLEditUtils::TransparentKeyword::Allowed)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ if (!isCSSEditable) {
+ return false;
+ }
+ }
+
+ // No luck so far. Now we check for a <span> with a single style=""
+ // attribute that sets only the style we're looking for, if this type of
+ // style supports it
+ if (!aElement.IsHTMLElement(nsGkAtoms::span) ||
+ !aElement.HasAttr(nsGkAtoms::style) ||
+ HTMLEditUtils::ElementHasAttributeExcept(aElement, *nsGkAtoms::style)) {
+ return false;
+ }
+
+ nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement);
+ if (MOZ_UNLIKELY(!styledElement)) {
+ return false;
+ }
+
+ // Some CSS styles are not so simple. For instance, underline is
+ // "text-decoration: underline", which decomposes into four different text-*
+ // properties. So for now, we just create a span, add the desired style, and
+ // see if it matches.
+ RefPtr<Element> newSpanElement =
+ aHTMLEditor.CreateHTMLContent(nsGkAtoms::span);
+ if (MOZ_UNLIKELY(!newSpanElement)) {
+ NS_WARNING("EditorBase::CreateHTMLContent(nsGkAtoms::span) failed");
+ return false;
+ }
+ nsStyledElement* styledNewSpanElement =
+ nsStyledElement::FromNode(newSpanElement);
+ if (MOZ_UNLIKELY(!styledNewSpanElement)) {
+ return false;
+ }
+ // MOZ_KnownLive(*styledNewSpanElement): It's newSpanElement whose type is
+ // RefPtr.
+ Result<size_t, nsresult> result = CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::No, aHTMLEditor, MOZ_KnownLive(*styledNewSpanElement),
+ *this, &mAttributeValue);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ // The call shouldn't return destroyed error because it must be
+ // impossible to run script with modifying the new orphan node.
+ MOZ_ASSERT_UNREACHABLE("How did you destroy this editor?");
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ return false;
+ }
+ return CSSEditUtils::DoStyledElementsHaveSameStyle(*styledNewSpanElement,
+ *styledElement);
+}
+
+bool HTMLEditor::AutoInlineStyleSetter::ElementIsGoodContainerToSetStyle(
+ nsStyledElement& aStyledElement) const {
+ if (!HTMLEditUtils::IsContainerNode(aStyledElement) ||
+ !EditorUtils::IsEditableContent(aStyledElement, EditorType::HTML)) {
+ return false;
+ }
+
+ // If it has `style` attribute, let's use it.
+ if (aStyledElement.HasAttr(nsGkAtoms::style)) {
+ return true;
+ }
+
+ // If it has `class` or `id` attribute, the element may have specific rule.
+ // For applying the new style, we may need to set `style` attribute to it
+ // to override the specified rule.
+ if (aStyledElement.HasAttr(nsGkAtoms::id) ||
+ aStyledElement.HasAttr(nsGkAtoms::_class)) {
+ return true;
+ }
+
+ // If we're setting text-decoration and the element represents a value of
+ // text-decoration, <ins> or <del>, let's use it.
+ if (IsStyleOfTextDecoration(IgnoreSElement::No) &&
+ aStyledElement.IsAnyOfHTMLElements(nsGkAtoms::u, nsGkAtoms::s,
+ nsGkAtoms::strike, nsGkAtoms::ins,
+ nsGkAtoms::del)) {
+ return true;
+ }
+
+ // If we're setting font-size, color or background-color, we should use <font>
+ // for compatibility with the other browsers.
+ if (&HTMLPropertyRef() == nsGkAtoms::font &&
+ aStyledElement.IsHTMLElement(nsGkAtoms::font)) {
+ return true;
+ }
+
+ // If the element has one or more <br> (even if it's invisible), we don't
+ // want to use the <span> for compatibility with the other browsers.
+ if (aStyledElement.QuerySelector("br"_ns, IgnoreErrors())) {
+ return false;
+ }
+
+ // NOTE: The following code does not match with the other browsers not
+ // completely. Blink considers this with relation with the range.
+ // However, we cannot do it now. We should fix here after or at
+ // fixing bug 1792386.
+
+ // If it's only visible element child of parent block, let's use it.
+ // E.g., we don't want to create new <span> when
+ // `<p>{ <span>abc</span> }</p>`.
+ if (aStyledElement.GetParentElement() &&
+ HTMLEditUtils::IsBlockElement(
+ *aStyledElement.GetParentElement(),
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ for (nsIContent* previousSibling = aStyledElement.GetPreviousSibling();
+ previousSibling;
+ previousSibling = previousSibling->GetPreviousSibling()) {
+ if (previousSibling->IsElement()) {
+ return false; // Assume any elements visible.
+ }
+ if (Text* text = Text::FromNode(previousSibling)) {
+ if (HTMLEditUtils::IsVisibleTextNode(*text)) {
+ return false;
+ }
+ continue;
+ }
+ }
+ for (nsIContent* nextSibling = aStyledElement.GetNextSibling(); nextSibling;
+ nextSibling = nextSibling->GetNextSibling()) {
+ if (nextSibling->IsElement()) {
+ if (!HTMLEditUtils::IsInvisibleBRElement(*nextSibling)) {
+ return false;
+ }
+ continue; // The invisible <br> element may be followed by a child
+ // block, let's continue to check it.
+ }
+ if (Text* text = Text::FromNode(nextSibling)) {
+ if (HTMLEditUtils::IsVisibleTextNode(*text)) {
+ return false;
+ }
+ continue;
+ }
+ }
+ return true;
+ }
+
+ // Otherwise, wrap it into new <span> for making
+ // `<span>[abc</span> <span>def]</span>` become
+ // `<span style="..."><span>abc</span> <span>def</span></span>` rather
+ // than `<span style="...">abc <span>def</span></span>`.
+ return false;
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult>
+HTMLEditor::AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode(
+ HTMLEditor& aHTMLEditor, Text& aText, uint32_t aStartOffset,
+ uint32_t aEndOffset) {
+ const RefPtr<Element> element = aText.GetParentElement();
+ if (!element || !HTMLEditUtils::CanNodeContain(*element, HTMLPropertyRef())) {
+ OnHandled(EditorDOMPoint(&aText, aStartOffset),
+ EditorDOMPoint(&aText, aEndOffset));
+ return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr);
+ }
+
+ // Don't need to do anything if no characters actually selected
+ if (aStartOffset == aEndOffset) {
+ OnHandled(EditorDOMPoint(&aText, aStartOffset),
+ EditorDOMPoint(&aText, aEndOffset));
+ return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr);
+ }
+
+ // Don't need to do anything if property already set on node
+ if (IsCSSSettable(*element)) {
+ // The HTML styles defined by this have a CSS equivalence for node;
+ // let's check if it carries those CSS styles
+ nsAutoString value(mAttributeValue);
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(aHTMLEditor, *element, *this,
+ value);
+ if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
+ NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError.propagateErr();
+ }
+ if (isComputedCSSEquivalentToStyleOrError.unwrap()) {
+ OnHandled(EditorDOMPoint(&aText, aStartOffset),
+ EditorDOMPoint(&aText, aEndOffset));
+ return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr);
+ }
+ } else if (HTMLEditUtils::IsInlineStyleSetByElement(aText, *this,
+ &mAttributeValue)) {
+ OnHandled(EditorDOMPoint(&aText, aStartOffset),
+ EditorDOMPoint(&aText, aEndOffset));
+ return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr);
+ }
+
+ // Make the range an independent node.
+ auto splitAtEndResult =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
+ EditorDOMPoint atEnd(&aText, aEndOffset);
+ if (atEnd.IsEndOfContainer()) {
+ return SplitNodeResult::NotHandled(atEnd);
+ }
+ // We need to split off back of text node
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ aHTMLEditor.SplitNodeWithTransaction(atEnd);
+ if (splitNodeResult.isErr()) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitNodeResult;
+ }
+ if (MOZ_UNLIKELY(!splitNodeResult.inspect().HasCaretPointSuggestion())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeWithTransaction() didn't suggest caret "
+ "point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return splitNodeResult;
+ }();
+ if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
+ return splitAtEndResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap();
+ EditorDOMPoint pointToPutCaret = unwrappedSplitAtEndResult.UnwrapCaretPoint();
+ auto splitAtStartResult =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
+ EditorDOMPoint atStart(unwrappedSplitAtEndResult.DidSplit()
+ ? unwrappedSplitAtEndResult.GetPreviousContent()
+ : &aText,
+ aStartOffset);
+ if (atStart.IsStartOfContainer()) {
+ return SplitNodeResult::NotHandled(atStart);
+ }
+ // We need to split off front of text node
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ aHTMLEditor.SplitNodeWithTransaction(atStart);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitNodeResult;
+ }
+ if (MOZ_UNLIKELY(!splitNodeResult.inspect().HasCaretPointSuggestion())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeWithTransaction() didn't suggest caret "
+ "point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return splitNodeResult;
+ }();
+ if (MOZ_UNLIKELY(splitAtStartResult.isErr())) {
+ return splitAtStartResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitAtStartResult = splitAtStartResult.unwrap();
+ if (unwrappedSplitAtStartResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedSplitAtStartResult.UnwrapCaretPoint();
+ }
+
+ MOZ_ASSERT_IF(unwrappedSplitAtStartResult.DidSplit(),
+ unwrappedSplitAtStartResult.GetPreviousContent()->IsText());
+ MOZ_ASSERT_IF(unwrappedSplitAtStartResult.DidSplit(),
+ unwrappedSplitAtStartResult.GetNextContent()->IsText());
+ MOZ_ASSERT_IF(unwrappedSplitAtEndResult.DidSplit(),
+ unwrappedSplitAtEndResult.GetPreviousContent()->IsText());
+ MOZ_ASSERT_IF(unwrappedSplitAtEndResult.DidSplit(),
+ unwrappedSplitAtEndResult.GetNextContent()->IsText());
+ // Note that those text nodes are grabbed by unwrappedSplitAtStartResult,
+ // unwrappedSplitAtEndResult or the callers. Therefore, we don't need to make
+ // them strong pointer.
+ Text* const leftTextNode =
+ unwrappedSplitAtStartResult.DidSplit()
+ ? unwrappedSplitAtStartResult.GetPreviousContentAs<Text>()
+ : nullptr;
+ Text* const middleTextNode =
+ unwrappedSplitAtStartResult.DidSplit()
+ ? unwrappedSplitAtStartResult.GetNextContentAs<Text>()
+ : (unwrappedSplitAtEndResult.DidSplit()
+ ? unwrappedSplitAtEndResult.GetPreviousContentAs<Text>()
+ : &aText);
+ Text* const rightTextNode =
+ unwrappedSplitAtEndResult.DidSplit()
+ ? unwrappedSplitAtEndResult.GetNextContentAs<Text>()
+ : nullptr;
+ if (mAttribute) {
+ // Look for siblings that are correct type of node
+ nsIContent* sibling = HTMLEditUtils::GetPreviousSibling(
+ *middleTextNode, {WalkTreeOption::IgnoreNonEditableNode});
+ if (sibling && sibling->IsElement()) {
+ OwningNonNull<Element> element(*sibling->AsElement());
+ Result<bool, nsresult> result =
+ ElementIsGoodContainerForTheStyle(aHTMLEditor, element);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
+ return result.propagateErr();
+ }
+ if (result.inspect()) {
+ // Previous sib is already right kind of inline node; slide this over
+ Result<MoveNodeResult, nsresult> moveTextNodeResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ MOZ_KnownLive(*middleTextNode), element);
+ if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveTextNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveTextNodeResult =
+ moveTextNodeResult.unwrap();
+ unwrappedMoveTextNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ OnHandled(*middleTextNode);
+ return SplitRangeOffFromNodeResult(leftTextNode, middleTextNode,
+ rightTextNode,
+ std::move(pointToPutCaret));
+ }
+ }
+ sibling = HTMLEditUtils::GetNextSibling(
+ *middleTextNode, {WalkTreeOption::IgnoreNonEditableNode});
+ if (sibling && sibling->IsElement()) {
+ OwningNonNull<Element> element(*sibling->AsElement());
+ Result<bool, nsresult> result =
+ ElementIsGoodContainerForTheStyle(aHTMLEditor, element);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
+ return result.propagateErr();
+ }
+ if (result.inspect()) {
+ // Following sib is already right kind of inline node; slide this over
+ Result<MoveNodeResult, nsresult> moveTextNodeResult =
+ aHTMLEditor.MoveNodeWithTransaction(MOZ_KnownLive(*middleTextNode),
+ EditorDOMPoint(sibling, 0u));
+ if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveTextNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveTextNodeResult =
+ moveTextNodeResult.unwrap();
+ unwrappedMoveTextNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ OnHandled(*middleTextNode);
+ return SplitRangeOffFromNodeResult(leftTextNode, middleTextNode,
+ rightTextNode,
+ std::move(pointToPutCaret));
+ }
+ }
+ }
+
+ // Wrap the node inside inline node.
+ Result<CaretPoint, nsresult> setStyleResult =
+ ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
+ aHTMLEditor, MOZ_KnownLive(*middleTextNode));
+ if (MOZ_UNLIKELY(setStyleResult.isErr())) {
+ NS_WARNING(
+ "AutoInlineStyleSetter::"
+ "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
+ return setStyleResult.propagateErr();
+ }
+ return SplitRangeOffFromNodeResult(
+ leftTextNode, middleTextNode, rightTextNode,
+ setStyleResult.unwrap().UnwrapCaretPoint());
+}
+
+Result<CaretPoint, nsresult> HTMLEditor::AutoInlineStyleSetter::ApplyStyle(
+ HTMLEditor& aHTMLEditor, nsIContent& aContent) {
+ // If this is an element that can't be contained in a span, we have to
+ // recurse to its children.
+ if (!HTMLEditUtils::CanNodeContain(*nsGkAtoms::span, aContent)) {
+ if (!aContent.HasChildren()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents;
+ HTMLEditUtils::CollectChildren(
+ aContent, arrayOfContents,
+ {CollectChildrenOption::IgnoreNonEditableChildren,
+ CollectChildrenOption::IgnoreInvisibleTextNodes});
+
+ // Then loop through the list, set the property on each node.
+ EditorDOMPoint pointToPutCaret;
+ for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ Result<CaretPoint, nsresult> setInlinePropertyResult =
+ ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
+ aHTMLEditor, MOZ_KnownLive(content));
+ if (MOZ_UNLIKELY(setInlinePropertyResult.isErr())) {
+ NS_WARNING(
+ "AutoInlineStyleSetter::"
+ "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
+ return setInlinePropertyResult;
+ }
+ setInlinePropertyResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ // First check if there's an adjacent sibling we can put our node into.
+ nsCOMPtr<nsIContent> previousSibling = HTMLEditUtils::GetPreviousSibling(
+ aContent, {WalkTreeOption::IgnoreNonEditableNode});
+ nsCOMPtr<nsIContent> nextSibling = HTMLEditUtils::GetNextSibling(
+ aContent, {WalkTreeOption::IgnoreNonEditableNode});
+ if (RefPtr<Element> previousElement =
+ Element::FromNodeOrNull(previousSibling)) {
+ Result<bool, nsresult> canMoveIntoPreviousSibling =
+ ElementIsGoodContainerForTheStyle(aHTMLEditor, *previousElement);
+ if (MOZ_UNLIKELY(canMoveIntoPreviousSibling.isErr())) {
+ NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
+ return canMoveIntoPreviousSibling.propagateErr();
+ }
+ if (canMoveIntoPreviousSibling.inspect()) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(aContent, *previousSibling);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ RefPtr<Element> nextElement = Element::FromNodeOrNull(nextSibling);
+ if (!nextElement) {
+ OnHandled(aContent);
+ return CaretPoint(unwrappedMoveNodeResult.UnwrapCaretPoint());
+ }
+ Result<bool, nsresult> canMoveIntoNextSibling =
+ ElementIsGoodContainerForTheStyle(aHTMLEditor, *nextElement);
+ if (MOZ_UNLIKELY(canMoveIntoNextSibling.isErr())) {
+ NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
+ unwrappedMoveNodeResult.IgnoreCaretPointSuggestion();
+ return canMoveIntoNextSibling.propagateErr();
+ }
+ if (!canMoveIntoNextSibling.inspect()) {
+ OnHandled(aContent);
+ return CaretPoint(unwrappedMoveNodeResult.UnwrapCaretPoint());
+ }
+ unwrappedMoveNodeResult.IgnoreCaretPointSuggestion();
+
+ // JoinNodesWithTransaction (DoJoinNodes) tries to collapse selection to
+ // the joined point and we want to skip updating `Selection` here.
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+ Result<JoinNodesResult, nsresult> joinNodesResult =
+ aHTMLEditor.JoinNodesWithTransaction(*previousElement, *nextElement);
+ if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
+ NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
+ return joinNodesResult.propagateErr();
+ }
+ // So, let's take it.
+ OnHandled(aContent);
+ return CaretPoint(
+ joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>());
+ }
+ }
+
+ if (RefPtr<Element> nextElement = Element::FromNodeOrNull(nextSibling)) {
+ Result<bool, nsresult> canMoveIntoNextSibling =
+ ElementIsGoodContainerForTheStyle(aHTMLEditor, *nextElement);
+ if (MOZ_UNLIKELY(canMoveIntoNextSibling.isErr())) {
+ NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
+ return canMoveIntoNextSibling.propagateErr();
+ }
+ if (canMoveIntoNextSibling.inspect()) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveNodeWithTransaction(aContent,
+ EditorDOMPoint(nextElement, 0u));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ OnHandled(aContent);
+ return CaretPoint(moveNodeResult.unwrap().UnwrapCaretPoint());
+ }
+ }
+
+ // Don't need to do anything if property already set on node
+ if (const RefPtr<Element> element = aContent.GetAsElementOrParentElement()) {
+ if (IsCSSSettable(*element)) {
+ nsAutoString value(mAttributeValue);
+ // MOZ_KnownLive(element) because it's aContent.
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(aHTMLEditor, *element, *this,
+ value);
+ if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
+ NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError.propagateErr();
+ }
+ if (isComputedCSSEquivalentToStyleOrError.unwrap()) {
+ OnHandled(aContent);
+ return CaretPoint(EditorDOMPoint());
+ }
+ } else if (HTMLEditUtils::IsInlineStyleSetByElement(*element, *this,
+ &mAttributeValue)) {
+ OnHandled(aContent);
+ return CaretPoint(EditorDOMPoint());
+ }
+ }
+
+ auto ShouldUseCSS = [&]() {
+ if (aHTMLEditor.IsCSSEnabled() && aContent.GetAsElementOrParentElement() &&
+ IsCSSSettable(*aContent.GetAsElementOrParentElement())) {
+ return true;
+ }
+ // bgcolor is always done using CSS
+ if (mAttribute == nsGkAtoms::bgcolor) {
+ return true;
+ }
+ // called for removing parent style, we should use CSS with <span> element.
+ if (IsStyleToInvert()) {
+ return true;
+ }
+ // If we set color value, the value may be able to specified only with CSS.
+ // In that case, we need to use CSS even in the HTML mode.
+ if (mAttribute == nsGkAtoms::color) {
+ return mAttributeValue.First() != '#' &&
+ !HTMLEditUtils::CanConvertToHTMLColorValue(mAttributeValue);
+ }
+ return false;
+ };
+
+ if (ShouldUseCSS()) {
+ // We need special handlings for text-decoration.
+ if (IsStyleOfTextDecoration(IgnoreSElement::No)) {
+ Result<CaretPoint, nsresult> result =
+ ApplyCSSTextDecoration(aHTMLEditor, aContent);
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "AutoInlineStyleSetter::ApplyCSSTextDecoration() failed");
+ return result;
+ }
+ EditorDOMPoint pointToPutCaret;
+ RefPtr<nsStyledElement> styledElement = [&]() -> nsStyledElement* {
+ auto* const styledElement = nsStyledElement::FromNode(&aContent);
+ return styledElement && ElementIsGoodContainerToSetStyle(*styledElement)
+ ? styledElement
+ : nullptr;
+ }();
+
+ // If we don't have good element to set the style, let's create new <span>.
+ if (!styledElement) {
+ Result<CreateElementResult, nsresult> wrapInSpanElementResult =
+ aHTMLEditor.InsertContainerWithTransaction(aContent,
+ *nsGkAtoms::span);
+ if (MOZ_UNLIKELY(wrapInSpanElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertContainerWithTransaction(nsGkAtoms::span) "
+ "failed");
+ return wrapInSpanElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedWrapInSpanElementResult =
+ wrapInSpanElementResult.unwrap();
+ MOZ_ASSERT(unwrappedWrapInSpanElementResult.GetNewNode());
+ unwrappedWrapInSpanElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ styledElement = nsStyledElement::FromNode(
+ unwrappedWrapInSpanElementResult.GetNewNode());
+ MOZ_ASSERT(styledElement);
+ if (MOZ_UNLIKELY(!styledElement)) {
+ // Don't return error to avoid creating new path to throwing error.
+ OnHandled(aContent);
+ return CaretPoint(pointToPutCaret);
+ }
+ }
+
+ // Add the CSS styles corresponding to the HTML style request
+ if (IsCSSSettable(*styledElement)) {
+ Result<size_t, nsresult> result = CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::Yes, aHTMLEditor, *styledElement, *this,
+ &mAttributeValue);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle() failed, but ignored");
+ }
+ }
+ OnHandled(aContent);
+ return CaretPoint(pointToPutCaret);
+ }
+
+ nsAutoString attributeValue(mAttributeValue);
+ if (mAttribute == nsGkAtoms::color && mAttributeValue.First() != '#') {
+ // At here, all color values should be able to be parsed as a CSS color
+ // value. Therefore, we need to convert it to normalized HTML color value.
+ HTMLEditUtils::ConvertToNormalizedHTMLColorValue(attributeValue,
+ attributeValue);
+ }
+
+ // is it already the right kind of node, but with wrong attribute?
+ if (aContent.IsHTMLElement(&HTMLPropertyRef())) {
+ if (NS_WARN_IF(!mAttribute)) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+ // Just set the attribute on it.
+ nsresult rv = aHTMLEditor.SetAttributeWithTransaction(
+ MOZ_KnownLive(*aContent.AsElement()), *mAttribute, attributeValue);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::SetAttributeWithTransaction() failed");
+ return Err(rv);
+ }
+ OnHandled(aContent);
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ // ok, chuck it in its very own container
+ Result<CreateElementResult, nsresult> wrapWithNewElementToFormatResult =
+ aHTMLEditor.InsertContainerWithTransaction(
+ aContent, MOZ_KnownLive(HTMLPropertyRef()),
+ !mAttribute ? HTMLEditor::DoNothingForNewElement
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ : [&](HTMLEditor& aHTMLEditor, Element& aNewElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ nsresult rv =
+ aNewElement.SetAttr(kNameSpaceID_None, mAttribute,
+ attributeValue, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Element::SetAttr() failed");
+ return rv;
+ });
+ if (MOZ_UNLIKELY(wrapWithNewElementToFormatResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
+ return wrapWithNewElementToFormatResult.propagateErr();
+ }
+ OnHandled(aContent);
+ MOZ_ASSERT(wrapWithNewElementToFormatResult.inspect().GetNewNode());
+ return CaretPoint(
+ wrapWithNewElementToFormatResult.unwrap().UnwrapCaretPoint());
+}
+
+Result<CaretPoint, nsresult>
+HTMLEditor::AutoInlineStyleSetter::ApplyCSSTextDecoration(
+ HTMLEditor& aHTMLEditor, nsIContent& aContent) {
+ MOZ_ASSERT(IsStyleOfTextDecoration(IgnoreSElement::No));
+
+ EditorDOMPoint pointToPutCaret;
+ RefPtr<nsStyledElement> styledElement = nsStyledElement::FromNode(aContent);
+ nsAutoString newTextDecorationValue;
+ if (&HTMLPropertyRef() == nsGkAtoms::u) {
+ newTextDecorationValue.AssignLiteral(u"underline");
+ } else if (&HTMLPropertyRef() == nsGkAtoms::s ||
+ &HTMLPropertyRef() == nsGkAtoms::strike) {
+ newTextDecorationValue.AssignLiteral(u"line-through");
+ } else {
+ MOZ_ASSERT_UNREACHABLE(
+ "Was new value added in "
+ "IsStyleOfTextDecoration(IgnoreSElement::No))?");
+ }
+ if (styledElement && IsCSSSettable(*styledElement) &&
+ ElementIsGoodContainerToSetStyle(*styledElement)) {
+ nsAutoString textDecorationValue;
+ nsresult rv = CSSEditUtils::GetSpecifiedProperty(
+ *styledElement, *nsGkAtoms::text_decoration, textDecorationValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "CSSEditUtils::GetSpecifiedProperty(nsGkAtoms::text_decoration) "
+ "failed");
+ return Err(rv);
+ }
+ // However, if the element is an element to style the text-decoration,
+ // replace it with new <span>.
+ if (styledElement && styledElement->IsAnyOfHTMLElements(
+ nsGkAtoms::u, nsGkAtoms::s, nsGkAtoms::strike)) {
+ Result<CreateElementResult, nsresult> replaceResult =
+ aHTMLEditor.ReplaceContainerAndCloneAttributesWithTransaction(
+ *styledElement, *nsGkAtoms::span);
+ if (MOZ_UNLIKELY(replaceResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction() "
+ "failed");
+ return replaceResult.propagateErr();
+ }
+ CreateElementResult unwrappedReplaceResult = replaceResult.unwrap();
+ MOZ_ASSERT(unwrappedReplaceResult.GetNewNode());
+ unwrappedReplaceResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ // The new <span> needs to specify the original element's text-decoration
+ // style unless it's specified explicitly.
+ if (textDecorationValue.IsEmpty()) {
+ if (!newTextDecorationValue.IsEmpty()) {
+ newTextDecorationValue.Append(HTMLEditUtils::kSpace);
+ }
+ if (styledElement->IsHTMLElement(nsGkAtoms::u)) {
+ newTextDecorationValue.AppendLiteral(u"underline");
+ } else {
+ newTextDecorationValue.AppendLiteral(u"line-through");
+ }
+ }
+ styledElement =
+ nsStyledElement::FromNode(unwrappedReplaceResult.GetNewNode());
+ if (NS_WARN_IF(!styledElement)) {
+ OnHandled(aContent);
+ return CaretPoint(pointToPutCaret);
+ }
+ }
+ // If the element has default style, we need to keep it after specifying
+ // text-decoration.
+ else if (textDecorationValue.IsEmpty() &&
+ styledElement->IsAnyOfHTMLElements(nsGkAtoms::u, nsGkAtoms::ins)) {
+ if (!newTextDecorationValue.IsEmpty()) {
+ newTextDecorationValue.Append(HTMLEditUtils::kSpace);
+ }
+ newTextDecorationValue.AppendLiteral(u"underline");
+ } else if (textDecorationValue.IsEmpty() &&
+ styledElement->IsAnyOfHTMLElements(
+ nsGkAtoms::s, nsGkAtoms::strike, nsGkAtoms::del)) {
+ if (!newTextDecorationValue.IsEmpty()) {
+ newTextDecorationValue.Append(HTMLEditUtils::kSpace);
+ }
+ newTextDecorationValue.AppendLiteral(u"line-through");
+ }
+ }
+ // Otherwise, use new <span> element.
+ else {
+ Result<CreateElementResult, nsresult> wrapInSpanElementResult =
+ aHTMLEditor.InsertContainerWithTransaction(aContent, *nsGkAtoms::span);
+ if (MOZ_UNLIKELY(wrapInSpanElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertContainerWithTransaction(nsGkAtoms::span) failed");
+ return wrapInSpanElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedWrapInSpanElementResult =
+ wrapInSpanElementResult.unwrap();
+ MOZ_ASSERT(unwrappedWrapInSpanElementResult.GetNewNode());
+ unwrappedWrapInSpanElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ styledElement = nsStyledElement::FromNode(
+ unwrappedWrapInSpanElementResult.GetNewNode());
+ if (NS_WARN_IF(!styledElement)) {
+ OnHandled(aContent);
+ return CaretPoint(pointToPutCaret);
+ }
+ }
+
+ nsresult rv = CSSEditUtils::SetCSSPropertyWithTransaction(
+ aHTMLEditor, *styledElement, *nsGkAtoms::text_decoration,
+ newTextDecorationValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::SetCSSPropertyWithTransaction() failed");
+ return Err(rv);
+ }
+ OnHandled(aContent);
+ return CaretPoint(pointToPutCaret);
+}
+
+Result<CaretPoint, nsresult> HTMLEditor::AutoInlineStyleSetter::
+ ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(HTMLEditor& aHTMLEditor,
+ nsIContent& aContent) {
+ if (NS_WARN_IF(!aContent.GetParentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ OwningNonNull<nsINode> parent = *aContent.GetParentNode();
+ nsCOMPtr<nsIContent> previousSibling = aContent.GetPreviousSibling(),
+ nextSibling = aContent.GetNextSibling();
+ EditorDOMPoint pointToPutCaret;
+ if (aContent.IsElement()) {
+ Result<EditorDOMPoint, nsresult> removeStyleResult =
+ aHTMLEditor.RemoveStyleInside(MOZ_KnownLive(*aContent.AsElement()),
+ *this, SpecifiedStyle::Preserve);
+ if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
+ return removeStyleResult.propagateErr();
+ }
+ if (removeStyleResult.inspect().IsSet()) {
+ pointToPutCaret = removeStyleResult.unwrap();
+ }
+ if (nsStaticAtom* similarElementNameAtom = GetSimilarElementNameAtom()) {
+ Result<EditorDOMPoint, nsresult> removeStyleResult =
+ aHTMLEditor.RemoveStyleInside(
+ MOZ_KnownLive(*aContent.AsElement()),
+ EditorInlineStyle(*similarElementNameAtom),
+ SpecifiedStyle::Preserve);
+ if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
+ return removeStyleResult.propagateErr();
+ }
+ if (removeStyleResult.inspect().IsSet()) {
+ pointToPutCaret = removeStyleResult.unwrap();
+ }
+ }
+ }
+
+ if (aContent.GetParentNode()) {
+ // The node is still where it was
+ Result<CaretPoint, nsresult> pointToPutCaretOrError =
+ ApplyStyle(aHTMLEditor, aContent);
+ NS_WARNING_ASSERTION(pointToPutCaretOrError.isOk(),
+ "AutoInlineStyleSetter::ApplyStyle() failed");
+ return pointToPutCaretOrError;
+ }
+
+ // It's vanished. Use the old siblings for reference to construct a
+ // list. But first, verify that the previous/next siblings are still
+ // where we expect them; otherwise we have to give up.
+ if (NS_WARN_IF(previousSibling &&
+ previousSibling->GetParentNode() != parent) ||
+ NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parent) ||
+ NS_WARN_IF(!parent->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ AutoTArray<OwningNonNull<nsIContent>, 24> nodesToSet;
+ for (nsIContent* content = previousSibling ? previousSibling->GetNextSibling()
+ : parent->GetFirstChild();
+ content && content != nextSibling; content = content->GetNextSibling()) {
+ if (EditorUtils::IsEditableContent(*content, EditorType::HTML)) {
+ nodesToSet.AppendElement(*content);
+ }
+ }
+
+ for (OwningNonNull<nsIContent>& content : nodesToSet) {
+ // MOZ_KnownLive because 'nodesToSet' is guaranteed to
+ // keep it alive.
+ Result<CaretPoint, nsresult> pointToPutCaretOrError =
+ ApplyStyle(aHTMLEditor, MOZ_KnownLive(content));
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("AutoInlineStyleSetter::ApplyStyle() failed");
+ return pointToPutCaretOrError;
+ }
+ pointToPutCaretOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ return CaretPoint(pointToPutCaret);
+}
+
+bool HTMLEditor::AutoInlineStyleSetter::ContentIsElementSettingTheStyle(
+ const HTMLEditor& aHTMLEditor, nsIContent& aContent) const {
+ Element* const element = Element::FromNode(&aContent);
+ if (!element) {
+ return false;
+ }
+ if (IsRepresentedBy(*element)) {
+ return true;
+ }
+ Result<bool, nsresult> specified = IsSpecifiedBy(aHTMLEditor, *element);
+ NS_WARNING_ASSERTION(specified.isOk(),
+ "EditorInlineStyle::IsSpecified() failed, but ignored");
+ return specified.unwrapOr(false);
+}
+
+// static
+nsIContent* HTMLEditor::AutoInlineStyleSetter::GetNextEditableInlineContent(
+ const nsIContent& aContent, const nsINode* aLimiter) {
+ auto* const nextContentInRange = [&]() -> nsIContent* {
+ for (nsIContent* parent : aContent.InclusiveAncestorsOfType<nsIContent>()) {
+ if (parent == aLimiter ||
+ !EditorUtils::IsEditableContent(*parent, EditorType::HTML) ||
+ (parent->IsElement() &&
+ (HTMLEditUtils::IsBlockElement(
+ *parent->AsElement(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ HTMLEditUtils::IsDisplayInsideFlowRoot(*parent->AsElement())))) {
+ return nullptr;
+ }
+ if (nsIContent* nextSibling = parent->GetNextSibling()) {
+ return nextSibling;
+ }
+ }
+ return nullptr;
+ }();
+ return nextContentInRange &&
+ EditorUtils::IsEditableContent(*nextContentInRange,
+ EditorType::HTML) &&
+ !HTMLEditUtils::IsBlockElement(
+ *nextContentInRange,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)
+ ? nextContentInRange
+ : nullptr;
+}
+
+// static
+nsIContent* HTMLEditor::AutoInlineStyleSetter::GetPreviousEditableInlineContent(
+ const nsIContent& aContent, const nsINode* aLimiter) {
+ auto* const previousContentInRange = [&]() -> nsIContent* {
+ for (nsIContent* parent : aContent.InclusiveAncestorsOfType<nsIContent>()) {
+ if (parent == aLimiter ||
+ !EditorUtils::IsEditableContent(*parent, EditorType::HTML) ||
+ (parent->IsElement() &&
+ (HTMLEditUtils::IsBlockElement(
+ *parent->AsElement(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ HTMLEditUtils::IsDisplayInsideFlowRoot(*parent->AsElement())))) {
+ return nullptr;
+ }
+ if (nsIContent* previousSibling = parent->GetPreviousSibling()) {
+ return previousSibling;
+ }
+ }
+ return nullptr;
+ }();
+ return previousContentInRange &&
+ EditorUtils::IsEditableContent(*previousContentInRange,
+ EditorType::HTML) &&
+ !HTMLEditUtils::IsBlockElement(
+ *previousContentInRange,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)
+ ? previousContentInRange
+ : nullptr;
+}
+
+EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::GetShrunkenRangeStart(
+ const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
+ const nsINode& aCommonAncestorOfRange,
+ const nsIContent* aFirstEntirelySelectedContentNodeInRange) const {
+ const EditorDOMPoint& startRef = aRange.StartRef();
+ // <a> cannot be nested and it should be represented with one element as far
+ // as possible. Therefore, we don't need to shrink the range.
+ if (IsStyleOfAnchorElement()) {
+ return startRef.To<EditorRawDOMPoint>();
+ }
+ // If the start boundary is at end of a node, we need to shrink the range
+ // to next content, e.g., `abc[<b>def` should be `abc<b>[def` unless the
+ // <b> is not entirely selected.
+ auto* const nextContentOrStartContainer = [&]() -> nsIContent* {
+ if (!startRef.IsInContentNode()) {
+ return nullptr;
+ }
+ if (!startRef.IsEndOfContainer()) {
+ return startRef.ContainerAs<nsIContent>();
+ }
+ nsIContent* const nextContent =
+ AutoInlineStyleSetter::GetNextEditableInlineContent(
+ *startRef.ContainerAs<nsIContent>(), &aCommonAncestorOfRange);
+ return nextContent ? nextContent : startRef.ContainerAs<nsIContent>();
+ }();
+ if (MOZ_UNLIKELY(!nextContentOrStartContainer)) {
+ return startRef.To<EditorRawDOMPoint>();
+ }
+ EditorRawDOMPoint startPoint =
+ nextContentOrStartContainer != startRef.ContainerAs<nsIContent>()
+ ? EditorRawDOMPoint(nextContentOrStartContainer)
+ : startRef.To<EditorRawDOMPoint>();
+ MOZ_ASSERT(startPoint.IsSet());
+ // If the start point points a content node, let's try to move it down to
+ // start of the child recursively.
+ while (nsIContent* child = startPoint.GetChild()) {
+ // We shouldn't cross editable and block boundary.
+ if (!EditorUtils::IsEditableContent(*child, EditorType::HTML) ||
+ HTMLEditUtils::IsBlockElement(
+ *child, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ break;
+ }
+ // If we reach a text node, the minimized range starts from start of it.
+ if (child->IsText()) {
+ startPoint.Set(child, 0u);
+ break;
+ }
+ // Don't shrink the range into element which applies the style to children
+ // because we want to update the element. E.g., if we are setting
+ // background color, we want to update style attribute of an element which
+ // specifies background color with `style` attribute.
+ if (child == aFirstEntirelySelectedContentNodeInRange) {
+ break;
+ }
+ // We should not start from an atomic element such as <br>, <img>, etc.
+ if (!HTMLEditUtils::IsContainerNode(*child)) {
+ break;
+ }
+ // If the element specifies the style, we should update it. Therefore, we
+ // need to wrap it in the range.
+ if (ContentIsElementSettingTheStyle(aHTMLEditor, *child)) {
+ break;
+ }
+ // If the child is an `<a>`, we should not shrink the range into it
+ // because user may not want to keep editing in the link except when user
+ // tries to update selection into it obviously.
+ if (child->IsHTMLElement(nsGkAtoms::a)) {
+ break;
+ }
+ startPoint.Set(child, 0u);
+ }
+ return startPoint;
+}
+
+EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::GetShrunkenRangeEnd(
+ const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
+ const nsINode& aCommonAncestorOfRange,
+ const nsIContent* aLastEntirelySelectedContentNodeInRange) const {
+ const EditorDOMPoint& endRef = aRange.EndRef();
+ // <a> cannot be nested and it should be represented with one element as far
+ // as possible. Therefore, we don't need to shrink the range.
+ if (IsStyleOfAnchorElement()) {
+ return endRef.To<EditorRawDOMPoint>();
+ }
+ // If the end boundary is at start of a node, we need to shrink the range
+ // to previous content, e.g., `abc</b>]def` should be `abc]</b>def` unless
+ // the <b> is not entirely selected.
+ auto* const previousContentOrEndContainer = [&]() -> nsIContent* {
+ if (!endRef.IsInContentNode()) {
+ return nullptr;
+ }
+ if (!endRef.IsStartOfContainer()) {
+ return endRef.ContainerAs<nsIContent>();
+ }
+ nsIContent* const previousContent =
+ AutoInlineStyleSetter::GetPreviousEditableInlineContent(
+ *endRef.ContainerAs<nsIContent>(), &aCommonAncestorOfRange);
+ return previousContent ? previousContent : endRef.ContainerAs<nsIContent>();
+ }();
+ if (MOZ_UNLIKELY(!previousContentOrEndContainer)) {
+ return endRef.To<EditorRawDOMPoint>();
+ }
+ EditorRawDOMPoint endPoint =
+ previousContentOrEndContainer != endRef.ContainerAs<nsIContent>()
+ ? EditorRawDOMPoint::After(*previousContentOrEndContainer)
+ : endRef.To<EditorRawDOMPoint>();
+ MOZ_ASSERT(endPoint.IsSet());
+ // If the end point points after a content node, let's try to move it down
+ // to end of the child recursively.
+ while (nsIContent* child = endPoint.GetPreviousSiblingOfChild()) {
+ // We shouldn't cross editable and block boundary.
+ if (!EditorUtils::IsEditableContent(*child, EditorType::HTML) ||
+ HTMLEditUtils::IsBlockElement(
+ *child, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ break;
+ }
+ // If we reach a text node, the minimized range starts from start of it.
+ if (child->IsText()) {
+ endPoint.SetToEndOf(child);
+ break;
+ }
+ // Don't shrink the range into element which applies the style to children
+ // because we want to update the element. E.g., if we are setting
+ // background color, we want to update style attribute of an element which
+ // specifies background color with `style` attribute.
+ if (child == aLastEntirelySelectedContentNodeInRange) {
+ break;
+ }
+ // We should not end in an atomic element such as <br>, <img>, etc.
+ if (!HTMLEditUtils::IsContainerNode(*child)) {
+ break;
+ }
+ // If the element specifies the style, we should update it. Therefore, we
+ // need to wrap it in the range.
+ if (ContentIsElementSettingTheStyle(aHTMLEditor, *child)) {
+ break;
+ }
+ // If the child is an `<a>`, we should not shrink the range into it
+ // because user may not want to keep editing in the link except when user
+ // tries to update selection into it obviously.
+ if (child->IsHTMLElement(nsGkAtoms::a)) {
+ break;
+ }
+ endPoint.SetToEndOf(child);
+ }
+ return endPoint;
+}
+
+EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::
+ GetExtendedRangeStartToWrapAncestorApplyingSameStyle(
+ const HTMLEditor& aHTMLEditor,
+ const EditorRawDOMPoint& aStartPoint) const {
+ MOZ_ASSERT(aStartPoint.IsSetAndValid());
+
+ EditorRawDOMPoint startPoint = aStartPoint;
+ // FIXME: This should handle ignore invisible white-spaces before the position
+ // if it's in a text node or invisible white-spaces.
+ if (!startPoint.IsStartOfContainer() ||
+ startPoint.GetContainer()->GetPreviousSibling()) {
+ return startPoint;
+ }
+
+ // FYI: Currently, we don't support setting `font-size` even in the CSS mode.
+ // Therefore, if the style is <font size="...">, we always set a <font>.
+ const bool isSettingFontElement =
+ IsStyleOfFontSize() ||
+ (!aHTMLEditor.IsCSSEnabled() && IsStyleOfFontElement());
+ Element* mostDistantStartParentHavingStyle = nullptr;
+ for (Element* parent :
+ startPoint.GetContainer()->InclusiveAncestorsOfType<Element>()) {
+ if (!EditorUtils::IsEditableContent(*parent, EditorType::HTML) ||
+ HTMLEditUtils::IsBlockElement(
+ *parent, BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ HTMLEditUtils::IsDisplayInsideFlowRoot(*parent)) {
+ break;
+ }
+ if (ContentIsElementSettingTheStyle(aHTMLEditor, *parent)) {
+ mostDistantStartParentHavingStyle = parent;
+ }
+ // If we're setting <font> element and there is a <font> element which is
+ // entirely selected, we should use it.
+ else if (isSettingFontElement && parent->IsHTMLElement(nsGkAtoms::font)) {
+ mostDistantStartParentHavingStyle = parent;
+ }
+ if (parent->GetPreviousSibling()) {
+ break; // The parent is not first element in its parent, stop climbing.
+ }
+ }
+ if (mostDistantStartParentHavingStyle) {
+ startPoint.Set(mostDistantStartParentHavingStyle);
+ }
+ return startPoint;
+}
+
+EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::
+ GetExtendedRangeEndToWrapAncestorApplyingSameStyle(
+ const HTMLEditor& aHTMLEditor,
+ const EditorRawDOMPoint& aEndPoint) const {
+ MOZ_ASSERT(aEndPoint.IsSetAndValid());
+
+ EditorRawDOMPoint endPoint = aEndPoint;
+ // FIXME: This should ignore invisible white-spaces after the position if it's
+ // in a text node, invisible <br> or following invisible text nodes.
+ if (!endPoint.IsEndOfContainer() ||
+ endPoint.GetContainer()->GetNextSibling()) {
+ return endPoint;
+ }
+
+ // FYI: Currently, we don't support setting `font-size` even in the CSS mode.
+ // Therefore, if the style is <font size="...">, we always set a <font>.
+ const bool isSettingFontElement =
+ IsStyleOfFontSize() ||
+ (!aHTMLEditor.IsCSSEnabled() && IsStyleOfFontElement());
+ Element* mostDistantEndParentHavingStyle = nullptr;
+ for (Element* parent :
+ endPoint.GetContainer()->InclusiveAncestorsOfType<Element>()) {
+ if (!EditorUtils::IsEditableContent(*parent, EditorType::HTML) ||
+ HTMLEditUtils::IsBlockElement(
+ *parent, BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ HTMLEditUtils::IsDisplayInsideFlowRoot(*parent)) {
+ break;
+ }
+ if (ContentIsElementSettingTheStyle(aHTMLEditor, *parent)) {
+ mostDistantEndParentHavingStyle = parent;
+ }
+ // If we're setting <font> element and there is a <font> element which is
+ // entirely selected, we should use it.
+ else if (isSettingFontElement && parent->IsHTMLElement(nsGkAtoms::font)) {
+ mostDistantEndParentHavingStyle = parent;
+ }
+ if (parent->GetNextSibling()) {
+ break; // The parent is not last element in its parent, stop climbing.
+ }
+ }
+ if (mostDistantEndParentHavingStyle) {
+ endPoint.SetAfter(mostDistantEndParentHavingStyle);
+ }
+ return endPoint;
+}
+
+EditorRawDOMRange HTMLEditor::AutoInlineStyleSetter::
+ GetExtendedRangeToMinimizeTheNumberOfNewElements(
+ const HTMLEditor& aHTMLEditor, const nsINode& aCommonAncestor,
+ EditorRawDOMPoint&& aStartPoint, EditorRawDOMPoint&& aEndPoint) const {
+ MOZ_ASSERT(aStartPoint.IsSet());
+ MOZ_ASSERT(aEndPoint.IsSet());
+
+ // For minimizing the number of new elements, we should extend the range as
+ // far as possible. E.g., `<span>[abc</span> <span>def]</span>` should be
+ // styled as `<b><span>abc</span> <span>def</span></b>`.
+ // Similarly, if the range crosses a block boundary, we should do same thing.
+ // I.e., `<p><span>[abc</span></p><p><span>def]</span></p>` should become
+ // `<p><b><span>abc</span></b></p><p><b><span>def</span></b></p>`.
+ if (aStartPoint.GetContainer() != aEndPoint.GetContainer()) {
+ while (aStartPoint.GetContainer() != &aCommonAncestor &&
+ aStartPoint.IsInContentNode() && aStartPoint.GetContainerParent() &&
+ aStartPoint.IsStartOfContainer()) {
+ if (!EditorUtils::IsEditableContent(
+ *aStartPoint.ContainerAs<nsIContent>(), EditorType::HTML) ||
+ (aStartPoint.ContainerAs<nsIContent>()->IsElement() &&
+ (HTMLEditUtils::IsBlockElement(
+ *aStartPoint.ContainerAs<Element>(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ HTMLEditUtils::IsDisplayInsideFlowRoot(
+ *aStartPoint.ContainerAs<Element>())))) {
+ break;
+ }
+ aStartPoint = aStartPoint.ParentPoint();
+ }
+ while (aEndPoint.GetContainer() != &aCommonAncestor &&
+ aEndPoint.IsInContentNode() && aEndPoint.GetContainerParent() &&
+ aEndPoint.IsEndOfContainer()) {
+ if (!EditorUtils::IsEditableContent(*aEndPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML) ||
+ (aEndPoint.ContainerAs<nsIContent>()->IsElement() &&
+ (HTMLEditUtils::IsBlockElement(
+ *aEndPoint.ContainerAs<Element>(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ HTMLEditUtils::IsDisplayInsideFlowRoot(
+ *aEndPoint.ContainerAs<Element>())))) {
+ break;
+ }
+ aEndPoint.SetAfter(aEndPoint.ContainerAs<nsIContent>());
+ }
+ }
+
+ // Additionally, if we'll set a CSS style, we want to wrap elements which
+ // should have the new style into the range to avoid creating new <span>
+ // element.
+ if (!IsRepresentableWithHTML() ||
+ (aHTMLEditor.IsCSSEnabled() && IsCSSSettable(*nsGkAtoms::span))) {
+ // First, if pointing in a text node, use parent point.
+ if (aStartPoint.IsInContentNode() && aStartPoint.IsStartOfContainer() &&
+ aStartPoint.GetContainerParentAs<nsIContent>() &&
+ EditorUtils::IsEditableContent(
+ *aStartPoint.ContainerParentAs<nsIContent>(), EditorType::HTML) &&
+ (!aStartPoint.GetContainerAs<Element>() ||
+ !HTMLEditUtils::IsContainerNode(
+ *aStartPoint.ContainerAs<nsIContent>())) &&
+ EditorUtils::IsEditableContent(*aStartPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML)) {
+ aStartPoint = aStartPoint.ParentPoint();
+ MOZ_ASSERT(aStartPoint.IsSet());
+ }
+ if (aEndPoint.IsInContentNode() && aEndPoint.IsEndOfContainer() &&
+ aEndPoint.GetContainerParentAs<nsIContent>() &&
+ EditorUtils::IsEditableContent(
+ *aEndPoint.ContainerParentAs<nsIContent>(), EditorType::HTML) &&
+ (!aEndPoint.GetContainerAs<Element>() ||
+ !HTMLEditUtils::IsContainerNode(
+ *aEndPoint.ContainerAs<nsIContent>())) &&
+ EditorUtils::IsEditableContent(*aEndPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML)) {
+ aEndPoint.SetAfter(aEndPoint.GetContainer());
+ MOZ_ASSERT(aEndPoint.IsSet());
+ }
+ // Then, wrap the container if it's a good element to set a CSS property.
+ if (aStartPoint.IsInContentNode() && aStartPoint.GetContainerParent() &&
+ // The point must be start of the container
+ aStartPoint.IsStartOfContainer() &&
+ // only if the pointing first child node cannot have `style` attribute
+ (!aStartPoint.GetChildAs<nsStyledElement>() ||
+ !ElementIsGoodContainerToSetStyle(
+ *aStartPoint.ChildAs<nsStyledElement>())) &&
+ // but don't cross block boundary at climbing up the tree
+ !HTMLEditUtils::IsBlockElement(
+ *aStartPoint.ContainerAs<nsIContent>(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle) &&
+ // and the container is a good editable element to set CSS style
+ aStartPoint.GetContainerAs<nsStyledElement>() &&
+ ElementIsGoodContainerToSetStyle(
+ *aStartPoint.ContainerAs<nsStyledElement>())) {
+ aStartPoint = aStartPoint.ParentPoint();
+ MOZ_ASSERT(aStartPoint.IsSet());
+ }
+ if (aEndPoint.IsInContentNode() && aEndPoint.GetContainerParent() &&
+ // The point must be end of the container
+ aEndPoint.IsEndOfContainer() &&
+ // only if the pointing last child node cannot have `style` attribute
+ (aEndPoint.IsStartOfContainer() ||
+ !aEndPoint.GetPreviousSiblingOfChildAs<nsStyledElement>() ||
+ !ElementIsGoodContainerToSetStyle(
+ *aEndPoint.GetPreviousSiblingOfChildAs<nsStyledElement>())) &&
+ // but don't cross block boundary at climbing up the tree
+ !HTMLEditUtils::IsBlockElement(
+ *aEndPoint.ContainerAs<nsIContent>(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle) &&
+ // and the container is a good editable element to set CSS style
+ aEndPoint.GetContainerAs<nsStyledElement>() &&
+ ElementIsGoodContainerToSetStyle(
+ *aEndPoint.ContainerAs<nsStyledElement>())) {
+ aEndPoint.SetAfter(aEndPoint.GetContainer());
+ MOZ_ASSERT(aEndPoint.IsSet());
+ }
+ }
+
+ return EditorRawDOMRange(std::move(aStartPoint), std::move(aEndPoint));
+}
+
+Result<EditorRawDOMRange, nsresult>
+HTMLEditor::AutoInlineStyleSetter::ExtendOrShrinkRangeToApplyTheStyle(
+ const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
+ const Element& aEditingHost) const {
+ if (NS_WARN_IF(!aRange.IsPositioned())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // For avoiding assertion hits in the utility methods, check whether the
+ // range is in same subtree, first. Even if the range crosses a subtree
+ // boundary, it's not a bug of this module.
+ nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor();
+ if (NS_WARN_IF(!commonAncestor)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // If the range does not select only invisible <br> element, let's extend the
+ // range to contain the <br> element.
+ EditorDOMRange range(aRange);
+ if (range.EndRef().IsInContentNode()) {
+ WSScanResult nextContentData =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost, range.EndRef(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (nextContentData.ReachedInvisibleBRElement() &&
+ nextContentData.BRElementPtr()->GetParentElement() &&
+ HTMLEditUtils::IsInlineContent(
+ *nextContentData.BRElementPtr()->GetParentElement(),
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ range.SetEnd(EditorDOMPoint::After(*nextContentData.BRElementPtr()));
+ MOZ_ASSERT(range.EndRef().IsSet());
+ commonAncestor = range.GetClosestCommonInclusiveAncestor();
+ if (NS_WARN_IF(!commonAncestor)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ }
+
+ // If the range is collapsed, we don't want to replace ancestors unless it's
+ // in an empty element.
+ if (range.Collapsed() && range.StartRef().GetContainer()->Length()) {
+ return EditorRawDOMRange(range);
+ }
+
+ // First, shrink the given range to minimize new style applied contents.
+ // However, we should not shrink the range into entirely selected element.
+ // E.g., if `abc[<i>def</i>]ghi`, shouldn't shrink it as
+ // `abc<i>[def]</i>ghi`.
+ EditorRawDOMPoint startPoint, endPoint;
+ if (range.Collapsed()) {
+ startPoint = endPoint = range.StartRef().To<EditorRawDOMPoint>();
+ } else {
+ ContentSubtreeIterator iter;
+ if (NS_FAILED(iter.Init(range.StartRef().ToRawRangeBoundary(),
+ range.EndRef().ToRawRangeBoundary()))) {
+ NS_WARNING("ContentSubtreeIterator::Init() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsIContent* const firstContentEntirelyInRange =
+ nsIContent::FromNodeOrNull(iter.GetCurrentNode());
+ nsIContent* const lastContentEntirelyInRange = [&]() {
+ iter.Last();
+ return nsIContent::FromNodeOrNull(iter.GetCurrentNode());
+ }();
+
+ // Compute the shrunken range boundaries.
+ startPoint = GetShrunkenRangeStart(aHTMLEditor, range, *commonAncestor,
+ firstContentEntirelyInRange);
+ MOZ_ASSERT(startPoint.IsSet());
+ endPoint = GetShrunkenRangeEnd(aHTMLEditor, range, *commonAncestor,
+ lastContentEntirelyInRange);
+ MOZ_ASSERT(endPoint.IsSet());
+
+ // If shrunken range is swapped, it could like this case:
+ // `abc[</span><span>]def`, starts at very end of a node and ends at
+ // very start of immediately next node. In this case, we should use
+ // the original range instead.
+ if (MOZ_UNLIKELY(!startPoint.EqualsOrIsBefore(endPoint))) {
+ startPoint = range.StartRef().To<EditorRawDOMPoint>();
+ endPoint = range.EndRef().To<EditorRawDOMPoint>();
+ }
+ }
+
+ // Then, we may need to extend the range to wrap parent inline elements
+ // which specify same style since we need to remove same style elements to
+ // apply new value. E.g., abc
+ // <span style="background-color: red">
+ // <span style="background-color: blue">[def]</span>
+ // </span>
+ // ghi
+ // In this case, we need to wrap the other <span> element if setting
+ // background color. Then, the inner <span> element is removed and the
+ // other <span> element's style attribute will be updated rather than
+ // inserting new <span> element.
+ startPoint = GetExtendedRangeStartToWrapAncestorApplyingSameStyle(aHTMLEditor,
+ startPoint);
+ MOZ_ASSERT(startPoint.IsSet());
+ endPoint =
+ GetExtendedRangeEndToWrapAncestorApplyingSameStyle(aHTMLEditor, endPoint);
+ MOZ_ASSERT(endPoint.IsSet());
+
+ // Finally, we need to extend the range unless the range is in an element to
+ // reduce the number of creating new elements. E.g., if now selects
+ // `<span>[abc</span><span>def]</span>`, we should make it
+ // `<b><span>abc</span><span>def</span></b>` rather than
+ // `<span><b>abc</b></span><span><b>def</b></span>`.
+ EditorRawDOMRange finalRange =
+ GetExtendedRangeToMinimizeTheNumberOfNewElements(
+ aHTMLEditor, *commonAncestor, std::move(startPoint),
+ std::move(endPoint));
+#if 0
+ fprintf(stderr,
+ "ExtendOrShrinkRangeToApplyTheStyle:\n"
+ " Result: {(\n %s\n ) - (\n %s\n )},\n"
+ " Input: {(\n %s\n ) - (\n %s\n )}\n",
+ ToString(finalRange.StartRef()).c_str(),
+ ToString(finalRange.EndRef()).c_str(),
+ ToString(aRange.StartRef()).c_str(),
+ ToString(aRange.EndRef()).c_str());
+#endif
+ return finalRange;
+}
+
+Result<SplitRangeOffResult, nsresult>
+HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges(
+ const EditorDOMRange& aRange, const EditorInlineStyle& aStyle,
+ SplitAtEdges aSplitAtEdges) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aRange.IsPositioned())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ EditorDOMRange range(aRange);
+
+ // split any matching style nodes above the start of range
+ auto resultAtStart =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
+ AutoTrackDOMRange tracker(RangeUpdaterRef(), &range);
+ Result<SplitNodeResult, nsresult> result =
+ SplitAncestorStyledInlineElementsAt(range.StartRef(), aStyle,
+ aSplitAtEdges);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
+ return result;
+ }
+ tracker.FlushAndStopTracking();
+ if (result.inspect().Handled()) {
+ auto startOfRange = result.inspect().AtSplitPoint<EditorDOMPoint>();
+ if (!startOfRange.IsSet()) {
+ result.inspect().IgnoreCaretPointSuggestion();
+ NS_WARNING(
+ "HTMLEditor::SplitAncestorStyledInlineElementsAt() didn't return "
+ "split point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ range.SetStart(std::move(startOfRange));
+ } else if (MOZ_UNLIKELY(!range.IsPositioned())) {
+ NS_WARNING(
+ "HTMLEditor::SplitAncestorStyledInlineElementsAt() caused unexpected "
+ "DOM tree");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ return result;
+ }();
+ if (MOZ_UNLIKELY(resultAtStart.isErr())) {
+ return resultAtStart.propagateErr();
+ }
+ SplitNodeResult unwrappedResultAtStart = resultAtStart.unwrap();
+
+ // second verse, same as the first...
+ auto resultAtEnd =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
+ AutoTrackDOMRange tracker(RangeUpdaterRef(), &range);
+ Result<SplitNodeResult, nsresult> result =
+ SplitAncestorStyledInlineElementsAt(range.EndRef(), aStyle,
+ aSplitAtEdges);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
+ return result;
+ }
+ tracker.FlushAndStopTracking();
+ if (NS_WARN_IF(result.inspect().Handled())) {
+ auto endOfRange = result.inspect().AtSplitPoint<EditorDOMPoint>();
+ if (!endOfRange.IsSet()) {
+ result.inspect().IgnoreCaretPointSuggestion();
+ NS_WARNING(
+ "HTMLEditor::SplitAncestorStyledInlineElementsAt() didn't return "
+ "split point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ range.SetEnd(std::move(endOfRange));
+ } else if (MOZ_UNLIKELY(!range.IsPositioned())) {
+ NS_WARNING(
+ "HTMLEditor::SplitAncestorStyledInlineElementsAt() caused unexpected "
+ "DOM tree");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ return result;
+ }();
+ if (MOZ_UNLIKELY(resultAtEnd.isErr())) {
+ unwrappedResultAtStart.IgnoreCaretPointSuggestion();
+ return resultAtEnd.propagateErr();
+ }
+
+ return SplitRangeOffResult(std::move(range),
+ std::move(unwrappedResultAtStart),
+ resultAtEnd.unwrap());
+}
+
+Result<SplitNodeResult, nsresult>
+HTMLEditor::SplitAncestorStyledInlineElementsAt(
+ const EditorDOMPoint& aPointToSplit, const EditorInlineStyle& aStyle,
+ SplitAtEdges aSplitAtEdges) {
+ // If the point is in a non-content node, e.g., in the document node, we
+ // should split nothing.
+ if (MOZ_UNLIKELY(!aPointToSplit.IsInContentNode())) {
+ return SplitNodeResult::NotHandled(aPointToSplit);
+ }
+
+ // We assume that this method is called only when we're removing style(s).
+ // Even if we're in HTML mode and there is no presentation element in the
+ // block, we may need to overwrite the block's style with `<span>` element
+ // and CSS. For example, `<h1>` element has `font-weight: bold;` as its
+ // default style. If `Document.execCommand("bold")` is called for its
+ // text, we should make it unbold. Therefore, we shouldn't check
+ // IsCSSEnabled() in most cases. However, there is an exception.
+ // FontFaceStateCommand::SetState() calls RemoveInlinePropertyAsAction()
+ // with nsGkAtoms::tt before calling SetInlinePropertyAsAction() if we
+ // are handling a XUL command. Only in that case, we need to check
+ // IsCSSEnabled().
+ const bool handleCSS =
+ aStyle.mHTMLProperty != nsGkAtoms::tt || IsCSSEnabled();
+
+ AutoTArray<OwningNonNull<Element>, 24> arrayOfParents;
+ for (Element* element :
+ aPointToSplit.GetContainer()->InclusiveAncestorsOfType<Element>()) {
+ if (element->IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::head,
+ nsGkAtoms::html) ||
+ HTMLEditUtils::IsBlockElement(
+ *element, BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ !element->GetParent() ||
+ !EditorUtils::IsEditableContent(*element->GetParent(),
+ EditorType::HTML) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(*element))) {
+ break;
+ }
+ arrayOfParents.AppendElement(*element);
+ }
+
+ // Split any matching style nodes above the point.
+ SplitNodeResult result = SplitNodeResult::NotHandled(aPointToSplit);
+ MOZ_ASSERT(!result.Handled());
+ EditorDOMPoint pointToPutCaret;
+ for (OwningNonNull<Element>& element : arrayOfParents) {
+ auto isSetByCSSOrError = [&]() -> Result<bool, nsresult> {
+ if (!handleCSS) {
+ return false;
+ }
+ // The HTML style defined by aStyle has a CSS equivalence in this
+ // implementation for the node; let's check if it carries those CSS
+ // styles
+ if (aStyle.IsCSSRemovable(*element)) {
+ nsAutoString firstValue;
+ Result<bool, nsresult> isSpecifiedByCSSOrError =
+ CSSEditUtils::IsSpecifiedCSSEquivalentTo(*this, *element, aStyle,
+ firstValue);
+ if (MOZ_UNLIKELY(isSpecifiedByCSSOrError.isErr())) {
+ result.IgnoreCaretPointSuggestion();
+ NS_WARNING("CSSEditUtils::IsSpecifiedCSSEquivalentTo() failed");
+ return isSpecifiedByCSSOrError;
+ }
+ if (isSpecifiedByCSSOrError.unwrap()) {
+ return true;
+ }
+ }
+ // If this is <sub> or <sup>, we won't use vertical-align CSS property
+ // because <sub>/<sup> changes font size but neither `vertical-align:
+ // sub` nor `vertical-align: super` changes it (bug 394304 comment 2).
+ // Therefore, they are not equivalents. However, they're obviously
+ // conflict with vertical-align style. Thus, we need to remove ancestor
+ // elements having vertical-align style.
+ if (aStyle.IsStyleConflictingWithVerticalAlign()) {
+ nsAutoString value;
+ nsresult rv = CSSEditUtils::GetSpecifiedProperty(
+ *element, *nsGkAtoms::vertical_align, value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::GetSpecifiedProperty() failed");
+ result.IgnoreCaretPointSuggestion();
+ return Err(rv);
+ }
+ if (!value.IsEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }();
+ if (MOZ_UNLIKELY(isSetByCSSOrError.isErr())) {
+ return isSetByCSSOrError.propagateErr();
+ }
+ if (!isSetByCSSOrError.inspect()) {
+ if (!aStyle.IsStyleToClearAllInlineStyles()) {
+ // If we're removing a link style and the element is an <a href>, we
+ // need to split it.
+ if (aStyle.mHTMLProperty == nsGkAtoms::href &&
+ HTMLEditUtils::IsLink(element)) {
+ }
+ // If we're removing HTML style, we should split only the element
+ // which represents the style.
+ else if (!element->IsHTMLElement(aStyle.mHTMLProperty) ||
+ (aStyle.mAttribute && !element->HasAttr(aStyle.mAttribute))) {
+ continue;
+ }
+ // If we're setting <font> related styles, it means that we're not
+ // toggling the style. In this case, we need to remove parent <font>
+ // elements and/or update parent <font> elements if there are some
+ // elements which have the attribute. However, we should not touch if
+ // the value is same as what the caller setting to keep the DOM tree
+ // as-is as far as possible.
+ if (aStyle.IsStyleOfFontElement() && aStyle.MaybeHasValue()) {
+ const nsAttrValue* const attrValue =
+ element->GetParsedAttr(aStyle.mAttribute);
+ if (attrValue) {
+ if (aStyle.mAttribute == nsGkAtoms::size) {
+ if (nsContentUtils::ParseLegacyFontSize(
+ aStyle.AsInlineStyleAndValue().mAttributeValue) ==
+ attrValue->GetIntegerValue()) {
+ continue;
+ }
+ } else if (aStyle.mAttribute == nsGkAtoms::color) {
+ nsAttrValue newValue;
+ nscolor oldColor, newColor;
+ if (attrValue->GetColorValue(oldColor) &&
+ newValue.ParseColor(
+ aStyle.AsInlineStyleAndValue().mAttributeValue) &&
+ newValue.GetColorValue(newColor) && oldColor == newColor) {
+ continue;
+ }
+ } else if (attrValue->Equals(
+ aStyle.AsInlineStyleAndValue().mAttributeValue,
+ eIgnoreCase)) {
+ continue;
+ }
+ }
+ }
+ }
+ // If aProperty is nullptr, we need to split any style.
+ else if (!EditorUtils::IsEditableContent(element, EditorType::HTML) ||
+ !HTMLEditUtils::IsRemovableInlineStyleElement(*element)) {
+ continue;
+ }
+ }
+
+ // Found a style node we need to split.
+ // XXX If first content is a text node and CSS is enabled, we call this
+ // with text node but in such case, this does nothing, but returns
+ // as handled with setting only previous or next node. If its parent
+ // is a block, we do nothing but return as handled.
+ AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret);
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitNodeDeepWithTransaction(MOZ_KnownLive(element),
+ result.AtSplitPoint<EditorDOMPoint>(),
+ aSplitAtEdges);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitNodeResult;
+ }
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ trackPointToPutCaret.FlushAndStopTracking();
+ unwrappedSplitNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+
+ // If it's not handled, it means that `content` is not a splittable node
+ // like a void element even if it has some children, and the split point
+ // is middle of it.
+ if (!unwrappedSplitNodeResult.Handled()) {
+ continue;
+ }
+ // Respect the last split result which actually did it.
+ if (!result.DidSplit() || unwrappedSplitNodeResult.DidSplit()) {
+ result = unwrappedSplitNodeResult.ToHandledResult();
+ }
+ MOZ_ASSERT(result.Handled());
+ }
+
+ return pointToPutCaret.IsSet()
+ ? SplitNodeResult(std::move(result), std::move(pointToPutCaret))
+ : std::move(result);
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::ClearStyleAt(
+ const EditorDOMPoint& aPoint, const EditorInlineStyle& aStyleToRemove,
+ SpecifiedStyle aSpecifiedStyle) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aPoint.IsSet())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ // TODO: We should rewrite this to stop unnecessary element creation and
+ // deleting it later because it causes the original element may be
+ // removed from the DOM tree even if same element is still in the
+ // DOM tree from point of view of users.
+
+ // First, split inline elements at the point.
+ // E.g., if aStyleToRemove.mHTMLProperty is nsGkAtoms::b and
+ // `<p><b><i>a[]bc</i></b></p>`, we want to make it as
+ // `<p><b><i>a</i></b><b><i>bc</i></b></p>`.
+ EditorDOMPoint pointToPutCaret(aPoint);
+ AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret);
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitAncestorStyledInlineElementsAt(
+ aPoint, aStyleToRemove, SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
+ return splitNodeResult.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ unwrappedSplitNodeResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+
+ // If there is no styled inline elements of aStyleToRemove, we just return the
+ // given point.
+ // E.g., `<p><i>a[]bc</i></p>` for nsGkAtoms::b.
+ if (!unwrappedSplitNodeResult.Handled()) {
+ return pointToPutCaret;
+ }
+
+ // If it did split nodes, but topmost ancestor inline element is split
+ // at start of it, we don't need the empty inline element. Let's remove
+ // it now. Then, we'll get the following DOM tree if there is no "a" in the
+ // above case:
+ // <p><b><i>bc</i></b></p>
+ // ^^
+ if (unwrappedSplitNodeResult.GetPreviousContent() &&
+ HTMLEditUtils::IsEmptyNode(
+ *unwrappedSplitNodeResult.GetPreviousContent(),
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible})) {
+ AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret);
+ // Delete previous node if it's empty.
+ // MOZ_KnownLive(unwrappedSplitNodeResult.GetPreviousContent()):
+ // It's grabbed by unwrappedSplitNodeResult.
+ nsresult rv = DeleteNodeWithTransaction(
+ MOZ_KnownLive(*unwrappedSplitNodeResult.GetPreviousContent()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ // If we reached block from end of a text node, we can do nothing here.
+ // E.g., `<p style="font-weight: bold;">a[]bc</p>` for nsGkAtoms::b and
+ // we're in CSS mode.
+ // XXX Chrome resets block style and creates `<span>` elements for each
+ // line in this case.
+ if (!unwrappedSplitNodeResult.GetNextContent()) {
+ return pointToPutCaret;
+ }
+
+ // Otherwise, the next node is topmost ancestor inline element which has
+ // the style. We want to put caret between the split nodes, but we need
+ // to keep other styles. Therefore, next, we need to split at start of
+ // the next node. The first example should become
+ // `<p><b><i>a</i></b><b><i></i></b><b><i>bc</i></b></p>`.
+ // ^^^^^^^^^^^^^^
+ nsIContent* firstLeafChildOfNextNode = HTMLEditUtils::GetFirstLeafContent(
+ *unwrappedSplitNodeResult.GetNextContent(), {LeafNodeType::OnlyLeafNode});
+ EditorDOMPoint atStartOfNextNode(
+ firstLeafChildOfNextNode ? firstLeafChildOfNextNode
+ : unwrappedSplitNodeResult.GetNextContent(),
+ 0);
+ RefPtr<HTMLBRElement> brElement;
+ // But don't try to split non-containers like `<br>`, `<hr>` and `<img>`
+ // element.
+ if (!atStartOfNextNode.IsInContentNode() ||
+ !HTMLEditUtils::IsContainerNode(
+ *atStartOfNextNode.ContainerAs<nsIContent>())) {
+ // If it's a `<br>` element, let's move it into new node later.
+ brElement = HTMLBRElement::FromNode(atStartOfNextNode.GetContainer());
+ if (!atStartOfNextNode.GetContainerParentAs<nsIContent>()) {
+ NS_WARNING("atStartOfNextNode was in an orphan node");
+ return Err(NS_ERROR_FAILURE);
+ }
+ atStartOfNextNode.Set(atStartOfNextNode.GetContainerParent(), 0);
+ }
+ AutoTrackDOMPoint trackPointToPutCaret2(RangeUpdaterRef(), &pointToPutCaret);
+ Result<SplitNodeResult, nsresult> splitResultAtStartOfNextNode =
+ SplitAncestorStyledInlineElementsAt(
+ atStartOfNextNode, aStyleToRemove,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitResultAtStartOfNextNode.isErr())) {
+ NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
+ return splitResultAtStartOfNextNode.propagateErr();
+ }
+ trackPointToPutCaret2.FlushAndStopTracking();
+ SplitNodeResult unwrappedSplitResultAtStartOfNextNode =
+ splitResultAtStartOfNextNode.unwrap();
+ unwrappedSplitResultAtStartOfNextNode.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+
+ if (unwrappedSplitResultAtStartOfNextNode.Handled() &&
+ unwrappedSplitResultAtStartOfNextNode.GetNextContent()) {
+ // If the right inline elements are empty, we should remove them. E.g.,
+ // if the split point is at end of a text node (or end of an inline
+ // element), e.g., <div><b><i>abc[]</i></b></div>, then now, it's been
+ // changed to:
+ // <div><b><i>abc</i></b><b><i>[]</i></b><b><i></i></b></div>
+ // ^^^^^^^^^^^^^^
+ // We will change it to:
+ // <div><b><i>abc</i></b><b><i>[]</i></b></div>
+ // ^^
+ // And if it has only padding <br> element, we should move it into the
+ // previous <i> which will have new content.
+ bool seenBR = false;
+ if (HTMLEditUtils::IsEmptyNode(
+ *unwrappedSplitResultAtStartOfNextNode.GetNextContent(),
+ {EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible},
+ &seenBR)) {
+ if (seenBR && !brElement) {
+ brElement = HTMLEditUtils::GetFirstBRElement(
+ *unwrappedSplitResultAtStartOfNextNode.GetNextContentAs<Element>());
+ }
+ // Once we remove <br> element's parent, we lose the rights to remove it
+ // from the parent because the parent becomes not editable. Therefore, we
+ // need to delete the <br> element before removing its parents for reusing
+ // it later.
+ if (brElement) {
+ nsresult rv = DeleteNodeWithTransaction(*brElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ // Delete next node if it's empty.
+ // MOZ_KnownLive because of grabbed by
+ // unwrappedSplitResultAtStartOfNextNode.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(
+ *unwrappedSplitResultAtStartOfNextNode.GetNextContent()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ }
+
+ if (!unwrappedSplitResultAtStartOfNextNode.Handled()) {
+ return std::move(pointToPutCaret);
+ }
+
+ // If there is no content, we should return here.
+ // XXX Is this possible case without mutation event listener?
+ if (!unwrappedSplitResultAtStartOfNextNode.GetPreviousContent()) {
+ // XXX This is really odd, but we return this value...
+ const auto splitPoint =
+ unwrappedSplitNodeResult.AtSplitPoint<EditorRawDOMPoint>();
+ const auto splitPointAtStartOfNextNode =
+ unwrappedSplitResultAtStartOfNextNode.AtSplitPoint<EditorRawDOMPoint>();
+ return EditorDOMPoint(splitPoint.GetContainer(),
+ splitPointAtStartOfNextNode.Offset());
+ }
+
+ // Now, we want to put `<br>` element into the empty split node if
+ // it was in next node of the first split.
+ // E.g., `<p><b><i>a</i></b><b><i><br></i></b><b><i>bc</i></b></p>`
+ nsIContent* firstLeafChildOfPreviousNode = HTMLEditUtils::GetFirstLeafContent(
+ *unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(),
+ {LeafNodeType::OnlyLeafNode});
+ pointToPutCaret.Set(
+ firstLeafChildOfPreviousNode
+ ? firstLeafChildOfPreviousNode
+ : unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(),
+ 0);
+
+ // If the right node starts with a `<br>`, suck it out of right node and into
+ // the left node left node. This is so we you don't revert back to the
+ // previous style if you happen to click at the end of a line.
+ if (brElement) {
+ if (brElement->GetParentNode()) {
+ Result<MoveNodeResult, nsresult> moveBRElementResult =
+ MoveNodeWithTransaction(*brElement, pointToPutCaret);
+ if (MOZ_UNLIKELY(moveBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveBRElementResult.propagateErr();
+ }
+ moveBRElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ } else {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertNodeWithTransaction<Element>(*brElement, pointToPutCaret);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ insertBRElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+
+ if (unwrappedSplitResultAtStartOfNextNode.GetNextContent() &&
+ unwrappedSplitResultAtStartOfNextNode.GetNextContent()
+ ->IsInComposedDoc()) {
+ // If we split inline elements at immediately before <br> element which is
+ // the last visible content in the right element, we don't need the right
+ // element anymore. Otherwise, we'll create the following DOM tree:
+ // - <b>abc</b>{}<br><b></b>
+ // ^^^^^^^
+ // - <b><i>abc</i></b><i><br></i><b></b>
+ // ^^^^^^^
+ if (HTMLEditUtils::IsEmptyNode(
+ *unwrappedSplitResultAtStartOfNextNode.GetNextContent(),
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible})) {
+ // MOZ_KnownLive because the result is grabbed by
+ // unwrappedSplitResultAtStartOfNextNode.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(
+ *unwrappedSplitResultAtStartOfNextNode.GetNextContent()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ // If the next content has only one <br> element, there may be empty
+ // inline elements around it. We don't need them anymore because user
+ // cannot put caret into them. E.g., <b><i>abc[]<br></i><br></b> has
+ // been changed to <b><i>abc</i></b><i>{}<br></i><b><i></i><br></b> now.
+ // ^^^^^^^^^^^^^^^^^^
+ // We don't need the empty <i>.
+ else if (HTMLEditUtils::IsEmptyNode(
+ *unwrappedSplitResultAtStartOfNextNode.GetNextContent(),
+ {EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible})) {
+ AutoTArray<OwningNonNull<nsIContent>, 4> emptyInlineContainerElements;
+ HTMLEditUtils::CollectEmptyInlineContainerDescendants(
+ *unwrappedSplitResultAtStartOfNextNode.GetNextContentAs<Element>(),
+ emptyInlineContainerElements,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ for (const OwningNonNull<nsIContent>& emptyInlineContainerElement :
+ emptyInlineContainerElements) {
+ // MOZ_KnownLive(emptyInlineContainerElement) due to bug 1622253.
+ nsresult rv = DeleteNodeWithTransaction(
+ MOZ_KnownLive(emptyInlineContainerElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ }
+ }
+
+ // Update the child.
+ pointToPutCaret.Set(pointToPutCaret.GetContainer(), 0);
+ }
+ // Finally, remove the specified style in the previous node at the
+ // second split and tells good insertion point to the caller. I.e., we
+ // want to make the first example as:
+ // `<p><b><i>a</i></b><i>[]</i><b><i>bc</i></b></p>`
+ // ^^^^^^^^^
+ if (auto* const previousElementOfSplitPoint =
+ unwrappedSplitResultAtStartOfNextNode
+ .GetPreviousContentAs<Element>()) {
+ // Track the point at the new hierarchy. This is so we can know where
+ // to put the selection after we call RemoveStyleInside().
+ // RemoveStyleInside() could remove any and all of those nodes, so I
+ // have to use the range tracking system to find the right spot to put
+ // selection.
+ AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToPutCaret);
+ // MOZ_KnownLive(previousElementOfSplitPoint):
+ // It's grabbed by unwrappedSplitResultAtStartOfNextNode.
+ Result<EditorDOMPoint, nsresult> removeStyleResult =
+ RemoveStyleInside(MOZ_KnownLive(*previousElementOfSplitPoint),
+ aStyleToRemove, aSpecifiedStyle);
+ if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
+ return removeStyleResult;
+ }
+ // We've already computed a suggested caret position at start of first leaf
+ // which is stored in pointToPutCaret, so we don't need to update it here.
+ }
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::RemoveStyleInside(
+ Element& aElement, const EditorInlineStyle& aStyleToRemove,
+ SpecifiedStyle aSpecifiedStyle) {
+ // First, handle all descendants.
+ AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfChildContents;
+ HTMLEditUtils::CollectAllChildren(aElement, arrayOfChildContents);
+ EditorDOMPoint pointToPutCaret;
+ for (const OwningNonNull<nsIContent>& child : arrayOfChildContents) {
+ if (!child->IsElement()) {
+ continue;
+ }
+ Result<EditorDOMPoint, nsresult> removeStyleResult = RemoveStyleInside(
+ MOZ_KnownLive(*child->AsElement()), aStyleToRemove, aSpecifiedStyle);
+ if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
+ return removeStyleResult;
+ }
+ if (removeStyleResult.inspect().IsSet()) {
+ pointToPutCaret = removeStyleResult.unwrap();
+ }
+ }
+
+ // TODO: It seems that if aElement is not editable, we should insert new
+ // container to remove the style if possible.
+ if (!EditorUtils::IsEditableContent(aElement, EditorType::HTML)) {
+ return pointToPutCaret;
+ }
+
+ // Next, remove CSS style first. Then, `style` attribute will be removed if
+ // the corresponding CSS property is last one.
+ auto isStyleSpecifiedOrError = [&]() -> Result<bool, nsresult> {
+ if (!aStyleToRemove.IsCSSRemovable(aElement)) {
+ return false;
+ }
+ MOZ_ASSERT(!aStyleToRemove.IsStyleToClearAllInlineStyles());
+ Result<bool, nsresult> elementHasSpecifiedCSSEquivalentStylesOrError =
+ CSSEditUtils::HaveSpecifiedCSSEquivalentStyles(*this, aElement,
+ aStyleToRemove);
+ NS_WARNING_ASSERTION(
+ elementHasSpecifiedCSSEquivalentStylesOrError.isOk(),
+ "CSSEditUtils::HaveSpecifiedCSSEquivalentStyles() failed");
+ return elementHasSpecifiedCSSEquivalentStylesOrError;
+ }();
+ if (MOZ_UNLIKELY(isStyleSpecifiedOrError.isErr())) {
+ return isStyleSpecifiedOrError.propagateErr();
+ }
+ bool styleSpecified = isStyleSpecifiedOrError.unwrap();
+ if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) {
+ if (styleSpecified) {
+ // MOZ_KnownLive(*styledElement) because it's an alias of aElement.
+ nsresult rv = CSSEditUtils::RemoveCSSEquivalentToStyle(
+ WithTransaction::Yes, *this, MOZ_KnownLive(*styledElement),
+ aStyleToRemove, nullptr);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSEquivalentToStyle() failed, but ignored");
+ }
+
+ // If the style is <sub> or <sup>, we won't use vertical-align CSS
+ // property because <sub>/<sup> changes font size but neither
+ // `vertical-align: sub` nor `vertical-align: super` changes it
+ // (bug 394304 comment 2). Therefore, they are not equivalents. However,
+ // they're obviously conflict with vertical-align style. Thus, we need to
+ // remove the vertical-align style from elements.
+ if (aStyleToRemove.IsStyleConflictingWithVerticalAlign()) {
+ nsAutoString value;
+ nsresult rv = CSSEditUtils::GetSpecifiedProperty(
+ aElement, *nsGkAtoms::vertical_align, value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::GetSpecifiedProperty() failed");
+ return Err(rv);
+ }
+ if (!value.IsEmpty()) {
+ // MOZ_KnownLive(*styledElement) because it's an alias of aElement.
+ nsresult rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::vertical_align,
+ value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CSSEditUtils::RemoveCSSPropertyWithTransaction() failed");
+ return Err(rv);
+ }
+ styleSpecified = true;
+ }
+ }
+ }
+
+ // Then, if we could and should remove or replace aElement, let's do it. Or
+ // just remove attribute.
+ const bool isStyleRepresentedByElement =
+ !aStyleToRemove.IsStyleToClearAllInlineStyles() &&
+ aStyleToRemove.IsRepresentedBy(aElement);
+
+ auto ShouldUpdateDOMTree = [&]() {
+ // If we're removing any inline styles and aElement is an inline style
+ // element, we can remove or replace it.
+ if (aStyleToRemove.IsStyleToClearAllInlineStyles() &&
+ HTMLEditUtils::IsRemovableInlineStyleElement(aElement)) {
+ return true;
+ }
+ // If we're a specific style and aElement represents it, we can remove or
+ // replace the element or remove the corresponding attribute.
+ if (isStyleRepresentedByElement) {
+ return true;
+ }
+ // If we've removed a CSS style from the `style` attribute of aElement, we
+ // could remove the element.
+ return aElement.IsHTMLElement(nsGkAtoms::span) && styleSpecified;
+ };
+ if (!ShouldUpdateDOMTree()) {
+ return pointToPutCaret;
+ }
+
+ const bool elementHasNecessaryAttributes = [&]() {
+ // If we're not removing nor replacing aElement itself, we don't need to
+ // take care of its `style` and `class` attributes even if aSpecifiedStyle
+ // is `Discard` because aSpecifiedStyle is not intended to be used in this
+ // case.
+ if (!isStyleRepresentedByElement) {
+ return HTMLEditUtils::ElementHasAttributeExcept(aElement,
+ *nsGkAtoms::_empty);
+ }
+ // If we're removing links, we don't need to keep <a> even if it has some
+ // specific attributes because it cannot be nested. However, if and only if
+ // it has `style` attribute and aSpecifiedStyle is not `Discard`, we need to
+ // replace it with new <span> to keep the style.
+ if (aStyleToRemove.IsStyleOfAnchorElement()) {
+ return aSpecifiedStyle == SpecifiedStyle::Preserve &&
+ (aElement.HasNonEmptyAttr(nsGkAtoms::style) ||
+ aElement.HasNonEmptyAttr(nsGkAtoms::_class));
+ }
+ nsAtom& attrKeepStaying = aStyleToRemove.mAttribute
+ ? *aStyleToRemove.mAttribute
+ : *nsGkAtoms::_empty;
+ return aSpecifiedStyle == SpecifiedStyle::Preserve
+ // If we're try to remove the element but the caller wants to
+ // preserve the style, check whether aElement has attributes
+ // except the removing attribute since `style` and `class` should
+ // keep existing to preserve the style.
+ ? HTMLEditUtils::ElementHasAttributeExcept(aElement,
+ attrKeepStaying)
+ // If we're try to remove the element and the caller wants to
+ // discard the style specified to the element, check whether
+ // aElement has attributes except the removing attribute, `style`
+ // and `class` since we don't want to keep these attributes.
+ : HTMLEditUtils::ElementHasAttributeExcept(
+ aElement, attrKeepStaying, *nsGkAtoms::style,
+ *nsGkAtoms::_class);
+ }();
+
+ // If the element is not a <span> and still has some attributes, we should
+ // replace it with new <span>.
+ auto ReplaceWithNewSpan = [&]() {
+ if (aStyleToRemove.IsStyleToClearAllInlineStyles()) {
+ return false; // Remove it even if it has attributes.
+ }
+ if (aElement.IsHTMLElement(nsGkAtoms::span)) {
+ return false; // Don't replace <span> with new <span>.
+ }
+ if (!isStyleRepresentedByElement) {
+ return false; // Keep non-related element as-is.
+ }
+ if (!elementHasNecessaryAttributes) {
+ return false; // Should remove it instead of replacing it.
+ }
+ if (aElement.IsHTMLElement(nsGkAtoms::font)) {
+ // Replace <font> if it won't have its specific attributes.
+ return (aStyleToRemove.mHTMLProperty == nsGkAtoms::color ||
+ !aElement.HasAttr(nsGkAtoms::color)) &&
+ (aStyleToRemove.mHTMLProperty == nsGkAtoms::face ||
+ !aElement.HasAttr(nsGkAtoms::face)) &&
+ (aStyleToRemove.mHTMLProperty == nsGkAtoms::size ||
+ !aElement.HasAttr(nsGkAtoms::size));
+ }
+ // The styled element has only global attributes, let's replace it with new
+ // <span> with cloning the attributes.
+ return true;
+ };
+
+ if (ReplaceWithNewSpan()) {
+ // Before cloning the attribute to new element, let's remove it.
+ if (aStyleToRemove.mAttribute) {
+ nsresult rv =
+ RemoveAttributeWithTransaction(aElement, *aStyleToRemove.mAttribute);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::RemoveAttributeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ if (aSpecifiedStyle == SpecifiedStyle::Discard) {
+ nsresult rv = RemoveAttributeWithTransaction(aElement, *nsGkAtoms::style);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::style) "
+ "failed");
+ return Err(rv);
+ }
+ rv = RemoveAttributeWithTransaction(aElement, *nsGkAtoms::_class);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::_class) "
+ "failed");
+ return Err(rv);
+ }
+ }
+ // Move `style` attribute and `class` element to span element before
+ // removing aElement from the tree.
+ auto replaceWithSpanResult =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<CreateElementResult, nsresult> {
+ if (!aStyleToRemove.IsStyleOfAnchorElement()) {
+ return ReplaceContainerAndCloneAttributesWithTransaction(
+ aElement, *nsGkAtoms::span);
+ }
+ nsString styleValue; // Use nsString to avoid copying the buffer at
+ // setting the attribute.
+ aElement.GetAttr(nsGkAtoms::style, styleValue);
+ return ReplaceContainerWithTransaction(aElement, *nsGkAtoms::span,
+ *nsGkAtoms::style, styleValue);
+ }();
+ if (MOZ_UNLIKELY(replaceWithSpanResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceContainerWithTransaction(nsGkAtoms::span) "
+ "failed");
+ return replaceWithSpanResult.propagateErr();
+ }
+ CreateElementResult unwrappedReplaceWithSpanResult =
+ replaceWithSpanResult.unwrap();
+ if (AllowsTransactionsToChangeSelection()) {
+ unwrappedReplaceWithSpanResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ } else {
+ unwrappedReplaceWithSpanResult.IgnoreCaretPointSuggestion();
+ }
+ return pointToPutCaret;
+ }
+
+ auto RemoveElement = [&]() {
+ if (aStyleToRemove.IsStyleToClearAllInlineStyles()) {
+ MOZ_ASSERT(HTMLEditUtils::IsRemovableInlineStyleElement(aElement));
+ return true;
+ }
+ // If the element still has some attributes, we should not remove it to keep
+ // current presentation and/or semantics.
+ if (elementHasNecessaryAttributes) {
+ return false;
+ }
+ // If the style is represented by the element, let's remove it.
+ if (isStyleRepresentedByElement) {
+ return true;
+ }
+ // If we've removed a CSS style and that made the <span> element have no
+ // attributes, we can delete it.
+ if (styleSpecified && aElement.IsHTMLElement(nsGkAtoms::span)) {
+ return true;
+ }
+ return false;
+ };
+
+ if (RemoveElement()) {
+ Result<EditorDOMPoint, nsresult> unwrapElementResult =
+ RemoveContainerWithTransaction(aElement);
+ if (MOZ_UNLIKELY(unwrapElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapElementResult.propagateErr();
+ }
+ if (AllowsTransactionsToChangeSelection() &&
+ unwrapElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapElementResult.unwrap();
+ }
+ return pointToPutCaret;
+ }
+
+ // If the element needs to keep having some attributes, just remove the
+ // attribute. Note that we don't need to remove `style` attribute here when
+ // aSpecifiedStyle is `Discard` because we've already removed unnecessary
+ // CSS style above.
+ if (isStyleRepresentedByElement && aStyleToRemove.mAttribute) {
+ nsresult rv =
+ RemoveAttributeWithTransaction(aElement, *aStyleToRemove.mAttribute);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::RemoveAttributeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ return pointToPutCaret;
+}
+
+EditorRawDOMRange HTMLEditor::GetExtendedRangeWrappingNamedAnchor(
+ const EditorRawDOMRange& aRange) const {
+ MOZ_ASSERT(aRange.StartRef().IsSet());
+ MOZ_ASSERT(aRange.EndRef().IsSet());
+
+ // FYI: We don't want to stop at ancestor block boundaries to extend the range
+ // because <a name> can have block elements with low level DOM API. We want
+ // to remove any <a name> ancestors to remove the style.
+
+ EditorRawDOMRange newRange(aRange);
+ for (Element* element :
+ aRange.StartRef().GetContainer()->InclusiveAncestorsOfType<Element>()) {
+ if (!HTMLEditUtils::IsNamedAnchor(element)) {
+ continue;
+ }
+ newRange.SetStart(EditorRawDOMPoint(element));
+ }
+ for (Element* element :
+ aRange.EndRef().GetContainer()->InclusiveAncestorsOfType<Element>()) {
+ if (!HTMLEditUtils::IsNamedAnchor(element)) {
+ continue;
+ }
+ newRange.SetEnd(EditorRawDOMPoint::After(*element));
+ }
+ return newRange;
+}
+
+EditorRawDOMRange HTMLEditor::GetExtendedRangeWrappingEntirelySelectedElements(
+ const EditorRawDOMRange& aRange) const {
+ MOZ_ASSERT(aRange.StartRef().IsSet());
+ MOZ_ASSERT(aRange.EndRef().IsSet());
+
+ // FYI: We don't want to stop at ancestor block boundaries to extend the range
+ // because the style may come from inline parents of block elements which may
+ // occur in invalid DOM tree. We want to split any (even invalid) ancestors
+ // at removing the styles.
+
+ EditorRawDOMRange newRange(aRange);
+ while (newRange.StartRef().IsInContentNode() &&
+ newRange.StartRef().IsStartOfContainer()) {
+ if (!EditorUtils::IsEditableContent(
+ *newRange.StartRef().ContainerAs<nsIContent>(), EditorType::HTML)) {
+ break;
+ }
+ newRange.SetStart(newRange.StartRef().ParentPoint());
+ }
+ while (newRange.EndRef().IsInContentNode() &&
+ newRange.EndRef().IsEndOfContainer()) {
+ if (!EditorUtils::IsEditableContent(
+ *newRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)) {
+ break;
+ }
+ newRange.SetEnd(
+ EditorRawDOMPoint::After(*newRange.EndRef().ContainerAs<nsIContent>()));
+ }
+ return newRange;
+}
+
+nsresult HTMLEditor::GetInlinePropertyBase(const EditorInlineStyle& aStyle,
+ const nsAString* aValue,
+ bool* aFirst, bool* aAny, bool* aAll,
+ nsAString* outValue) const {
+ MOZ_ASSERT(!aStyle.IsStyleToClearAllInlineStyles());
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ *aAny = false;
+ *aAll = true;
+ *aFirst = false;
+ bool first = true;
+
+ const bool isCollapsed = SelectionRef().IsCollapsed();
+ RefPtr<nsRange> range = SelectionRef().GetRangeAt(0);
+ // XXX: Should be a while loop, to get each separate range
+ // XXX: ERROR_HANDLING can currentItem be null?
+ if (range) {
+ // For each range, set a flag
+ bool firstNodeInRange = true;
+
+ if (isCollapsed) {
+ if (NS_WARN_IF(!range->GetStartContainer())) {
+ return NS_ERROR_FAILURE;
+ }
+ nsString tOutString;
+ const PendingStyleState styleState = [&]() {
+ if (aStyle.mAttribute) {
+ auto state = mPendingStylesToApplyToNewContent->GetStyleState(
+ *aStyle.mHTMLProperty, aStyle.mAttribute, &tOutString);
+ if (outValue) {
+ outValue->Assign(tOutString);
+ }
+ return state;
+ }
+ return mPendingStylesToApplyToNewContent->GetStyleState(
+ *aStyle.mHTMLProperty);
+ }();
+ if (styleState != PendingStyleState::NotUpdated) {
+ *aFirst = *aAny = *aAll =
+ (styleState == PendingStyleState::BeingPreserved);
+ return NS_OK;
+ }
+
+ nsIContent* const collapsedContent =
+ nsIContent::FromNode(range->GetStartContainer());
+ if (MOZ_LIKELY(collapsedContent &&
+ collapsedContent->GetAsElementOrParentElement()) &&
+ aStyle.IsCSSSettable(
+ *collapsedContent->GetAsElementOrParentElement())) {
+ if (aValue) {
+ tOutString.Assign(*aValue);
+ }
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(
+ *this, MOZ_KnownLive(*collapsedContent), aStyle, tOutString);
+ if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
+ NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError.unwrapErr();
+ }
+ *aFirst = *aAny = *aAll =
+ isComputedCSSEquivalentToStyleOrError.unwrap();
+ if (outValue) {
+ outValue->Assign(tOutString);
+ }
+ return NS_OK;
+ }
+
+ *aFirst = *aAny = *aAll =
+ collapsedContent && HTMLEditUtils::IsInlineStyleSetByElement(
+ *collapsedContent, aStyle, aValue, outValue);
+ return NS_OK;
+ }
+
+ // Non-collapsed selection
+
+ nsAutoString firstValue, theValue;
+
+ nsCOMPtr<nsINode> endNode = range->GetEndContainer();
+ uint32_t endOffset = range->EndOffset();
+
+ PostContentIterator postOrderIter;
+ DebugOnly<nsresult> rvIgnored = postOrderIter.Init(range);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to initialize post-order content iterator");
+ for (; !postOrderIter.IsDone(); postOrderIter.Next()) {
+ if (postOrderIter.GetCurrentNode()->IsHTMLElement(nsGkAtoms::body)) {
+ break;
+ }
+ RefPtr<Text> textNode = Text::FromNode(postOrderIter.GetCurrentNode());
+ if (!textNode) {
+ continue;
+ }
+
+ // just ignore any non-editable nodes
+ if (!EditorUtils::IsEditableContent(*textNode, EditorType::HTML) ||
+ !HTMLEditUtils::IsVisibleTextNode(*textNode)) {
+ continue;
+ }
+
+ if (!isCollapsed && first && firstNodeInRange) {
+ firstNodeInRange = false;
+ if (range->StartOffset() == textNode->TextDataLength()) {
+ continue;
+ }
+ } else if (textNode == endNode && !endOffset) {
+ continue;
+ }
+
+ const RefPtr<Element> element = textNode->GetParentElement();
+
+ bool isSet = false;
+ if (first) {
+ if (element) {
+ if (aStyle.IsCSSSettable(*element)) {
+ // The HTML styles defined by aHTMLProperty/aAttribute have a CSS
+ // equivalence in this implementation for node; let's check if it
+ // carries those CSS styles
+ if (aValue) {
+ firstValue.Assign(*aValue);
+ }
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle,
+ firstValue);
+ if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
+ NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError.unwrapErr();
+ }
+ isSet = isComputedCSSEquivalentToStyleOrError.unwrap();
+ } else {
+ isSet = HTMLEditUtils::IsInlineStyleSetByElement(
+ *element, aStyle, aValue, &firstValue);
+ }
+ }
+ *aFirst = isSet;
+ first = false;
+ if (outValue) {
+ *outValue = firstValue;
+ }
+ } else {
+ if (element) {
+ if (aStyle.IsCSSSettable(*element)) {
+ // The HTML styles defined by aHTMLProperty/aAttribute have a CSS
+ // equivalence in this implementation for node; let's check if it
+ // carries those CSS styles
+ if (aValue) {
+ theValue.Assign(*aValue);
+ }
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle,
+ theValue);
+ if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
+ NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError.unwrapErr();
+ }
+ isSet = isComputedCSSEquivalentToStyleOrError.unwrap();
+ } else {
+ isSet = HTMLEditUtils::IsInlineStyleSetByElement(*element, aStyle,
+ aValue, &theValue);
+ }
+ }
+
+ if (firstValue != theValue &&
+ // For text-decoration related HTML properties, i.e. <u> and
+ // <strike>, we have to also check |isSet| because text-decoration
+ // is a shorthand property, and it may contains other unrelated
+ // longhand components, e.g. text-decoration-color, so we have to do
+ // an extra check before setting |*aAll| to false.
+ // e.g.
+ // firstValue: "underline rgb(0, 0, 0)"
+ // theValue: "underline rgb(0, 0, 238)" // <a> uses blue color
+ // These two values should be the same if we are checking `<u>`.
+ // That's why we need to check |*aFirst| and |isSet|.
+ //
+ // This is a work-around for text-decoration.
+ // The spec issue: https://github.com/w3c/editing/issues/241.
+ // Once this spec issue is resolved, we could drop this work-around
+ // check.
+ (!aStyle.IsStyleOfTextDecoration(
+ EditorInlineStyle::IgnoreSElement::Yes) ||
+ *aFirst != isSet)) {
+ *aAll = false;
+ }
+ }
+
+ if (isSet) {
+ *aAny = true;
+ } else {
+ *aAll = false;
+ }
+ }
+ }
+ if (!*aAny) {
+ // make sure that if none of the selection is set, we don't report all is
+ // set
+ *aAll = false;
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::GetInlineProperty(nsStaticAtom& aHTMLProperty,
+ nsAtom* aAttribute,
+ const nsAString& aValue, bool* aFirst,
+ bool* aAny, bool* aAll) const {
+ if (NS_WARN_IF(!aFirst) || NS_WARN_IF(!aAny) || NS_WARN_IF(!aAll)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ const nsAString* val = !aValue.IsEmpty() ? &aValue : nullptr;
+ nsresult rv =
+ GetInlinePropertyBase(EditorInlineStyle(aHTMLProperty, aAttribute), val,
+ aFirst, aAny, aAll, nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetInlinePropertyBase() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::GetInlinePropertyWithAttrValue(
+ const nsAString& aHTMLProperty, const nsAString& aAttribute,
+ const nsAString& aValue, bool* aFirst, bool* aAny, bool* aAll,
+ nsAString& outValue) {
+ nsStaticAtom* property = NS_GetStaticAtom(aHTMLProperty);
+ if (NS_WARN_IF(!property)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute);
+ // MOZ_KnownLive because nsStaticAtom is available until shutting down.
+ nsresult rv = GetInlinePropertyWithAttrValue(MOZ_KnownLive(*property),
+ MOZ_KnownLive(attribute), aValue,
+ aFirst, aAny, aAll, outValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetInlinePropertyWithAttrValue() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::GetInlinePropertyWithAttrValue(
+ nsStaticAtom& aHTMLProperty, nsAtom* aAttribute, const nsAString& aValue,
+ bool* aFirst, bool* aAny, bool* aAll, nsAString& outValue) {
+ if (NS_WARN_IF(!aFirst) || NS_WARN_IF(!aAny) || NS_WARN_IF(!aAll)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ const nsAString* val = !aValue.IsEmpty() ? &aValue : nullptr;
+ nsresult rv =
+ GetInlinePropertyBase(EditorInlineStyle(aHTMLProperty, aAttribute), val,
+ aFirst, aAny, aAll, &outValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetInlinePropertyBase() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::RemoveAllInlinePropertiesAsAction(
+ nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eRemoveAllInlineStyleProperties, aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eRemoveAllTextProperties, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return EditorBase::ToGenericNSResult(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ AutoTArray<EditorInlineStyle, 1> removeAllInlineStyles;
+ removeAllInlineStyles.AppendElement(EditorInlineStyle::RemoveAllStyles());
+ rv = RemoveInlinePropertiesAsSubAction(removeAllInlineStyles);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveInlinePropertiesAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::RemoveInlinePropertyAsAction(nsStaticAtom& aHTMLProperty,
+ nsStaticAtom* aAttribute,
+ nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(
+ *this,
+ HTMLEditUtils::GetEditActionForFormatText(aHTMLProperty, aAttribute,
+ false),
+ aPrincipal);
+ switch (editActionData.GetEditAction()) {
+ case EditAction::eRemoveFontFamilyProperty:
+ MOZ_ASSERT(!u""_ns.IsVoid());
+ editActionData.SetData(u""_ns);
+ break;
+ case EditAction::eRemoveColorProperty:
+ case EditAction::eRemoveBackgroundColorPropertyInline:
+ editActionData.SetColorData(u""_ns);
+ break;
+ default:
+ break;
+ }
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoTArray<EditorInlineStyle, 8> removeInlineStyleAndRelatedElements;
+ AppendInlineStyleAndRelatedStyle(EditorInlineStyle(aHTMLProperty, aAttribute),
+ removeInlineStyleAndRelatedElements);
+ rv = RemoveInlinePropertiesAsSubAction(removeInlineStyleAndRelatedElements);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveInlinePropertiesAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::RemoveInlineProperty(const nsAString& aProperty,
+ const nsAString& aAttribute) {
+ nsStaticAtom* property = NS_GetStaticAtom(aProperty);
+ nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute);
+
+ AutoEditActionDataSetter editActionData(
+ *this,
+ HTMLEditUtils::GetEditActionForFormatText(*property, attribute, false));
+ switch (editActionData.GetEditAction()) {
+ case EditAction::eRemoveFontFamilyProperty:
+ MOZ_ASSERT(!u""_ns.IsVoid());
+ editActionData.SetData(u""_ns);
+ break;
+ case EditAction::eRemoveColorProperty:
+ case EditAction::eRemoveBackgroundColorPropertyInline:
+ editActionData.SetColorData(u""_ns);
+ break;
+ default:
+ break;
+ }
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoTArray<EditorInlineStyle, 1> removeOneInlineStyle;
+ removeOneInlineStyle.AppendElement(EditorInlineStyle(*property, attribute));
+ rv = RemoveInlinePropertiesAsSubAction(removeOneInlineStyle);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::RemoveInlinePropertiesAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+void HTMLEditor::AppendInlineStyleAndRelatedStyle(
+ const EditorInlineStyle& aStyleToRemove,
+ nsTArray<EditorInlineStyle>& aStylesToRemove) const {
+ if (nsStaticAtom* similarElementName =
+ aStyleToRemove.GetSimilarElementNameAtom()) {
+ EditorInlineStyle anotherStyle(*similarElementName);
+ if (!aStylesToRemove.Contains(anotherStyle)) {
+ aStylesToRemove.AppendElement(std::move(anotherStyle));
+ }
+ } else if (aStyleToRemove.mHTMLProperty == nsGkAtoms::font) {
+ if (aStyleToRemove.mAttribute == nsGkAtoms::size) {
+ EditorInlineStyle big(*nsGkAtoms::big), small(*nsGkAtoms::small);
+ if (!aStylesToRemove.Contains(big)) {
+ aStylesToRemove.AppendElement(std::move(big));
+ }
+ if (!aStylesToRemove.Contains(small)) {
+ aStylesToRemove.AppendElement(std::move(small));
+ }
+ }
+ // Handling <tt> element code was implemented for composer (bug 115922).
+ // This shouldn't work with Document.execCommand() for compatibility with
+ // the other browsers. Currently, edit action principal is set only when
+ // the root caller is Document::ExecCommand() so that we should handle <tt>
+ // element only when the principal is nullptr that must be only when XUL
+ // command is executed on composer.
+ else if (aStyleToRemove.mAttribute == nsGkAtoms::face &&
+ !GetEditActionPrincipal()) {
+ EditorInlineStyle tt(*nsGkAtoms::tt);
+ if (!aStylesToRemove.Contains(tt)) {
+ aStylesToRemove.AppendElement(std::move(tt));
+ }
+ }
+ }
+ if (!aStylesToRemove.Contains(aStyleToRemove)) {
+ aStylesToRemove.AppendElement(aStyleToRemove);
+ }
+}
+
+nsresult HTMLEditor::RemoveInlinePropertiesAsSubAction(
+ const nsTArray<EditorInlineStyle>& aStylesToRemove) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aStylesToRemove.IsEmpty());
+
+ DebugOnly<nsresult> rvIgnored = CommitComposition();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CommitComposition() failed, but ignored");
+
+ if (SelectionRef().IsCollapsed()) {
+ // Manipulating text attributes on a collapsed selection only sets state
+ // for the next text insertion
+ mPendingStylesToApplyToNewContent->ClearStyles(aStylesToRemove);
+ return NS_OK;
+ }
+
+ // XXX Shouldn't we quit before calling `CommitComposition()`?
+ if (IsPlaintextMailComposer()) {
+ return NS_OK;
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eRemoveTextProperty, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ AutoRangeArray selectionRanges(SelectionRef());
+ for (const EditorInlineStyle& styleToRemove : aStylesToRemove) {
+ // The ranges may be updated by changing the DOM tree. In strictly
+ // speaking, we should save and restore the ranges at every range loop,
+ // but we've never done so and it may be expensive if there are a lot of
+ // ranges. Therefore, we should do it for every style handling for now.
+ // TODO: We should collect everything required for removing the style before
+ // touching the DOM tree. Then, we need to save and restore the
+ // ranges only once.
+ Maybe<AutoInlineStyleSetter> styleInverter;
+ if (styleToRemove.IsInvertibleWithCSS()) {
+ styleInverter.emplace(EditorInlineStyleAndValue::ToInvert(styleToRemove));
+ }
+ for (OwningNonNull<nsRange>& selectionRange : selectionRanges.Ranges()) {
+ AutoTrackDOMRange trackSelectionRange(RangeUpdaterRef(), &selectionRange);
+ // If we're removing <a name>, we don't want to split ancestors because
+ // the split fragment will keep working as named anchor. Therefore, we
+ // need to remove all <a name> elements which the selection range even
+ // partially contains.
+ const EditorDOMRange range(
+ styleToRemove.mHTMLProperty == nsGkAtoms::name
+ ? GetExtendedRangeWrappingNamedAnchor(
+ EditorRawDOMRange(selectionRange))
+ : GetExtendedRangeWrappingEntirelySelectedElements(
+ EditorRawDOMRange(selectionRange)));
+ if (NS_WARN_IF(!range.IsPositioned())) {
+ continue;
+ }
+
+ // Remove this style from ancestors of our range endpoints, splitting
+ // them as appropriate
+ Result<SplitRangeOffResult, nsresult> splitRangeOffResult =
+ SplitAncestorStyledInlineElementsAtRangeEdges(
+ range, styleToRemove, SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitRangeOffResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges() "
+ "failed");
+ return splitRangeOffResult.unwrapErr();
+ }
+ // There is AutoTransactionsConserveSelection, so we don't need to
+ // update selection here.
+ splitRangeOffResult.inspect().IgnoreCaretPointSuggestion();
+
+ // XXX Modifying `range` means that we may modify ranges in `Selection`.
+ // Is this intentional? Note that the range may be not in
+ // `Selection` too. It seems that at least one of them is not
+ // an unexpected case.
+ const EditorDOMRange& splitRange =
+ splitRangeOffResult.inspect().RangeRef();
+ if (NS_WARN_IF(!splitRange.IsPositioned())) {
+ continue;
+ }
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContentsToInvertStyle;
+ {
+ // Collect top level children in the range first.
+ // TODO: Perhaps, HTMLEditUtils::IsSplittableNode should be used here
+ // instead of EditorUtils::IsEditableContent.
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContentsAroundRange;
+ if (splitRange.InSameContainer() &&
+ splitRange.StartRef().IsInTextNode()) {
+ if (!EditorUtils::IsEditableContent(
+ *splitRange.StartRef().ContainerAs<Text>(),
+ EditorType::HTML)) {
+ continue;
+ }
+ arrayOfContentsAroundRange.AppendElement(
+ *splitRange.StartRef().ContainerAs<Text>());
+ } else if (splitRange.IsInTextNodes() &&
+ splitRange.InAdjacentSiblings()) {
+ // Adjacent siblings are in a same element, so the editable state of
+ // both text nodes are always same.
+ if (!EditorUtils::IsEditableContent(
+ *splitRange.StartRef().ContainerAs<Text>(),
+ EditorType::HTML)) {
+ continue;
+ }
+ arrayOfContentsAroundRange.AppendElement(
+ *splitRange.StartRef().ContainerAs<Text>());
+ arrayOfContentsAroundRange.AppendElement(
+ *splitRange.EndRef().ContainerAs<Text>());
+ } else {
+ // Append first node if it's a text node but selected not entirely.
+ if (splitRange.StartRef().IsInTextNode() &&
+ !splitRange.StartRef().IsStartOfContainer() &&
+ EditorUtils::IsEditableContent(
+ *splitRange.StartRef().ContainerAs<Text>(),
+ EditorType::HTML)) {
+ arrayOfContentsAroundRange.AppendElement(
+ *splitRange.StartRef().ContainerAs<Text>());
+ }
+ // Append all entirely selected nodes.
+ ContentSubtreeIterator subtreeIter;
+ if (NS_SUCCEEDED(
+ subtreeIter.Init(splitRange.StartRef().ToRawRangeBoundary(),
+ splitRange.EndRef().ToRawRangeBoundary()))) {
+ for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
+ nsCOMPtr<nsINode> node = subtreeIter.GetCurrentNode();
+ if (NS_WARN_IF(!node)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (node->IsContent() &&
+ EditorUtils::IsEditableContent(*node->AsContent(),
+ EditorType::HTML)) {
+ arrayOfContentsAroundRange.AppendElement(*node->AsContent());
+ }
+ }
+ }
+ // Append last node if it's a text node but selected not entirely.
+ if (!splitRange.InSameContainer() &&
+ splitRange.EndRef().IsInTextNode() &&
+ !splitRange.EndRef().IsEndOfContainer() &&
+ EditorUtils::IsEditableContent(
+ *splitRange.EndRef().ContainerAs<Text>(), EditorType::HTML)) {
+ arrayOfContentsAroundRange.AppendElement(
+ *splitRange.EndRef().ContainerAs<Text>());
+ }
+ }
+ if (styleToRemove.IsInvertibleWithCSS()) {
+ arrayOfContentsToInvertStyle.SetCapacity(
+ arrayOfContentsAroundRange.Length());
+ }
+
+ for (OwningNonNull<nsIContent>& content : arrayOfContentsAroundRange) {
+ // We should remove style from the element and its descendants.
+ if (content->IsElement()) {
+ Result<EditorDOMPoint, nsresult> removeStyleResult =
+ RemoveStyleInside(MOZ_KnownLive(*content->AsElement()),
+ styleToRemove, SpecifiedStyle::Preserve);
+ if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
+ return removeStyleResult.unwrapErr();
+ }
+ // There is AutoTransactionsConserveSelection, so we don't need to
+ // update selection here.
+
+ // If the element was removed from the DOM tree by
+ // RemoveStyleInside, we need to do nothing for it anymore.
+ if (!content->GetParentNode()) {
+ continue;
+ }
+ }
+
+ if (styleToRemove.IsInvertibleWithCSS()) {
+ arrayOfContentsToInvertStyle.AppendElement(content);
+ }
+ } // for-loop for arrayOfContentsAroundRange
+ }
+
+ auto FlushAndStopTrackingAndShrinkSelectionRange =
+ [&]() MOZ_CAN_RUN_SCRIPT {
+ trackSelectionRange.FlushAndStopTracking();
+ if (NS_WARN_IF(!selectionRange->IsPositioned())) {
+ return;
+ }
+ EditorRawDOMRange range(selectionRange);
+ nsINode* const commonAncestor =
+ range.GetClosestCommonInclusiveAncestor();
+ // Shrink range for compatibility between browsers.
+ nsIContent* const maybeNextContent =
+ range.StartRef().IsInContentNode() &&
+ range.StartRef().IsEndOfContainer()
+ ? AutoInlineStyleSetter::GetNextEditableInlineContent(
+ *range.StartRef().ContainerAs<nsIContent>(),
+ commonAncestor)
+ : nullptr;
+ nsIContent* const maybePreviousContent =
+ range.EndRef().IsInContentNode() &&
+ range.EndRef().IsStartOfContainer()
+ ? AutoInlineStyleSetter::GetPreviousEditableInlineContent(
+ *range.EndRef().ContainerAs<nsIContent>(),
+ commonAncestor)
+ : nullptr;
+ if (!maybeNextContent && !maybePreviousContent) {
+ return;
+ }
+ const auto startPoint =
+ maybeNextContent &&
+ maybeNextContent != selectionRange->GetStartContainer()
+ ? HTMLEditUtils::GetDeepestEditableStartPointOf<
+ EditorRawDOMPoint>(*maybeNextContent)
+ : range.StartRef();
+ const auto endPoint =
+ maybePreviousContent && maybePreviousContent !=
+ selectionRange->GetEndContainer()
+ ? HTMLEditUtils::GetDeepestEditableEndPointOf<
+ EditorRawDOMPoint>(*maybePreviousContent)
+ : range.EndRef();
+ DebugOnly<nsresult> rvIgnored = selectionRange->SetStartAndEnd(
+ startPoint.ToRawRangeBoundary(), endPoint.ToRawRangeBoundary());
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsRange::SetStartAndEnd() failed, but ignored");
+ };
+
+ if (arrayOfContentsToInvertStyle.IsEmpty()) {
+ FlushAndStopTrackingAndShrinkSelectionRange();
+ continue;
+ }
+ MOZ_ASSERT(styleToRemove.IsInvertibleWithCSS());
+
+ // If the style is specified in parent block and we can remove the
+ // style with inserting new <span> element, we should do it.
+ for (OwningNonNull<nsIContent>& content : arrayOfContentsToInvertStyle) {
+ if (Element* element = Element::FromNode(content)) {
+ // XXX Do we need to call this even when data node or something? If
+ // so, for what?
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ nsresult rv = styleInverter->InvertStyleIfApplied(
+ *this, MOZ_KnownLive(*element));
+ if (NS_FAILED(rv)) {
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "AutoInlineStyleSetter::InvertStyleIfApplied() failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "AutoInlineStyleSetter::InvertStyleIfApplied() failed, but "
+ "ignored");
+ }
+ continue;
+ }
+
+ // Unfortunately, all browsers don't join text nodes when removing a
+ // style. Therefore, there may be multiple text nodes as adjacent
+ // siblings. That's the reason why we need to handle text nodes in this
+ // loop.
+ if (Text* textNode = Text::FromNode(content)) {
+ const uint32_t startOffset =
+ content == splitRange.StartRef().GetContainer()
+ ? splitRange.StartRef().Offset()
+ : 0u;
+ const uint32_t endOffset =
+ content == splitRange.EndRef().GetContainer()
+ ? splitRange.EndRef().Offset()
+ : textNode->TextDataLength();
+ Result<SplitRangeOffFromNodeResult, nsresult>
+ wrapTextInStyledElementResult =
+ styleInverter->InvertStyleIfApplied(
+ *this, MOZ_KnownLive(*textNode), startOffset, endOffset);
+ if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
+ NS_WARNING("AutoInlineStyleSetter::InvertStyleIfApplied() failed");
+ return wrapTextInStyledElementResult.unwrapErr();
+ }
+ SplitRangeOffFromNodeResult unwrappedWrapTextInStyledElementResult =
+ wrapTextInStyledElementResult.unwrap();
+ // There is AutoTransactionsConserveSelection, so we don't need to
+ // update selection here.
+ unwrappedWrapTextInStyledElementResult.IgnoreCaretPointSuggestion();
+ // If we've split the content, let's swap content in
+ // arrayOfContentsToInvertStyle with the text node which is applied
+ // the style.
+ if (unwrappedWrapTextInStyledElementResult.DidSplit() &&
+ styleToRemove.IsInvertibleWithCSS()) {
+ MOZ_ASSERT(unwrappedWrapTextInStyledElementResult
+ .GetMiddleContentAs<Text>());
+ if (Text* styledTextNode = unwrappedWrapTextInStyledElementResult
+ .GetMiddleContentAs<Text>()) {
+ if (styledTextNode != content) {
+ arrayOfContentsToInvertStyle.ReplaceElementAt(
+ arrayOfContentsToInvertStyle.Length() - 1,
+ OwningNonNull<nsIContent>(*styledTextNode));
+ }
+ }
+ }
+ continue;
+ }
+
+ // If the node is not an element nor a text node, it's invisible.
+ // In this case, we don't need to make it wrapped in new element.
+ }
+
+ // Finally, we should remove the style from all leaf text nodes if
+ // they still have the style.
+ AutoTArray<OwningNonNull<Text>, 32> leafTextNodes;
+ for (const OwningNonNull<nsIContent>& content :
+ arrayOfContentsToInvertStyle) {
+ // XXX Should we ignore content which has already removed from the
+ // DOM tree by the previous for-loop?
+ if (content->IsElement()) {
+ CollectEditableLeafTextNodes(*content->AsElement(), leafTextNodes);
+ }
+ }
+ for (const OwningNonNull<Text>& textNode : leafTextNodes) {
+ Result<SplitRangeOffFromNodeResult, nsresult>
+ wrapTextInStyledElementResult = styleInverter->InvertStyleIfApplied(
+ *this, MOZ_KnownLive(*textNode), 0, textNode->TextLength());
+ if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
+ NS_WARNING(
+ "AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode() "
+ "failed");
+ return wrapTextInStyledElementResult.unwrapErr();
+ }
+ // There is AutoTransactionsConserveSelection, so we don't need to
+ // update selection here.
+ wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion();
+ } // for-loop of leafTextNodes
+
+ // styleInverter may have touched a part of the range. Therefore, we
+ // cannot adjust the range without comparing DOM node position and
+ // first/last touched positions, but it may be too expensive. I think
+ // that shrinking only the tracked range boundaries must be enough in most
+ // cases.
+ FlushAndStopTrackingAndShrinkSelectionRange();
+ } // for-loop of selectionRanges
+ } // for-loop of styles
+
+ MOZ_ASSERT(!selectionRanges.HasSavedRanges());
+ nsresult rv = selectionRanges.ApplyTo(SelectionRef());
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::AutoInlineStyleSetter::InvertStyleIfApplied(
+ HTMLEditor& aHTMLEditor, Element& aElement) {
+ MOZ_ASSERT(IsStyleToInvert());
+
+ Result<bool, nsresult> isRemovableParentStyleOrError =
+ aHTMLEditor.IsRemovableParentStyleWithNewSpanElement(aElement, *this);
+ if (MOZ_UNLIKELY(isRemovableParentStyleOrError.isErr())) {
+ NS_WARNING("HTMLEditor::IsRemovableParentStyleWithNewSpanElement() failed");
+ return isRemovableParentStyleOrError.unwrapErr();
+ }
+ if (!isRemovableParentStyleOrError.unwrap()) {
+ // E.g., text-decoration cannot be override visually in children.
+ // In such cases, we can do nothing.
+ return NS_OK;
+ }
+
+ // Wrap it into a new element, move it into direct child which has same style,
+ // or specify the style to its parent.
+ Result<CaretPoint, nsresult> pointToPutCaretOrError =
+ ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(aHTMLEditor, aElement);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "AutoInlineStyleSetter::"
+ "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ // The caller must update `Selection` later so that we don't need this.
+ pointToPutCaretOrError.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult>
+HTMLEditor::AutoInlineStyleSetter::InvertStyleIfApplied(HTMLEditor& aHTMLEditor,
+ Text& aTextNode,
+ uint32_t aStartOffset,
+ uint32_t aEndOffset) {
+ MOZ_ASSERT(IsStyleToInvert());
+
+ Result<bool, nsresult> isRemovableParentStyleOrError =
+ aHTMLEditor.IsRemovableParentStyleWithNewSpanElement(aTextNode, *this);
+ if (MOZ_UNLIKELY(isRemovableParentStyleOrError.isErr())) {
+ NS_WARNING("HTMLEditor::IsRemovableParentStyleWithNewSpanElement() failed");
+ return isRemovableParentStyleOrError.propagateErr();
+ }
+ if (!isRemovableParentStyleOrError.unwrap()) {
+ // E.g., text-decoration cannot be override visually in children.
+ // In such cases, we can do nothing.
+ return SplitRangeOffFromNodeResult(nullptr, &aTextNode, nullptr);
+ }
+
+ // We need to use new `<span>` element or existing element if it's available
+ // to overwrite parent style.
+ Result<SplitRangeOffFromNodeResult, nsresult> wrapTextInStyledElementResult =
+ SplitTextNodeAndApplyStyleToMiddleNode(aHTMLEditor, aTextNode,
+ aStartOffset, aEndOffset);
+ NS_WARNING_ASSERTION(
+ wrapTextInStyledElementResult.isOk(),
+ "AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode() failed");
+ return wrapTextInStyledElementResult;
+}
+
+Result<bool, nsresult> HTMLEditor::IsRemovableParentStyleWithNewSpanElement(
+ nsIContent& aContent, const EditorInlineStyle& aStyle) const {
+ // We don't support to remove all inline styles with this path.
+ if (aStyle.IsStyleToClearAllInlineStyles()) {
+ return false;
+ }
+
+ // First check whether the style is invertible since this is the fastest
+ // check.
+ if (!aStyle.IsInvertibleWithCSS()) {
+ return false;
+ }
+
+ // If aContent is not an element and it's not in an element, it means that
+ // aContent is disconnected non-element node. In this case, it's never
+ // applied any styles which are invertible.
+ const RefPtr<Element> element = aContent.GetAsElementOrParentElement();
+ if (MOZ_UNLIKELY(!element)) {
+ return false;
+ }
+
+ // If parent block has invertible style, we should remove the style with
+ // creating new `<span>` element even in HTML mode because Chrome does it.
+ if (!aStyle.IsCSSSettable(*element)) {
+ return false;
+ }
+ nsAutoString emptyString;
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle,
+ emptyString);
+ NS_WARNING_ASSERTION(isComputedCSSEquivalentToStyleOrError.isOk(),
+ "CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError;
+}
+
+void HTMLEditor::CollectEditableLeafTextNodes(
+ Element& aElement, nsTArray<OwningNonNull<Text>>& aLeafTextNodes) const {
+ for (nsIContent* child = aElement.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsElement()) {
+ CollectEditableLeafTextNodes(*child->AsElement(), aLeafTextNodes);
+ continue;
+ }
+ if (child->IsText()) {
+ aLeafTextNodes.AppendElement(*child->AsText());
+ }
+ }
+}
+
+nsresult HTMLEditor::IncreaseFontSizeAsAction(nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eIncrementFontSize,
+ aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = IncrementOrDecrementFontSizeAsSubAction(FontSize::incr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::IncrementOrDecrementFontSizeAsSubAction("
+ "FontSize::incr) failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::DecreaseFontSizeAsAction(nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eDecrementFontSize,
+ aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = IncrementOrDecrementFontSizeAsSubAction(FontSize::decr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::IncrementOrDecrementFontSizeAsSubAction("
+ "FontSize::decr) failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::IncrementOrDecrementFontSizeAsSubAction(
+ FontSize aIncrementOrDecrement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Committing composition and changing font size should be undone together.
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ DebugOnly<nsresult> rvIgnored = CommitComposition();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CommitComposition() failed, but ignored");
+
+ // If selection is collapsed, set typing state
+ if (SelectionRef().IsCollapsed()) {
+ nsStaticAtom& bigOrSmallTagName = aIncrementOrDecrement == FontSize::incr
+ ? *nsGkAtoms::big
+ : *nsGkAtoms::small;
+
+ // Let's see in what kind of element the selection is
+ if (!SelectionRef().RangeCount()) {
+ return NS_OK;
+ }
+ const auto firstRangeStartPoint =
+ EditorBase::GetFirstSelectionStartPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!firstRangeStartPoint.IsSet())) {
+ return NS_OK;
+ }
+ Element* element =
+ firstRangeStartPoint.GetContainerOrContainerParentElement();
+ if (NS_WARN_IF(!element)) {
+ return NS_OK;
+ }
+ if (!HTMLEditUtils::CanNodeContain(*element, bigOrSmallTagName)) {
+ return NS_OK;
+ }
+
+ // Manipulating text attributes on a collapsed selection only sets state
+ // for the next text insertion
+ mPendingStylesToApplyToNewContent->PreserveStyle(bigOrSmallTagName, nullptr,
+ u""_ns);
+ return NS_OK;
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSetTextProperty, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ AutoRangeArray selectionRanges(SelectionRef());
+ MOZ_ALWAYS_TRUE(selectionRanges.SaveAndTrackRanges(*this));
+ for (const OwningNonNull<nsRange>& domRange : selectionRanges.Ranges()) {
+ // TODO: We should stop extending the range outside ancestor blocks because
+ // we don't need to do it for setting inline styles. However, here is
+ // chrome only handling path. Therefore, we don't need to fix here
+ // soon.
+ const EditorDOMRange range(GetExtendedRangeWrappingEntirelySelectedElements(
+ EditorRawDOMRange(domRange)));
+ if (NS_WARN_IF(!range.IsPositioned())) {
+ continue;
+ }
+
+ if (range.InSameContainer() && range.StartRef().IsInTextNode()) {
+ Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult =
+ SetFontSizeOnTextNode(
+ MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()),
+ range.StartRef().Offset(), range.EndRef().Offset(),
+ aIncrementOrDecrement);
+ if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed");
+ return wrapInBigOrSmallElementResult.unwrapErr();
+ }
+ // There is an AutoTransactionsConserveSelection instance so that we don't
+ // need to update selection for this change.
+ wrapInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion();
+ continue;
+ }
+
+ // Not the easy case. Range not contained in single text node. There
+ // are up to three phases here. There are all the nodes reported by the
+ // subtree iterator to be processed. And there are potentially a
+ // starting textnode and an ending textnode which are only partially
+ // contained by the range.
+
+ // Let's handle the nodes reported by the iterator. These nodes are
+ // entirely contained in the selection range. We build up a list of them
+ // (since doing operations on the document during iteration would perturb
+ // the iterator).
+
+ // Iterate range and build up array
+ ContentSubtreeIterator subtreeIter;
+ if (NS_SUCCEEDED(subtreeIter.Init(range.StartRef().ToRawRangeBoundary(),
+ range.EndRef().ToRawRangeBoundary()))) {
+ nsTArray<OwningNonNull<nsIContent>> arrayOfContents;
+ for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
+ if (NS_WARN_IF(!subtreeIter.GetCurrentNode()->IsContent())) {
+ return NS_ERROR_FAILURE;
+ }
+ OwningNonNull<nsIContent> content =
+ *subtreeIter.GetCurrentNode()->AsContent();
+
+ if (EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ arrayOfContents.AppendElement(content);
+ }
+ }
+
+ // Now that we have the list, do the font size change on each node
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // MOZ_KnownLive because of bug 1622253
+ Result<EditorDOMPoint, nsresult> fontChangeOnNodeResult =
+ SetFontSizeWithBigOrSmallElement(MOZ_KnownLive(content),
+ aIncrementOrDecrement);
+ if (MOZ_UNLIKELY(fontChangeOnNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeWithBigOrSmallElement() failed");
+ return fontChangeOnNodeResult.unwrapErr();
+ }
+ // There is an AutoTransactionsConserveSelection, so we don't need to
+ // update selection here.
+ }
+ }
+ // Now check the start and end parents of the range to see if they need
+ // to be separately handled (they do if they are text nodes, due to how
+ // the subtree iterator works - it will not have reported them).
+ if (range.StartRef().IsInTextNode() &&
+ !range.StartRef().IsEndOfContainer() &&
+ EditorUtils::IsEditableContent(*range.StartRef().ContainerAs<Text>(),
+ EditorType::HTML)) {
+ Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult =
+ SetFontSizeOnTextNode(
+ MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()),
+ range.StartRef().Offset(),
+ range.StartRef().ContainerAs<Text>()->TextDataLength(),
+ aIncrementOrDecrement);
+ if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed");
+ return wrapInBigOrSmallElementResult.unwrapErr();
+ }
+ // There is an AutoTransactionsConserveSelection instance so that we
+ // don't need to update selection for this change.
+ wrapInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+ if (range.EndRef().IsInTextNode() && !range.EndRef().IsStartOfContainer() &&
+ EditorUtils::IsEditableContent(*range.EndRef().ContainerAs<Text>(),
+ EditorType::HTML)) {
+ Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult =
+ SetFontSizeOnTextNode(
+ MOZ_KnownLive(*range.EndRef().ContainerAs<Text>()), 0u,
+ range.EndRef().Offset(), aIncrementOrDecrement);
+ if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed");
+ return wrapInBigOrSmallElementResult.unwrapErr();
+ }
+ // There is an AutoTransactionsConserveSelection instance so that we
+ // don't need to update selection for this change.
+ wrapInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+ }
+
+ MOZ_ASSERT(selectionRanges.HasSavedRanges());
+ selectionRanges.RestoreFromSavedRanges();
+ nsresult rv = selectionRanges.ApplyTo(SelectionRef());
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed");
+ return rv;
+}
+
+Result<CreateElementResult, nsresult> HTMLEditor::SetFontSizeOnTextNode(
+ Text& aTextNode, uint32_t aStartOffset, uint32_t aEndOffset,
+ FontSize aIncrementOrDecrement) {
+ // Don't need to do anything if no characters actually selected
+ if (aStartOffset == aEndOffset) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (!aTextNode.GetParentNode() ||
+ !HTMLEditUtils::CanNodeContain(*aTextNode.GetParentNode(),
+ *nsGkAtoms::big)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ aEndOffset = std::min(aTextNode.Length(), aEndOffset);
+
+ // Make the range an independent node.
+ RefPtr<Text> textNodeForTheRange = &aTextNode;
+
+ EditorDOMPoint pointToPutCaret;
+ {
+ auto pointToPutCaretOrError =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMPoint, nsresult> {
+ EditorDOMPoint pointToPutCaret;
+ // Split at the end of the range.
+ EditorDOMPoint atEnd(textNodeForTheRange, aEndOffset);
+ if (!atEnd.IsEndOfContainer()) {
+ // We need to split off back of text node
+ Result<SplitNodeResult, nsresult> splitAtEndResult =
+ SplitNodeWithTransaction(atEnd);
+ if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitAtEndResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap();
+ if (MOZ_UNLIKELY(
+ !unwrappedSplitAtEndResult.HasCaretPointSuggestion())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeWithTransaction() didn't suggest caret "
+ "point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ unwrappedSplitAtEndResult.MoveCaretPointTo(pointToPutCaret, *this, {});
+ MOZ_ASSERT_IF(AllowsTransactionsToChangeSelection(),
+ pointToPutCaret.IsSet());
+ textNodeForTheRange =
+ unwrappedSplitAtEndResult.GetPreviousContentAs<Text>();
+ MOZ_DIAGNOSTIC_ASSERT(textNodeForTheRange);
+ }
+
+ // Split at the start of the range.
+ EditorDOMPoint atStart(textNodeForTheRange, aStartOffset);
+ if (!atStart.IsStartOfContainer()) {
+ // We need to split off front of text node
+ Result<SplitNodeResult, nsresult> splitAtStartResult =
+ SplitNodeWithTransaction(atStart);
+ if (MOZ_UNLIKELY(splitAtStartResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitAtStartResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitAtStartResult =
+ splitAtStartResult.unwrap();
+ if (MOZ_UNLIKELY(
+ !unwrappedSplitAtStartResult.HasCaretPointSuggestion())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeWithTransaction() didn't suggest caret "
+ "point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ unwrappedSplitAtStartResult.MoveCaretPointTo(pointToPutCaret, *this,
+ {});
+ MOZ_ASSERT_IF(AllowsTransactionsToChangeSelection(),
+ pointToPutCaret.IsSet());
+ textNodeForTheRange =
+ unwrappedSplitAtStartResult.GetNextContentAs<Text>();
+ MOZ_DIAGNOSTIC_ASSERT(textNodeForTheRange);
+ }
+
+ return pointToPutCaret;
+ }();
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ // Don't warn here since it should be done in the lambda.
+ return pointToPutCaretOrError.propagateErr();
+ }
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+
+ // Look for siblings that are correct type of node
+ nsStaticAtom* const bigOrSmallTagName =
+ aIncrementOrDecrement == FontSize::incr ? nsGkAtoms::big
+ : nsGkAtoms::small;
+ nsCOMPtr<nsIContent> sibling = HTMLEditUtils::GetPreviousSibling(
+ *textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode});
+ if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) {
+ // Previous sib is already right kind of inline node; slide this over
+ Result<MoveNodeResult, nsresult> moveTextNodeResult =
+ MoveNodeToEndWithTransaction(*textNodeForTheRange, *sibling);
+ if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveTextNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveTextNodeResult = moveTextNodeResult.unwrap();
+ unwrappedMoveTextNodeResult.MoveCaretPointTo(
+ pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion});
+ // XXX Should we return the new container?
+ return CreateElementResult::NotHandled(std::move(pointToPutCaret));
+ }
+ sibling = HTMLEditUtils::GetNextSibling(
+ *textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode});
+ if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) {
+ // Following sib is already right kind of inline node; slide this over
+ Result<MoveNodeResult, nsresult> moveTextNodeResult =
+ MoveNodeWithTransaction(*textNodeForTheRange,
+ EditorDOMPoint(sibling, 0u));
+ if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveTextNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveTextNodeResult = moveTextNodeResult.unwrap();
+ unwrappedMoveTextNodeResult.MoveCaretPointTo(
+ pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion});
+ // XXX Should we return the new container?
+ return CreateElementResult::NotHandled(std::move(pointToPutCaret));
+ }
+
+ // Else wrap the node inside font node with appropriate relative size
+ Result<CreateElementResult, nsresult> wrapTextInBigOrSmallElementResult =
+ InsertContainerWithTransaction(*textNodeForTheRange,
+ MOZ_KnownLive(*bigOrSmallTagName));
+ if (wrapTextInBigOrSmallElementResult.isErr()) {
+ NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
+ return wrapTextInBigOrSmallElementResult;
+ }
+ CreateElementResult unwrappedWrapTextInBigOrSmallElementResult =
+ wrapTextInBigOrSmallElementResult.unwrap();
+ unwrappedWrapTextInBigOrSmallElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return CreateElementResult(
+ unwrappedWrapTextInBigOrSmallElementResult.UnwrapNewNode(),
+ std::move(pointToPutCaret));
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::SetFontSizeOfFontElementChildren(
+ nsIContent& aContent, FontSize aIncrementOrDecrement) {
+ // This routine looks for all the font nodes in the tree rooted by aNode,
+ // including aNode itself, looking for font nodes that have the size attr
+ // set. Any such nodes need to have big or small put inside them, since
+ // they override any big/small that are above them.
+
+ // If this is a font node with size, put big/small inside it.
+ if (aContent.IsHTMLElement(nsGkAtoms::font) &&
+ aContent.AsElement()->HasAttr(nsGkAtoms::size)) {
+ EditorDOMPoint pointToPutCaret;
+
+ // Cycle through children and adjust relative font size.
+ AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents;
+ HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents);
+ for (const auto& child : arrayOfContents) {
+ // MOZ_KnownLive because of bug 1622253
+ Result<EditorDOMPoint, nsresult> setFontSizeOfChildResult =
+ SetFontSizeWithBigOrSmallElement(MOZ_KnownLive(child),
+ aIncrementOrDecrement);
+ if (MOZ_UNLIKELY(setFontSizeOfChildResult.isErr())) {
+ NS_WARNING("HTMLEditor::WrapContentInBigOrSmallElement() failed");
+ return setFontSizeOfChildResult;
+ }
+ if (setFontSizeOfChildResult.inspect().IsSet()) {
+ pointToPutCaret = setFontSizeOfChildResult.unwrap();
+ }
+ }
+
+ // WrapContentInBigOrSmallElement already calls us recursively,
+ // so we don't need to check our children again.
+ return pointToPutCaret;
+ }
+
+ // Otherwise cycle through the children.
+ EditorDOMPoint pointToPutCaret;
+ AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents;
+ HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents);
+ for (const auto& child : arrayOfContents) {
+ // MOZ_KnownLive because of bug 1622253
+ Result<EditorDOMPoint, nsresult> fontSizeChangeResult =
+ SetFontSizeOfFontElementChildren(MOZ_KnownLive(child),
+ aIncrementOrDecrement);
+ if (MOZ_UNLIKELY(fontSizeChangeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeOfFontElementChildren() failed");
+ return fontSizeChangeResult;
+ }
+ if (fontSizeChangeResult.inspect().IsSet()) {
+ pointToPutCaret = fontSizeChangeResult.unwrap();
+ }
+ }
+
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::SetFontSizeWithBigOrSmallElement(
+ nsIContent& aContent, FontSize aIncrementOrDecrement) {
+ nsStaticAtom* const bigOrSmallTagName =
+ aIncrementOrDecrement == FontSize::incr ? nsGkAtoms::big
+ : nsGkAtoms::small;
+
+ // Is aContent the opposite of what we want?
+ if ((aIncrementOrDecrement == FontSize::incr &&
+ aContent.IsHTMLElement(nsGkAtoms::small)) ||
+ (aIncrementOrDecrement == FontSize::decr &&
+ aContent.IsHTMLElement(nsGkAtoms::big))) {
+ // First, populate any nested font elements that have the size attr set
+ Result<EditorDOMPoint, nsresult> fontSizeChangeOfDescendantsResult =
+ SetFontSizeOfFontElementChildren(aContent, aIncrementOrDecrement);
+ if (MOZ_UNLIKELY(fontSizeChangeOfDescendantsResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeOfFontElementChildren() failed");
+ return fontSizeChangeOfDescendantsResult;
+ }
+ EditorDOMPoint pointToPutCaret = fontSizeChangeOfDescendantsResult.unwrap();
+ // In that case, just unwrap the <big> or <small> element.
+ Result<EditorDOMPoint, nsresult> unwrapBigOrSmallElementResult =
+ RemoveContainerWithTransaction(MOZ_KnownLive(*aContent.AsElement()));
+ if (MOZ_UNLIKELY(unwrapBigOrSmallElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapBigOrSmallElementResult;
+ }
+ if (unwrapBigOrSmallElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapBigOrSmallElementResult.unwrap();
+ }
+ return pointToPutCaret;
+ }
+
+ if (HTMLEditUtils::CanNodeContain(*bigOrSmallTagName, aContent)) {
+ // First, populate any nested font tags that have the size attr set
+ Result<EditorDOMPoint, nsresult> fontSizeChangeOfDescendantsResult =
+ SetFontSizeOfFontElementChildren(aContent, aIncrementOrDecrement);
+ if (MOZ_UNLIKELY(fontSizeChangeOfDescendantsResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeOfFontElementChildren() failed");
+ return fontSizeChangeOfDescendantsResult;
+ }
+
+ EditorDOMPoint pointToPutCaret = fontSizeChangeOfDescendantsResult.unwrap();
+
+ // Next, if next or previous is <big> or <small>, move aContent into it.
+ nsCOMPtr<nsIContent> sibling = HTMLEditUtils::GetPreviousSibling(
+ aContent, {WalkTreeOption::IgnoreNonEditableNode});
+ if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(aContent, *sibling);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ unwrappedMoveNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return pointToPutCaret;
+ }
+
+ sibling = HTMLEditUtils::GetNextSibling(
+ aContent, {WalkTreeOption::IgnoreNonEditableNode});
+ if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeWithTransaction(aContent, EditorDOMPoint(sibling, 0u));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ unwrappedMoveNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return pointToPutCaret;
+ }
+
+ // Otherwise, wrap aContent in new <big> or <small>
+ Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult =
+ InsertContainerWithTransaction(aContent,
+ MOZ_KnownLive(*bigOrSmallTagName));
+ if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
+ return Err(wrapInBigOrSmallElementResult.unwrapErr());
+ }
+ CreateElementResult unwrappedWrapInBigOrSmallElementResult =
+ wrapInBigOrSmallElementResult.unwrap();
+ MOZ_ASSERT(unwrappedWrapInBigOrSmallElementResult.GetNewNode());
+ unwrappedWrapInBigOrSmallElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return pointToPutCaret;
+ }
+
+ // none of the above? then cycle through the children.
+ // MOOSE: we should group the children together if possible
+ // into a single "big" or "small". For the moment they are
+ // each getting their own.
+ EditorDOMPoint pointToPutCaret;
+ AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents;
+ HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents);
+ for (const auto& child : arrayOfContents) {
+ // MOZ_KnownLive because of bug 1622253
+ Result<EditorDOMPoint, nsresult> setFontSizeOfChildResult =
+ SetFontSizeWithBigOrSmallElement(MOZ_KnownLive(child),
+ aIncrementOrDecrement);
+ if (MOZ_UNLIKELY(setFontSizeOfChildResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeWithBigOrSmallElement() failed");
+ return setFontSizeOfChildResult;
+ }
+ if (setFontSizeOfChildResult.inspect().IsSet()) {
+ pointToPutCaret = setFontSizeOfChildResult.unwrap();
+ }
+ }
+
+ return pointToPutCaret;
+}
+
+NS_IMETHODIMP HTMLEditor::GetFontFaceState(bool* aMixed, nsAString& outFace) {
+ if (NS_WARN_IF(!aMixed)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aMixed = true;
+ outFace.Truncate();
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ bool first, any, all;
+
+ nsresult rv = GetInlinePropertyBase(
+ EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::face), nullptr, &first,
+ &any, &all, &outFace);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::GetInlinePropertyBase(nsGkAtoms::font, nsGkAtoms::face) "
+ "failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (any && !all) {
+ return NS_OK; // mixed
+ }
+ if (all) {
+ *aMixed = false;
+ return NS_OK;
+ }
+
+ // if there is no font face, check for tt
+ rv = GetInlinePropertyBase(EditorInlineStyle(*nsGkAtoms::tt), nullptr, &first,
+ &any, &all, nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetInlinePropertyBase(nsGkAtoms::tt) failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (any && !all) {
+ return NS_OK; // mixed
+ }
+ if (all) {
+ *aMixed = false;
+ outFace.AssignLiteral("tt");
+ }
+
+ if (!any) {
+ // there was no font face attrs of any kind. We are in normal font.
+ outFace.Truncate();
+ *aMixed = false;
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::GetFontColorState(bool* aMixed, nsAString& aOutColor) {
+ if (NS_WARN_IF(!aMixed)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aMixed = true;
+ aOutColor.Truncate();
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ bool first, any, all;
+ nsresult rv = GetInlinePropertyBase(
+ EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::color), nullptr, &first,
+ &any, &all, &aOutColor);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::GetInlinePropertyBase(nsGkAtoms::font, nsGkAtoms::color) "
+ "failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (any && !all) {
+ return NS_OK; // mixed
+ }
+ if (all) {
+ *aMixed = false;
+ return NS_OK;
+ }
+
+ if (!any) {
+ // there was no font color attrs of any kind..
+ aOutColor.Truncate();
+ *aMixed = false;
+ }
+ return NS_OK;
+}
+
+// The return value is true only if the instance of the HTML editor we created
+// can handle CSS styles and if the CSS preference is checked.
+NS_IMETHODIMP HTMLEditor::GetIsCSSEnabled(bool* aIsCSSEnabled) {
+ *aIsCSSEnabled = IsCSSEnabled();
+ return NS_OK;
+}
+
+bool HTMLEditor::HasStyleOrIdOrClassAttribute(Element& aElement) {
+ return aElement.HasNonEmptyAttr(nsGkAtoms::style) ||
+ aElement.HasNonEmptyAttr(nsGkAtoms::_class) ||
+ aElement.HasNonEmptyAttr(nsGkAtoms::id);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/HTMLTableEditor.cpp b/editor/libeditor/HTMLTableEditor.cpp
new file mode 100644
index 0000000000..ea470b2203
--- /dev/null
+++ b/editor/libeditor/HTMLTableEditor.cpp
@@ -0,0 +1,4601 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdio.h>
+
+#include "HTMLEditor.h"
+#include "HTMLEditorInlines.h"
+
+#include "EditAction.h"
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "HTMLEditUtils.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/FlushType.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Element.h"
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsFrameSelection.h"
+#include "nsGkAtoms.h"
+#include "nsAtom.h"
+#include "nsIContent.h"
+#include "nsIFrame.h"
+#include "nsINode.h"
+#include "nsISupportsUtils.h"
+#include "nsITableCellLayout.h" // For efficient access to table cell
+#include "nsLiteralString.h"
+#include "nsQueryFrame.h"
+#include "nsRange.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsTableCellFrame.h"
+#include "nsTableWrapperFrame.h"
+#include "nscore.h"
+#include <algorithm>
+
+namespace mozilla {
+
+using namespace dom;
+using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+
+/**
+ * Stack based helper class for restoring selection after table edit.
+ */
+class MOZ_STACK_CLASS AutoSelectionSetterAfterTableEdit final {
+ private:
+ const RefPtr<HTMLEditor> mHTMLEditor;
+ const RefPtr<Element> mTable;
+ int32_t mCol, mRow, mDirection, mSelected;
+
+ public:
+ AutoSelectionSetterAfterTableEdit(HTMLEditor& aHTMLEditor, Element* aTable,
+ int32_t aRow, int32_t aCol,
+ int32_t aDirection, bool aSelected)
+ : mHTMLEditor(&aHTMLEditor),
+ mTable(aTable),
+ mCol(aCol),
+ mRow(aRow),
+ mDirection(aDirection),
+ mSelected(aSelected) {}
+
+ MOZ_CAN_RUN_SCRIPT ~AutoSelectionSetterAfterTableEdit() {
+ if (mHTMLEditor) {
+ mHTMLEditor->SetSelectionAfterTableEdit(mTable, mRow, mCol, mDirection,
+ mSelected);
+ }
+ }
+};
+
+/******************************************************************************
+ * HTMLEditor::CellIndexes
+ ******************************************************************************/
+
+void HTMLEditor::CellIndexes::Update(HTMLEditor& aHTMLEditor,
+ Selection& aSelection) {
+ // Guarantee the life time of the cell element since Init() will access
+ // layout methods.
+ RefPtr<Element> cellElement =
+ aHTMLEditor.GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td);
+ if (!cellElement) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) "
+ "failed");
+ return;
+ }
+
+ RefPtr<PresShell> presShell{aHTMLEditor.GetPresShell()};
+ Update(*cellElement, presShell);
+}
+
+void HTMLEditor::CellIndexes::Update(Element& aCellElement,
+ PresShell* aPresShell) {
+ // If the table cell is created immediately before this call, e.g., using
+ // innerHTML, frames have not been created yet. Hence, flush layout to create
+ // them.
+ if (NS_WARN_IF(!aPresShell)) {
+ return;
+ }
+
+ aPresShell->FlushPendingNotifications(FlushType::Frames);
+
+ nsIFrame* frameOfCell = aCellElement.GetPrimaryFrame();
+ if (!frameOfCell) {
+ NS_WARNING("There was no layout information of aCellElement");
+ return;
+ }
+
+ nsITableCellLayout* tableCellLayout = do_QueryFrame(frameOfCell);
+ if (!tableCellLayout) {
+ NS_WARNING("aCellElement was not a table cell");
+ return;
+ }
+
+ if (NS_FAILED(tableCellLayout->GetCellIndexes(mRow, mColumn))) {
+ NS_WARNING("nsITableCellLayout::GetCellIndexes() failed");
+ mRow = mColumn = -1;
+ return;
+ }
+
+ MOZ_ASSERT(!isErr());
+}
+
+/******************************************************************************
+ * HTMLEditor::CellData
+ ******************************************************************************/
+
+// static
+HTMLEditor::CellData HTMLEditor::CellData::AtIndexInTableElement(
+ const HTMLEditor& aHTMLEditor, const Element& aTableElement,
+ int32_t aRowIndex, int32_t aColumnIndex) {
+ nsTableWrapperFrame* tableFrame = HTMLEditor::GetTableFrame(&aTableElement);
+ if (!tableFrame) {
+ NS_WARNING("There was no layout information of the table");
+ return CellData::Error(aRowIndex, aColumnIndex);
+ }
+
+ // If there is no cell at the indexes. Don't set the error state to the new
+ // instance.
+ nsTableCellFrame* cellFrame =
+ tableFrame->GetCellFrameAt(aRowIndex, aColumnIndex);
+ if (!cellFrame) {
+ return CellData::NotFound(aRowIndex, aColumnIndex);
+ }
+
+ Element* cellElement = Element::FromNodeOrNull(cellFrame->GetContent());
+ if (!cellElement) {
+ return CellData::Error(aRowIndex, aColumnIndex);
+ }
+ return CellData(*cellElement, aRowIndex, aColumnIndex, *cellFrame,
+ *tableFrame);
+}
+
+HTMLEditor::CellData::CellData(Element& aElement, int32_t aRowIndex,
+ int32_t aColumnIndex,
+ nsTableCellFrame& aTableCellFrame,
+ nsTableWrapperFrame& aTableWrapperFrame)
+ : mElement(&aElement),
+ mCurrent(aRowIndex, aColumnIndex),
+ mFirst(aTableCellFrame.RowIndex(), aTableCellFrame.ColIndex()),
+ mRowSpan(aTableCellFrame.GetRowSpan()),
+ mColSpan(aTableCellFrame.GetColSpan()),
+ mEffectiveRowSpan(
+ aTableWrapperFrame.GetEffectiveRowSpanAt(aRowIndex, aColumnIndex)),
+ mEffectiveColSpan(
+ aTableWrapperFrame.GetEffectiveColSpanAt(aRowIndex, aColumnIndex)),
+ mIsSelected(aTableCellFrame.IsSelected()) {
+ MOZ_ASSERT(!mCurrent.isErr());
+}
+
+/******************************************************************************
+ * HTMLEditor::TableSize
+ ******************************************************************************/
+
+// static
+Result<HTMLEditor::TableSize, nsresult> HTMLEditor::TableSize::Create(
+ HTMLEditor& aHTMLEditor, Element& aTableOrElementInTable) {
+ // Currently, nsTableWrapperFrame::GetRowCount() and
+ // nsTableWrapperFrame::GetColCount() are safe to use without grabbing
+ // <table> element. However, editor developers may not watch layout API
+ // changes. So, for keeping us safer, we should use RefPtr here.
+ RefPtr<Element> tableElement =
+ aHTMLEditor.GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::table,
+ aTableOrElementInTable);
+ if (!tableElement) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) "
+ "failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsTableWrapperFrame* tableFrame =
+ do_QueryFrame(tableElement->GetPrimaryFrame());
+ if (!tableFrame) {
+ NS_WARNING("There was no layout information of the <table> element");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const int32_t rowCount = tableFrame->GetRowCount();
+ const int32_t columnCount = tableFrame->GetColCount();
+ if (NS_WARN_IF(rowCount < 0) || NS_WARN_IF(columnCount < 0)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return TableSize(rowCount, columnCount);
+}
+
+/******************************************************************************
+ * HTMLEditor
+ ******************************************************************************/
+
+nsresult HTMLEditor::InsertCell(Element* aCell, int32_t aRowSpan,
+ int32_t aColSpan, bool aAfter, bool aIsHeader,
+ Element** aNewCell) {
+ if (aNewCell) {
+ *aNewCell = nullptr;
+ }
+
+ if (NS_WARN_IF(!aCell)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // And the parent and offsets needed to do an insert
+ EditorDOMPoint pointToInsert(aCell);
+ if (NS_WARN_IF(!pointToInsert.IsSet())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<Element> newCell =
+ CreateElementWithDefaults(aIsHeader ? *nsGkAtoms::th : *nsGkAtoms::td);
+ if (!newCell) {
+ NS_WARNING(
+ "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::th or td) failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Optional: return new cell created
+ if (aNewCell) {
+ *aNewCell = do_AddRef(newCell).take();
+ }
+
+ if (aRowSpan > 1) {
+ // Note: Do NOT use editor transaction for this
+ nsAutoString newRowSpan;
+ newRowSpan.AppendInt(aRowSpan, 10);
+ DebugOnly<nsresult> rvIgnored = newCell->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::rowspan, newRowSpan, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::rawspan) failed, but ignored");
+ }
+ if (aColSpan > 1) {
+ // Note: Do NOT use editor transaction for this
+ nsAutoString newColSpan;
+ newColSpan.AppendInt(aColSpan, 10);
+ DebugOnly<nsresult> rvIgnored = newCell->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::colspan, newColSpan, true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::colspan) failed, but ignored");
+ }
+ if (aAfter) {
+ DebugOnly<bool> advanced = pointToInsert.AdvanceOffset();
+ NS_WARNING_ASSERTION(advanced,
+ "Failed to advance offset to after the old cell");
+ }
+
+ // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary
+ // in normal cases. However, it may be required for nested edit
+ // actions which may be caused by legacy mutation event listeners or
+ // chrome script.
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+ Result<CreateElementResult, nsresult> insertNewCellResult =
+ InsertNodeWithTransaction<Element>(*newCell, pointToInsert);
+ if (MOZ_UNLIKELY(insertNewCellResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertNewCellResult.unwrapErr();
+ }
+ // Because of dontChangeSelection, we've never allowed to transactions to
+ // update selection here.
+ insertNewCellResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+nsresult HTMLEditor::SetColSpan(Element* aCell, int32_t aColSpan) {
+ if (NS_WARN_IF(!aCell)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsAutoString newSpan;
+ newSpan.AppendInt(aColSpan, 10);
+ nsresult rv =
+ SetAttributeWithTransaction(*aCell, *nsGkAtoms::colspan, newSpan);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::colspan) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SetRowSpan(Element* aCell, int32_t aRowSpan) {
+ if (NS_WARN_IF(!aCell)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsAutoString newSpan;
+ newSpan.AppendInt(aRowSpan, 10);
+ nsresult rv =
+ SetAttributeWithTransaction(*aCell, *nsGkAtoms::rowspan, newSpan);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::rowspan) failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::InsertTableCell(int32_t aNumberOfCellsToInsert,
+ bool aInsertAfterSelectedCell) {
+ if (aNumberOfCellsToInsert <= 0) {
+ return NS_OK; // Just do nothing.
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertTableCellElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ Result<RefPtr<Element>, nsresult> cellElementOrError =
+ GetFirstSelectedCellElementInTable();
+ if (cellElementOrError.isErr()) {
+ NS_WARNING("HTMLEditor::GetFirstSelectedCellElementInTable() failed");
+ return EditorBase::ToGenericNSResult(cellElementOrError.unwrapErr());
+ }
+
+ if (!cellElementOrError.inspect()) {
+ return NS_OK;
+ }
+
+ EditorDOMPoint pointToInsert(cellElementOrError.inspect());
+ if (!pointToInsert.IsSet()) {
+ NS_WARNING("Found an orphan cell element");
+ return NS_ERROR_FAILURE;
+ }
+ if (aInsertAfterSelectedCell && !pointToInsert.IsEndOfContainer()) {
+ DebugOnly<bool> advanced = pointToInsert.AdvanceOffset();
+ NS_WARNING_ASSERTION(
+ advanced,
+ "Failed to set insertion point after current cell, but ignored");
+ }
+ Result<CreateElementResult, nsresult> insertCellElementResult =
+ InsertTableCellsWithTransaction(pointToInsert, aNumberOfCellsToInsert);
+ if (MOZ_UNLIKELY(insertCellElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(insertCellElementResult.unwrapErr());
+ }
+ // We don't need to modify selection here.
+ insertCellElementResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::InsertTableCellsWithTransaction(
+ const EditorDOMPoint& aPointToInsert, int32_t aNumberOfCellsToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+ MOZ_ASSERT(aNumberOfCellsToInsert > 0);
+
+ if (!HTMLEditUtils::IsTableRow(aPointToInsert.GetContainer())) {
+ NS_WARNING("Tried to insert cell elements to non-<tr> element");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ // Prevent auto insertion of BR in new cell until we're done
+ // XXX Why? I think that we should insert <br> element for every cell
+ // **before** inserting new cell into the <tr> element.
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertNode, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(error.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+ error.SuppressException();
+
+ // Put caret into the cell before the first inserting cell, or the first
+ // table cell in the row.
+ RefPtr<Element> cellToPutCaret =
+ aPointToInsert.IsEndOfContainer()
+ ? nullptr
+ : HTMLEditUtils::GetPreviousTableCellElementSibling(
+ *aPointToInsert.GetChild());
+
+ RefPtr<Element> firstCellElement, lastCellElement;
+ nsresult rv = [&]() MOZ_CAN_RUN_SCRIPT {
+ // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary
+ // in normal cases. However, it may be required for nested edit
+ // actions which may be caused by legacy mutation event listeners or
+ // chrome script.
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+
+ // Block legacy mutation events for making this job simpler.
+ nsAutoScriptBlockerSuppressNodeRemoved blockToRunScript;
+
+ // If there is a child to put a cell, we need to put all cell elements
+ // before it. Therefore, creating `EditorDOMPoint` with the child element
+ // is safe. Otherwise, we need to try to append cell elements in the row.
+ // Therefore, using `EditorDOMPoint::AtEndOf()` is safe. Note that it's
+ // not safe to creat it once because the offset and child relation in the
+ // point becomes invalid after inserting a cell element.
+ nsIContent* referenceContent = aPointToInsert.GetChild();
+ for ([[maybe_unused]] const auto i :
+ IntegerRange<uint32_t>(aNumberOfCellsToInsert)) {
+ RefPtr<Element> newCell = CreateElementWithDefaults(*nsGkAtoms::td);
+ if (!newCell) {
+ NS_WARNING(
+ "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed");
+ return NS_ERROR_FAILURE;
+ }
+ Result<CreateElementResult, nsresult> insertNewCellResult =
+ InsertNodeWithTransaction(
+ *newCell, referenceContent
+ ? EditorDOMPoint(referenceContent)
+ : EditorDOMPoint::AtEndOf(
+ *aPointToInsert.ContainerAs<Element>()));
+ if (MOZ_UNLIKELY(insertNewCellResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertNewCellResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInsertNewCellResult =
+ insertNewCellResult.unwrap();
+ lastCellElement = unwrappedInsertNewCellResult.UnwrapNewNode();
+ if (!firstCellElement) {
+ firstCellElement = lastCellElement;
+ }
+ // Because of dontChangeSelection, we've never allowed to transactions
+ // to update selection here.
+ unwrappedInsertNewCellResult.IgnoreCaretPointSuggestion();
+ if (!cellToPutCaret) {
+ cellToPutCaret = std::move(newCell); // This is first cell in the row.
+ }
+ }
+
+ // TODO: Stop touching selection here.
+ MOZ_ASSERT(cellToPutCaret);
+ MOZ_ASSERT(cellToPutCaret->GetParent());
+ CollapseSelectionToDeepestNonTableFirstChild(cellToPutCaret);
+ return NS_OK;
+ }();
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED ||
+ NS_WARN_IF(Destroyed()))) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+ MOZ_ASSERT(firstCellElement);
+ MOZ_ASSERT(lastCellElement);
+ return CreateElementResult(std::move(firstCellElement),
+ EditorDOMPoint(lastCellElement, 0u));
+}
+
+NS_IMETHODIMP HTMLEditor::GetFirstRow(Element* aTableOrElementInTable,
+ Element** aFirstRowElement) {
+ if (NS_WARN_IF(!aTableOrElementInTable) || NS_WARN_IF(!aFirstRowElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eGetFirstRow);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::GetFirstRow() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ Result<RefPtr<Element>, nsresult> firstRowElementOrError =
+ GetFirstTableRowElement(*aTableOrElementInTable);
+ NS_WARNING_ASSERTION(!firstRowElementOrError.isErr(),
+ "HTMLEditor::GetFirstTableRowElement() failed");
+ if (firstRowElementOrError.isErr()) {
+ NS_WARNING("HTMLEditor::GetFirstTableRowElement() failed");
+ return EditorBase::ToGenericNSResult(firstRowElementOrError.unwrapErr());
+ }
+ firstRowElementOrError.unwrap().forget(aFirstRowElement);
+ return NS_OK;
+}
+
+Result<RefPtr<Element>, nsresult> HTMLEditor::GetFirstTableRowElement(
+ const Element& aTableOrElementInTable) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Element* tableElement = GetInclusiveAncestorByTagNameInternal(
+ *nsGkAtoms::table, aTableOrElementInTable);
+ // If the element is not in <table>, return error.
+ if (!tableElement) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) "
+ "failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ for (nsIContent* tableChild = tableElement->GetFirstChild(); tableChild;
+ tableChild = tableChild->GetNextSibling()) {
+ if (tableChild->IsHTMLElement(nsGkAtoms::tr)) {
+ // Found a row directly under <table>
+ return RefPtr<Element>(tableChild->AsElement());
+ }
+ // <table> can have table section elements like <tbody>. <tr> elements
+ // may be children of them.
+ if (tableChild->IsAnyOfHTMLElements(nsGkAtoms::tbody, nsGkAtoms::thead,
+ nsGkAtoms::tfoot)) {
+ for (nsIContent* tableSectionChild = tableChild->GetFirstChild();
+ tableSectionChild;
+ tableSectionChild = tableSectionChild->GetNextSibling()) {
+ if (tableSectionChild->IsHTMLElement(nsGkAtoms::tr)) {
+ return RefPtr<Element>(tableSectionChild->AsElement());
+ }
+ }
+ }
+ }
+ // Don't return error when there is no <tr> element in the <table>.
+ return RefPtr<Element>();
+}
+
+Result<RefPtr<Element>, nsresult> HTMLEditor::GetNextTableRowElement(
+ const Element& aTableRowElement) const {
+ if (NS_WARN_IF(!aTableRowElement.IsHTMLElement(nsGkAtoms::tr))) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ for (nsIContent* maybeNextRow = aTableRowElement.GetNextSibling();
+ maybeNextRow; maybeNextRow = maybeNextRow->GetNextSibling()) {
+ if (maybeNextRow->IsHTMLElement(nsGkAtoms::tr)) {
+ return RefPtr<Element>(maybeNextRow->AsElement());
+ }
+ }
+
+ // In current table section (e.g., <tbody>), there is no <tr> element.
+ // Then, check the following table sections.
+ Element* parentElementOfRow = aTableRowElement.GetParentElement();
+ if (!parentElementOfRow) {
+ NS_WARNING("aTableRowElement was an orphan node");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Basically, <tr> elements should be in table section elements even if
+ // they are not written in the source explicitly. However, for preventing
+ // cross table boundary, check it now.
+ if (parentElementOfRow->IsHTMLElement(nsGkAtoms::table)) {
+ // Don't return error since this means just not found.
+ return RefPtr<Element>();
+ }
+
+ for (nsIContent* maybeNextTableSection = parentElementOfRow->GetNextSibling();
+ maybeNextTableSection;
+ maybeNextTableSection = maybeNextTableSection->GetNextSibling()) {
+ // If the sibling of parent of given <tr> is a table section element,
+ // check its children.
+ if (maybeNextTableSection->IsAnyOfHTMLElements(
+ nsGkAtoms::tbody, nsGkAtoms::thead, nsGkAtoms::tfoot)) {
+ for (nsIContent* maybeNextRow = maybeNextTableSection->GetFirstChild();
+ maybeNextRow; maybeNextRow = maybeNextRow->GetNextSibling()) {
+ if (maybeNextRow->IsHTMLElement(nsGkAtoms::tr)) {
+ return RefPtr<Element>(maybeNextRow->AsElement());
+ }
+ }
+ }
+ // I'm not sure whether this is a possible case since table section
+ // elements are created automatically. However, DOM API may create
+ // <tr> elements without table section elements. So, let's check it.
+ else if (maybeNextTableSection->IsHTMLElement(nsGkAtoms::tr)) {
+ return RefPtr<Element>(maybeNextTableSection->AsElement());
+ }
+ }
+ // Don't return error when the given <tr> element is the last <tr> element in
+ // the <table>.
+ return RefPtr<Element>();
+}
+
+NS_IMETHODIMP HTMLEditor::InsertTableColumn(int32_t aNumberOfColumnsToInsert,
+ bool aInsertAfterSelectedCell) {
+ if (aNumberOfColumnsToInsert <= 0) {
+ return NS_OK; // XXX Traditional behavior
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertTableColumn);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ Result<RefPtr<Element>, nsresult> cellElementOrError =
+ GetFirstSelectedCellElementInTable();
+ if (cellElementOrError.isErr()) {
+ NS_WARNING("HTMLEditor::GetFirstSelectedCellElementInTable() failed");
+ return EditorBase::ToGenericNSResult(cellElementOrError.unwrapErr());
+ }
+
+ if (!cellElementOrError.inspect()) {
+ return NS_OK;
+ }
+
+ EditorDOMPoint pointToInsert(cellElementOrError.inspect());
+ if (!pointToInsert.IsSet()) {
+ NS_WARNING("Found an orphan cell element");
+ return NS_ERROR_FAILURE;
+ }
+ if (aInsertAfterSelectedCell && !pointToInsert.IsEndOfContainer()) {
+ DebugOnly<bool> advanced = pointToInsert.AdvanceOffset();
+ NS_WARNING_ASSERTION(
+ advanced,
+ "Failed to set insertion point after current cell, but ignored");
+ }
+ rv = InsertTableColumnsWithTransaction(pointToInsert,
+ aNumberOfColumnsToInsert);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertTableColumnsWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::InsertTableColumnsWithTransaction(
+ const EditorDOMPoint& aPointToInsert, int32_t aNumberOfColumnsToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+ MOZ_ASSERT(aNumberOfColumnsToInsert > 0);
+
+ const RefPtr<PresShell> presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!HTMLEditUtils::IsTableRow(aPointToInsert.GetContainer())) {
+ NS_WARNING("Tried to insert columns to non-<tr> element");
+ return NS_ERROR_FAILURE;
+ }
+
+ const RefPtr<Element> tableElement =
+ HTMLEditUtils::GetClosestAncestorTableElement(
+ *aPointToInsert.ContainerAs<Element>());
+ if (!tableElement) {
+ NS_WARNING("There was no ancestor <table> element");
+ return NS_ERROR_FAILURE;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *tableElement);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.inspectErr();
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+ if (NS_WARN_IF(tableSize.IsEmpty())) {
+ return NS_ERROR_FAILURE; // We cannot handle it in an empty table
+ }
+
+ // If aPointToInsert points non-cell element or end of the row, it means that
+ // the caller wants to insert column immediately after the last cell of
+ // the pointing cell element or in the raw.
+ const bool insertAfterPreviousCell = [&]() {
+ if (!aPointToInsert.IsEndOfContainer() &&
+ HTMLEditUtils::IsTableCell(aPointToInsert.GetChild())) {
+ return false; // Insert before the cell element.
+ }
+ // There is a previous cell element, we should add a column after it.
+ Element* previousCellElement =
+ aPointToInsert.IsEndOfContainer()
+ ? HTMLEditUtils::GetLastTableCellElementChild(
+ *aPointToInsert.ContainerAs<Element>())
+ : HTMLEditUtils::GetPreviousTableCellElementSibling(
+ *aPointToInsert.GetChild());
+ return previousCellElement != nullptr;
+ }();
+
+ // Consider the column index in the table from given point and direction.
+ auto referenceColumnIndexOrError =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<int32_t, nsresult> {
+ if (!insertAfterPreviousCell) {
+ if (aPointToInsert.IsEndOfContainer()) {
+ return tableSize.mColumnCount; // Empty row, append columns to the end
+ }
+ // Insert columns immediately before current column.
+ const OwningNonNull<Element> tableCellElement =
+ *aPointToInsert.GetChild()->AsElement();
+ MOZ_ASSERT(HTMLEditUtils::IsTableCell(tableCellElement));
+ CellIndexes cellIndexes(*tableCellElement, presShell);
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return cellIndexes.mColumn;
+ }
+
+ // Otherwise, insert columns immediately after the previous column.
+ Element* previousCellElement =
+ aPointToInsert.IsEndOfContainer()
+ ? HTMLEditUtils::GetLastTableCellElementChild(
+ *aPointToInsert.ContainerAs<Element>())
+ : HTMLEditUtils::GetPreviousTableCellElementSibling(
+ *aPointToInsert.GetChild());
+ MOZ_ASSERT(previousCellElement);
+ CellIndexes cellIndexes(*previousCellElement, presShell);
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return cellIndexes.mColumn;
+ }();
+ if (MOZ_UNLIKELY(referenceColumnIndexOrError.isErr())) {
+ return referenceColumnIndexOrError.unwrapErr();
+ }
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ // Prevent auto insertion of <br> element in new cell until we're done.
+ // XXX Why? We should put <br> element to every cell element before inserting
+ // the cells into the tree.
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertNode, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return error.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+ error.SuppressException();
+
+ // Suppress Rules System selection munging.
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+
+ // If we are inserting after all existing columns, make sure table is
+ // "well formed" before appending new column.
+ // XXX As far as I've tested, NormalizeTableInternal() always fails to
+ // normalize non-rectangular table. So, the following CellData will
+ // fail if the table is not rectangle.
+ if (referenceColumnIndexOrError.inspect() >= tableSize.mColumnCount) {
+ DebugOnly<nsresult> rv = NormalizeTableInternal(*tableElement);
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING(
+ "HTMLEditor::NormalizeTableInternal() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::NormalizeTableInternal() failed, but ignored");
+ }
+
+ // First, we should collect all reference nodes to insert new table cells.
+ AutoTArray<CellData, 32> arrayOfCellData;
+ {
+ arrayOfCellData.SetCapacity(tableSize.mRowCount);
+ for (const int32_t rowIndex : IntegerRange(tableSize.mRowCount)) {
+ const auto cellData = CellData::AtIndexInTableElement(
+ *this, *tableElement, rowIndex,
+ referenceColumnIndexOrError.inspect());
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+ arrayOfCellData.AppendElement(cellData);
+ }
+ }
+
+ // Note that checking whether the editor destroyed or not should be done
+ // after inserting all cell elements. Otherwise, the table is left as
+ // not a rectangle.
+ auto cellElementToPutCaretOrError =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<RefPtr<Element>, nsresult> {
+ // Block legacy mutation events for making this job simpler.
+ nsAutoScriptBlockerSuppressNodeRemoved blockToRunScript;
+ RefPtr<Element> cellElementToPutCaret;
+ for (const CellData& cellData : arrayOfCellData) {
+ // Don't fail entire process if we fail to find a cell (may fail just in
+ // particular rows with < adequate cells per row).
+ // XXX So, here wants to know whether the CellData actually failed
+ // above. Fix this later.
+ if (!cellData.mElement) {
+ continue;
+ }
+
+ if ((!insertAfterPreviousCell && cellData.IsSpannedFromOtherColumn()) ||
+ (insertAfterPreviousCell &&
+ cellData.IsNextColumnSpannedFromOtherColumn())) {
+ // If we have a cell spanning this location, simply increase its
+ // colspan to keep table rectangular.
+ if (cellData.mColSpan > 0) {
+ DebugOnly<nsresult> rvIgnored = SetColSpan(
+ cellData.mElement, cellData.mColSpan + aNumberOfColumnsToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SetColSpan() failed, but ignored");
+ }
+ continue;
+ }
+
+ EditorDOMPoint pointToInsert = [&]() {
+ if (!insertAfterPreviousCell) {
+ // Insert before the reference cell.
+ return EditorDOMPoint(cellData.mElement);
+ }
+ if (!cellData.mElement->GetNextSibling()) {
+ // Insert after the reference cell, but nothing follows it, append
+ // to the end of the row.
+ return EditorDOMPoint::AtEndOf(*cellData.mElement->GetParentNode());
+ }
+ // Otherwise, returns immediately before the next sibling. Note that
+ // the next sibling may not be a table cell element. E.g., it may be
+ // a text node containing only white-spaces in most cases.
+ return EditorDOMPoint(cellData.mElement->GetNextSibling());
+ }();
+ if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ Result<CreateElementResult, nsresult> insertCellElementsResult =
+ InsertTableCellsWithTransaction(pointToInsert,
+ aNumberOfColumnsToInsert);
+ if (MOZ_UNLIKELY(insertCellElementsResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTableCellsWithTransaction() failed");
+ return insertCellElementsResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertCellElementsResult =
+ insertCellElementsResult.unwrap();
+ // We'll update selection later into the first inserted cell element in
+ // the current row.
+ unwrappedInsertCellElementsResult.IgnoreCaretPointSuggestion();
+ if (pointToInsert.ContainerAs<Element>() ==
+ aPointToInsert.ContainerAs<Element>()) {
+ cellElementToPutCaret =
+ unwrappedInsertCellElementsResult.UnwrapNewNode();
+ MOZ_ASSERT(cellElementToPutCaret);
+ MOZ_ASSERT(HTMLEditUtils::IsTableCell(cellElementToPutCaret));
+ }
+ }
+ return cellElementToPutCaret;
+ }();
+ if (MOZ_UNLIKELY(cellElementToPutCaretOrError.isErr())) {
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED
+ : cellElementToPutCaretOrError.unwrapErr();
+ }
+ const RefPtr<Element> cellElementToPutCaret =
+ cellElementToPutCaretOrError.unwrap();
+ NS_WARNING_ASSERTION(
+ cellElementToPutCaret,
+ "Didn't find the first inserted cell element in the specified row");
+ if (MOZ_LIKELY(cellElementToPutCaret)) {
+ CollapseSelectionToDeepestNonTableFirstChild(cellElementToPutCaret);
+ }
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::InsertTableRow(int32_t aNumberOfRowsToInsert,
+ bool aInsertAfterSelectedCell) {
+ if (aNumberOfRowsToInsert <= 0) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eInsertTableRowElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ Result<RefPtr<Element>, nsresult> cellElementOrError =
+ GetFirstSelectedCellElementInTable();
+ if (cellElementOrError.isErr()) {
+ NS_WARNING("HTMLEditor::GetFirstSelectedCellElementInTable() failed");
+ return EditorBase::ToGenericNSResult(cellElementOrError.unwrapErr());
+ }
+
+ if (!cellElementOrError.inspect()) {
+ return NS_OK;
+ }
+
+ rv = InsertTableRowsWithTransaction(
+ MOZ_KnownLive(*cellElementOrError.inspect()), aNumberOfRowsToInsert,
+ aInsertAfterSelectedCell ? InsertPosition::eAfterSelectedCell
+ : InsertPosition::eBeforeSelectedCell);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertTableRowsWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::InsertTableRowsWithTransaction(
+ Element& aCellElement, int32_t aNumberOfRowsToInsert,
+ InsertPosition aInsertPosition) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(HTMLEditUtils::IsTableCell(&aCellElement));
+
+ const RefPtr<PresShell> presShell = GetPresShell();
+ if (MOZ_UNLIKELY(NS_WARN_IF(!presShell))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (MOZ_UNLIKELY(
+ !HTMLEditUtils::IsTableRow(aCellElement.GetParentElement()))) {
+ NS_WARNING("Tried to insert columns to non-<tr> element");
+ return NS_ERROR_FAILURE;
+ }
+
+ const RefPtr<Element> tableElement =
+ HTMLEditUtils::GetClosestAncestorTableElement(aCellElement);
+ if (MOZ_UNLIKELY(!tableElement)) {
+ return NS_OK;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *tableElement);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.inspectErr();
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+ // Should not be empty since we've already found a cell.
+ MOZ_ASSERT(!tableSize.IsEmpty());
+
+ const CellIndexes cellIndexes(aCellElement, presShell);
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get more data for current cell in row we are inserting at because we need
+ // rowspan.
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *tableElement, cellIndexes);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(&aCellElement == cellData.mElement);
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ // Prevent auto insertion of BR in new cell until we're done
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertNode, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return error.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ struct ElementWithNewRowSpan final {
+ const OwningNonNull<Element> mCellElement;
+ const int32_t mNewRowSpan;
+
+ ElementWithNewRowSpan(Element& aCellElement, int32_t aNewRowSpan)
+ : mCellElement(aCellElement), mNewRowSpan(aNewRowSpan) {}
+ };
+ AutoTArray<ElementWithNewRowSpan, 16> cellElementsToModifyRowSpan;
+ if (aInsertPosition == InsertPosition::eAfterSelectedCell &&
+ !cellData.mRowSpan) {
+ // Detect when user is adding after a rowspan=0 case.
+ // Assume they want to stop the "0" behavior and really add a new row.
+ // Thus we set the rowspan to its true value.
+ cellElementsToModifyRowSpan.AppendElement(
+ ElementWithNewRowSpan(aCellElement, cellData.mEffectiveRowSpan));
+ }
+
+ struct MOZ_STACK_CLASS TableRowData {
+ RefPtr<Element> mElement;
+ int32_t mNumberOfCellsInStartRow;
+ int32_t mOffsetInTRElementToPutCaret;
+ };
+ const auto referenceRowDataOrError = [&]() -> Result<TableRowData, nsresult> {
+ const int32_t startRowIndex =
+ aInsertPosition == InsertPosition::eBeforeSelectedCell
+ ? cellData.mCurrent.mRow
+ : cellData.mCurrent.mRow + cellData.mEffectiveRowSpan;
+ if (startRowIndex < tableSize.mRowCount) {
+ // We are inserting above an existing row. Get each cell in the insert
+ // row to adjust for rowspan effects while we count how many cells are
+ // needed.
+ RefPtr<Element> referenceRowElement;
+ int32_t numberOfCellsInStartRow = 0;
+ int32_t offsetInTRElementToPutCaret = 0;
+ for (int32_t colIndex = 0;;) {
+ const auto cellDataInStartRow = CellData::AtIndexInTableElement(
+ *this, *tableElement, startRowIndex, colIndex);
+ if (cellDataInStartRow.FailedOrNotFound()) {
+ break; // Perhaps, we reach end of the row.
+ }
+
+ // XXX So, this is impossible case. Will be removed.
+ if (!cellDataInStartRow.mElement) {
+ NS_WARNING("CellData::Update() succeeded, but didn't set mElement");
+ break;
+ }
+
+ if (cellDataInStartRow.IsSpannedFromOtherRow()) {
+ // We have a cell spanning this location. Increase its rowspan.
+ // Note that if rowspan is 0, we do nothing since that cell should
+ // automatically extend into the new row.
+ if (cellDataInStartRow.mRowSpan > 0) {
+ cellElementsToModifyRowSpan.AppendElement(ElementWithNewRowSpan(
+ *cellDataInStartRow.mElement,
+ cellDataInStartRow.mRowSpan + aNumberOfRowsToInsert));
+ }
+ colIndex = cellDataInStartRow.NextColumnIndex();
+ continue;
+ }
+
+ if (colIndex < cellDataInStartRow.mCurrent.mColumn) {
+ offsetInTRElementToPutCaret++;
+ }
+
+ numberOfCellsInStartRow += cellDataInStartRow.mEffectiveColSpan;
+ if (!referenceRowElement) {
+ if (Element* maybeTableRowElement =
+ cellDataInStartRow.mElement->GetParentElement()) {
+ if (HTMLEditUtils::IsTableRow(maybeTableRowElement)) {
+ referenceRowElement = maybeTableRowElement;
+ }
+ }
+ }
+ MOZ_ASSERT(colIndex < cellDataInStartRow.NextColumnIndex());
+ colIndex = cellDataInStartRow.NextColumnIndex();
+ }
+ if (MOZ_UNLIKELY(!referenceRowElement)) {
+ NS_WARNING(
+ "Reference row element to insert new row elements was not found");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return TableRowData{std::move(referenceRowElement),
+ numberOfCellsInStartRow, offsetInTRElementToPutCaret};
+ }
+
+ // We are adding a new row after all others. If it weren't for colspan=0
+ // effect, we could simply use tableSize.mColumnCount for number of new
+ // cells...
+ // XXX colspan=0 support has now been removed in table layout so maybe this
+ // can be cleaned up now? (bug 1243183)
+ int32_t numberOfCellsInStartRow = tableSize.mColumnCount;
+ int32_t offsetInTRElementToPutCaret = 0;
+
+ // but we must compensate for all cells with rowspan = 0 in the last row.
+ const int32_t lastRowIndex = tableSize.mRowCount - 1;
+ for (int32_t colIndex = 0;;) {
+ const auto cellDataInLastRow = CellData::AtIndexInTableElement(
+ *this, *tableElement, lastRowIndex, colIndex);
+ if (cellDataInLastRow.FailedOrNotFound()) {
+ break; // Perhaps, we reach end of the row.
+ }
+
+ if (!cellDataInLastRow.mRowSpan) {
+ MOZ_ASSERT(numberOfCellsInStartRow >=
+ cellDataInLastRow.mEffectiveColSpan);
+ numberOfCellsInStartRow -= cellDataInLastRow.mEffectiveColSpan;
+ } else if (colIndex < cellDataInLastRow.mCurrent.mColumn) {
+ offsetInTRElementToPutCaret++;
+ }
+ MOZ_ASSERT(colIndex < cellDataInLastRow.NextColumnIndex());
+ colIndex = cellDataInLastRow.NextColumnIndex();
+ }
+ return TableRowData{nullptr, numberOfCellsInStartRow,
+ offsetInTRElementToPutCaret};
+ }();
+ if (MOZ_UNLIKELY(referenceRowDataOrError.isErr())) {
+ return referenceRowDataOrError.inspectErr();
+ }
+
+ const TableRowData& referenceRowData = referenceRowDataOrError.inspect();
+ if (MOZ_UNLIKELY(!referenceRowData.mNumberOfCellsInStartRow)) {
+ NS_WARNING("There was no cell element in the row");
+ return NS_OK;
+ }
+
+ MOZ_ASSERT_IF(referenceRowData.mElement,
+ HTMLEditUtils::IsTableRow(referenceRowData.mElement));
+ if (NS_WARN_IF(!HTMLEditUtils::IsTableRow(aCellElement.GetParentElement()))) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+
+ // The row parent and offset where we will insert new row.
+ EditorDOMPoint pointToInsert = [&]() {
+ if (aInsertPosition == InsertPosition::eBeforeSelectedCell) {
+ MOZ_ASSERT(referenceRowData.mElement);
+ return EditorDOMPoint(referenceRowData.mElement);
+ }
+ // Look for the last row element in the same table section or immediately
+ // before the reference row element. Then, we can insert new rows
+ // immediately after the given row element.
+ Element* lastRowElement = nullptr;
+ for (Element* rowElement = aCellElement.GetParentElement();
+ rowElement && rowElement != referenceRowData.mElement;) {
+ lastRowElement = rowElement;
+ const Result<RefPtr<Element>, nsresult> nextRowElementOrError =
+ GetNextTableRowElement(*rowElement);
+ if (MOZ_UNLIKELY(nextRowElementOrError.isErr())) {
+ NS_WARNING("HTMLEditor::GetNextTableRowElement() failed");
+ return EditorDOMPoint();
+ }
+ rowElement = nextRowElementOrError.inspect();
+ }
+ MOZ_ASSERT(lastRowElement);
+ return EditorDOMPoint::After(*lastRowElement);
+ }();
+ if (NS_WARN_IF(!pointToInsert.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ // Note that checking whether the editor destroyed or not should be done
+ // after inserting all cell elements. Otherwise, the table is left as
+ // not a rectangle.
+ auto firstInsertedTRElementOrError =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<RefPtr<Element>, nsresult> {
+ // Block legacy mutation events for making this job simpler.
+ nsAutoScriptBlockerSuppressNodeRemoved blockToRunScript;
+
+ // Suppress Rules System selection munging.
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+
+ for (const ElementWithNewRowSpan& cellElementAndNewRowSpan :
+ cellElementsToModifyRowSpan) {
+ DebugOnly<nsresult> rvIgnored =
+ SetRowSpan(MOZ_KnownLive(cellElementAndNewRowSpan.mCellElement),
+ cellElementAndNewRowSpan.mNewRowSpan);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SetRowSpan() failed, but ignored");
+ }
+
+ RefPtr<Element> firstInsertedTRElement;
+ IgnoredErrorResult error;
+ for ([[maybe_unused]] const int32_t rowIndex :
+ Reversed(IntegerRange(aNumberOfRowsToInsert))) {
+ // Create a new row
+ RefPtr<Element> newRowElement = CreateElementWithDefaults(*nsGkAtoms::tr);
+ if (!newRowElement) {
+ NS_WARNING(
+ "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::tr) failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ for ([[maybe_unused]] const int32_t i :
+ IntegerRange(referenceRowData.mNumberOfCellsInStartRow)) {
+ const RefPtr<Element> newCellElement =
+ CreateElementWithDefaults(*nsGkAtoms::td);
+ if (!newCellElement) {
+ NS_WARNING(
+ "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::td) failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ newRowElement->AppendChild(*newCellElement, error);
+ if (error.Failed()) {
+ NS_WARNING("nsINode::AppendChild() failed");
+ return Err(error.StealNSResult());
+ }
+ }
+
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsert);
+ Result<CreateElementResult, nsresult> insertNewRowResult =
+ InsertNodeWithTransaction<Element>(*newRowElement, pointToInsert);
+ if (MOZ_UNLIKELY(insertNewRowResult.isErr())) {
+ if (insertNewRowResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertNewRowResult.propagateErr();
+ }
+ NS_WARNING(
+ "EditorBase::InsertNodeWithTransaction() failed, but ignored");
+ }
+ firstInsertedTRElement = std::move(newRowElement);
+ // We'll update selection later.
+ insertNewRowResult.inspect().IgnoreCaretPointSuggestion();
+ }
+ return firstInsertedTRElement;
+ }();
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (MOZ_UNLIKELY(firstInsertedTRElementOrError.isErr())) {
+ return firstInsertedTRElementOrError.unwrapErr();
+ }
+
+ const OwningNonNull<Element> cellElementToPutCaret = [&]() {
+ if (MOZ_LIKELY(firstInsertedTRElementOrError.inspect())) {
+ EditorRawDOMPoint point(firstInsertedTRElementOrError.inspect(),
+ referenceRowData.mOffsetInTRElementToPutCaret);
+ if (MOZ_LIKELY(point.IsSetAndValid()) &&
+ MOZ_LIKELY(!point.IsEndOfContainer()) &&
+ MOZ_LIKELY(HTMLEditUtils::IsTableCell(point.GetChild()))) {
+ return OwningNonNull<Element>(*point.GetChild()->AsElement());
+ }
+ }
+ return OwningNonNull<Element>(aCellElement);
+ }();
+ CollapseSelectionToDeepestNonTableFirstChild(cellElementToPutCaret);
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+nsresult HTMLEditor::DeleteTableElementAndChildrenWithTransaction(
+ Element& aTableElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Block selectionchange event. It's enough to dispatch selectionchange
+ // event immediately after removing the table element.
+ {
+ AutoHideSelectionChanges hideSelection(SelectionRef());
+
+ // Select the <table> element after clear current selection.
+ if (SelectionRef().RangeCount()) {
+ ErrorResult error;
+ SelectionRef().RemoveAllRanges(error);
+ if (error.Failed()) {
+ NS_WARNING("Selection::RemoveAllRanges() failed");
+ return error.StealNSResult();
+ }
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(&aTableElement);
+ ErrorResult error;
+ range->SelectNode(aTableElement, error);
+ if (error.Failed()) {
+ NS_WARNING("nsRange::SelectNode() failed");
+ return error.StealNSResult();
+ }
+ SelectionRef().AddRangeAndSelectFramesAndNotifyListeners(*range, error);
+ if (error.Failed()) {
+ NS_WARNING(
+ "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed");
+ return error.StealNSResult();
+ }
+
+#ifdef DEBUG
+ range = SelectionRef().GetRangeAt(0);
+ MOZ_ASSERT(range);
+ MOZ_ASSERT(range->GetStartContainer() == aTableElement.GetParent());
+ MOZ_ASSERT(range->GetEndContainer() == aTableElement.GetParent());
+ MOZ_ASSERT(range->GetChildAtStartOffset() == &aTableElement);
+ MOZ_ASSERT(range->GetChildAtEndOffset() == aTableElement.GetNextSibling());
+#endif // #ifdef DEBUG
+ }
+
+ nsresult rv = DeleteSelectionAsSubAction(eNext, eStrip);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsSubAction(eNext, eStrip) failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::DeleteTable() {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eRemoveTableElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Element> table;
+ rv = GetCellContext(getter_AddRefs(table), nullptr, nullptr, nullptr, nullptr,
+ nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (!table) {
+ NS_WARNING("HTMLEditor::GetCellContext() didn't return <table> element");
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = DeleteTableElementAndChildrenWithTransaction(*table);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::DeleteTableCell(int32_t aNumberOfCellsToDelete) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eRemoveTableCellElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = DeleteTableCellWithTransaction(aNumberOfCellsToDelete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteTableCellWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::DeleteTableCellWithTransaction(
+ int32_t aNumberOfCellsToDelete) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Element> table;
+ RefPtr<Element> cell;
+ int32_t startRowIndex, startColIndex;
+
+ nsresult rv =
+ GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr,
+ nullptr, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return rv;
+ }
+ if (!table || !cell) {
+ NS_WARNING(
+ "HTMLEditor::GetCellContext() didn't return <table> and/or cell");
+ // Don't fail if we didn't find a table or cell.
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ return NS_ERROR_FAILURE; // XXX Should we just return NS_OK?
+ }
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ // Prevent rules testing until we're done
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ MOZ_ASSERT(SelectionRef().RangeCount());
+
+ SelectedTableCellScanner scanner(SelectionRef());
+
+ Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *table);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.unwrapErr();
+ }
+ // FYI: Cannot be a const reference because the row count will be updated
+ TableSize tableSize = tableSizeOrError.unwrap();
+ MOZ_ASSERT(!tableSize.IsEmpty());
+
+ // If only one cell is selected or no cell is selected, remove cells
+ // starting from the first selected cell or a cell containing first
+ // selection range.
+ if (!scanner.IsInTableCellSelectionMode() ||
+ SelectionRef().RangeCount() == 1) {
+ for (int32_t i = 0; i < aNumberOfCellsToDelete; i++) {
+ nsresult rv =
+ GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr,
+ nullptr, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return rv;
+ }
+ if (!table || !cell) {
+ NS_WARNING(
+ "HTMLEditor::GetCellContext() didn't return <table> and/or cell");
+ // Don't fail if no cell found
+ return NS_OK;
+ }
+
+ int32_t numberOfCellsInRow = GetNumberOfCellsInRow(*table, startRowIndex);
+ NS_WARNING_ASSERTION(
+ numberOfCellsInRow >= 0,
+ "HTMLEditor::GetNumberOfCellsInRow() failed, but ignored");
+
+ if (numberOfCellsInRow == 1) {
+ // Remove <tr> or <table> if we're removing all cells in the row or
+ // the table.
+ if (tableSize.mRowCount == 1) {
+ nsresult rv = DeleteTableElementAndChildrenWithTransaction(*table);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() "
+ "failed");
+ return rv;
+ }
+
+ // We need to call DeleteSelectedTableRowsWithTransaction() to handle
+ // cells with rowspan attribute.
+ rv = DeleteSelectedTableRowsWithTransaction(1);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::DeleteSelectedTableRowsWithTransaction(1) failed");
+ return rv;
+ }
+
+ // Adjust table rows simply. In strictly speaking, we should
+ // recompute table size with the latest layout information since
+ // mutation event listener may have changed the DOM tree. However,
+ // this is not in usual path of Firefox. So, we can assume that
+ // there are no mutation event listeners.
+ MOZ_ASSERT(tableSize.mRowCount);
+ tableSize.mRowCount--;
+ continue;
+ }
+
+ // The setCaret object will call AutoSelectionSetterAfterTableEdit in its
+ // destructor
+ AutoSelectionSetterAfterTableEdit setCaret(
+ *this, table, startRowIndex, startColIndex, ePreviousColumn, false);
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+
+ // XXX Removing cell element causes not adjusting colspan.
+ rv = DeleteNodeWithTransaction(*cell);
+ // If we fail, don't try to delete any more cells???
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ // Note that we don't refer column number in this loop. So, it must
+ // be safe not to recompute table size since number of row is synced
+ // above.
+ }
+ return NS_OK;
+ }
+
+ // When 2 or more cells are selected, ignore aNumberOfCellsToRemove and
+ // remove all selected cells.
+ const RefPtr<PresShell> presShell{GetPresShell()};
+ // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because scanner grabs
+ // it until it's destroyed later.
+ const CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]),
+ presShell);
+ if (NS_WARN_IF(firstCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ startRowIndex = firstCellIndexes.mRow;
+ startColIndex = firstCellIndexes.mColumn;
+
+ // The setCaret object will call AutoSelectionSetterAfterTableEdit in its
+ // destructor
+ AutoSelectionSetterAfterTableEdit setCaret(
+ *this, table, startRowIndex, startColIndex, ePreviousColumn, false);
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+
+ bool checkToDeleteRow = true;
+ bool checkToDeleteColumn = true;
+ for (RefPtr<Element> selectedCellElement = scanner.GetFirstElement();
+ selectedCellElement;) {
+ if (checkToDeleteRow) {
+ // Optimize to delete an entire row
+ // Clear so we don't repeat AllCellsInRowSelected within the same row
+ checkToDeleteRow = false;
+ if (AllCellsInRowSelected(table, startRowIndex, tableSize.mColumnCount)) {
+ // First, find the next cell in a different row to continue after we
+ // delete this row.
+ int32_t nextRow = startRowIndex;
+ while (nextRow == startRowIndex) {
+ selectedCellElement = scanner.GetNextElement();
+ if (!selectedCellElement) {
+ break;
+ }
+ const CellIndexes nextSelectedCellIndexes(*selectedCellElement,
+ presShell);
+ if (NS_WARN_IF(nextSelectedCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ nextRow = nextSelectedCellIndexes.mRow;
+ startColIndex = nextSelectedCellIndexes.mColumn;
+ }
+ if (tableSize.mRowCount == 1) {
+ nsresult rv = DeleteTableElementAndChildrenWithTransaction(*table);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() "
+ "failed");
+ return rv;
+ }
+ nsresult rv = DeleteTableRowWithTransaction(*table, startRowIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteTableRowWithTransaction() failed");
+ return rv;
+ }
+ // Adjust table rows simply. In strictly speaking, we should
+ // recompute table size with the latest layout information since
+ // mutation event listener may have changed the DOM tree. However,
+ // this is not in usual path of Firefox. So, we can assume that
+ // there are no mutation event listeners.
+ MOZ_ASSERT(tableSize.mRowCount);
+ tableSize.mRowCount--;
+ if (!selectedCellElement) {
+ break; // XXX Seems like a dead path
+ }
+ // For the next cell: Subtract 1 for row we deleted
+ startRowIndex = nextRow - 1;
+ // Set true since we know we will look at a new row next
+ checkToDeleteRow = true;
+ continue;
+ }
+ }
+
+ if (checkToDeleteColumn) {
+ // Optimize to delete an entire column
+ // Clear this so we don't repeat AllCellsInColSelected within the same Col
+ checkToDeleteColumn = false;
+ if (AllCellsInColumnSelected(table, startColIndex,
+ tableSize.mColumnCount)) {
+ // First, find the next cell in a different column to continue after
+ // we delete this column.
+ int32_t nextCol = startColIndex;
+ while (nextCol == startColIndex) {
+ selectedCellElement = scanner.GetNextElement();
+ if (!selectedCellElement) {
+ break;
+ }
+ const CellIndexes nextSelectedCellIndexes(*selectedCellElement,
+ presShell);
+ if (NS_WARN_IF(nextSelectedCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ startRowIndex = nextSelectedCellIndexes.mRow;
+ nextCol = nextSelectedCellIndexes.mColumn;
+ }
+ // Delete all cells which belong to the column.
+ nsresult rv = DeleteTableColumnWithTransaction(*table, startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteTableColumnWithTransaction() failed");
+ return rv;
+ }
+ // Adjust table columns simply. In strictly speaking, we should
+ // recompute table size with the latest layout information since
+ // mutation event listener may have changed the DOM tree. However,
+ // this is not in usual path of Firefox. So, we can assume that
+ // there are no mutation event listeners.
+ MOZ_ASSERT(tableSize.mColumnCount);
+ tableSize.mColumnCount--;
+ if (!selectedCellElement) {
+ break;
+ }
+ // For the next cell, subtract 1 for col. deleted
+ startColIndex = nextCol - 1;
+ // Set true since we know we will look at a new column next
+ checkToDeleteColumn = true;
+ continue;
+ }
+ }
+
+ nsresult rv = DeleteNodeWithTransaction(*selectedCellElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+
+ selectedCellElement = scanner.GetNextElement();
+ if (!selectedCellElement) {
+ return NS_OK;
+ }
+
+ const CellIndexes nextCellIndexes(*selectedCellElement, presShell);
+ if (NS_WARN_IF(nextCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ startRowIndex = nextCellIndexes.mRow;
+ startColIndex = nextCellIndexes.mColumn;
+ // When table cell is removed, table size of column may be changed.
+ // For example, if there are 2 rows, one has 2 cells, the other has
+ // 3 cells, tableSize.mColumnCount is 3. When this removes a cell
+ // in the latter row, mColumnCount should be come 2. However, we
+ // don't use mColumnCount in this loop, so, this must be okay for now.
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::DeleteTableCellContents() {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eDeleteTableCellContents);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = DeleteTableCellContentsWithTransaction();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteTableCellContentsWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::DeleteTableCellContentsWithTransaction() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Element> table;
+ RefPtr<Element> cell;
+ int32_t startRowIndex, startColIndex;
+ nsresult rv =
+ GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr,
+ nullptr, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return rv;
+ }
+ if (!cell) {
+ NS_WARNING("HTMLEditor::GetCellContext() didn't return cell element");
+ // Don't fail if no cell found.
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ return NS_ERROR_FAILURE; // XXX Should we just return NS_OK?
+ }
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ // Prevent rules testing until we're done
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // Don't let Rules System change the selection
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+
+ SelectedTableCellScanner scanner(SelectionRef());
+ if (scanner.IsInTableCellSelectionMode()) {
+ const RefPtr<PresShell> presShell{GetPresShell()};
+ // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because scanner
+ // grabs it until it's destroyed later.
+ const CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]),
+ presShell);
+ if (NS_WARN_IF(firstCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ cell = scanner.ElementsRef()[0];
+ startRowIndex = firstCellIndexes.mRow;
+ startColIndex = firstCellIndexes.mColumn;
+ }
+
+ AutoSelectionSetterAfterTableEdit setCaret(
+ *this, table, startRowIndex, startColIndex, ePreviousColumn, false);
+
+ for (RefPtr<Element> selectedCellElement = std::move(cell);
+ selectedCellElement; selectedCellElement = scanner.GetNextElement()) {
+ DebugOnly<nsresult> rvIgnored =
+ DeleteAllChildrenWithTransaction(*selectedCellElement);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteAllChildrenWithTransaction() failed, but ignored");
+ if (!scanner.IsInTableCellSelectionMode()) {
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::DeleteTableColumn(int32_t aNumberOfColumnsToDelete) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eRemoveTableColumn);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = DeleteSelectedTableColumnsWithTransaction(aNumberOfColumnsToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteSelectedTableColumnsWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::DeleteSelectedTableColumnsWithTransaction(
+ int32_t aNumberOfColumnsToDelete) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Element> table;
+ RefPtr<Element> cell;
+ int32_t startRowIndex, startColIndex;
+ nsresult rv =
+ GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr,
+ nullptr, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return rv;
+ }
+ if (!table || !cell) {
+ NS_WARNING(
+ "HTMLEditor::GetCellContext() didn't return <table> and/or cell");
+ // Don't fail if no cell found.
+ return NS_OK;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *table);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr());
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ // Prevent rules testing until we're done
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteNode, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return error.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // Shortcut the case of deleting all columns in table
+ if (!startColIndex && aNumberOfColumnsToDelete >= tableSize.mColumnCount) {
+ nsresult rv = DeleteTableElementAndChildrenWithTransaction(*table);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() failed");
+ return rv;
+ }
+
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ return NS_ERROR_FAILURE; // XXX Should we just return NS_OK?
+ }
+
+ SelectedTableCellScanner scanner(SelectionRef());
+ if (scanner.IsInTableCellSelectionMode() && SelectionRef().RangeCount() > 1) {
+ const RefPtr<PresShell> presShell{GetPresShell()};
+ // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because `scanner`
+ // grabs it until it's destroyed later.
+ const CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]),
+ presShell);
+ if (NS_WARN_IF(firstCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ startRowIndex = firstCellIndexes.mRow;
+ startColIndex = firstCellIndexes.mColumn;
+ }
+
+ // We control selection resetting after the insert...
+ AutoSelectionSetterAfterTableEdit setCaret(
+ *this, table, startRowIndex, startColIndex, ePreviousRow, false);
+
+ // If 2 or more cells are not selected, removing columns starting from
+ // a column which contains first selection range.
+ if (!scanner.IsInTableCellSelectionMode() ||
+ SelectionRef().RangeCount() == 1) {
+ int32_t columnCountToRemove = std::min(
+ aNumberOfColumnsToDelete, tableSize.mColumnCount - startColIndex);
+ for (int32_t i = 0; i < columnCountToRemove; i++) {
+ nsresult rv = DeleteTableColumnWithTransaction(*table, startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteTableColumnWithTransaction() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+ }
+
+ // If 2 or more cells are selected, remove all columns which contain selected
+ // cells. I.e., we ignore aNumberOfColumnsToDelete in this case.
+ const RefPtr<PresShell> presShell{GetPresShell()};
+ for (RefPtr<Element> selectedCellElement = scanner.GetFirstElement();
+ selectedCellElement;) {
+ if (selectedCellElement != scanner.ElementsRef()[0]) {
+ const CellIndexes cellIndexes(*selectedCellElement, presShell);
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ startRowIndex = cellIndexes.mRow;
+ startColIndex = cellIndexes.mColumn;
+ }
+ // Find the next cell in a different column
+ // to continue after we delete this column
+ int32_t nextCol = startColIndex;
+ while (nextCol == startColIndex) {
+ selectedCellElement = scanner.GetNextElement();
+ if (!selectedCellElement) {
+ break;
+ }
+ const CellIndexes cellIndexes(*selectedCellElement, presShell);
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ startRowIndex = cellIndexes.mRow;
+ nextCol = cellIndexes.mColumn;
+ }
+ nsresult rv = DeleteTableColumnWithTransaction(*table, startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteTableColumnWithTransaction() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::DeleteTableColumnWithTransaction(Element& aTableElement,
+ int32_t aColumnIndex) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ for (int32_t rowIndex = 0;; rowIndex++) {
+ const auto cellData = CellData::AtIndexInTableElement(
+ *this, aTableElement, rowIndex, aColumnIndex);
+ // Failure means that there is no more row in the table. In this case,
+ // we shouldn't return error since we just reach the end of the table.
+ // XXX Should distinguish whether CellData returns error or just not found
+ // later.
+ if (cellData.FailedOrNotFound()) {
+ return NS_OK;
+ }
+
+ // Find cells that don't start in column we are deleting.
+ MOZ_ASSERT(cellData.mColSpan >= 0);
+ if (cellData.IsSpannedFromOtherColumn() || cellData.mColSpan != 1) {
+ // If we have a cell spanning this location, decrease its colspan to
+ // keep table rectangular, but if colspan is 0, it'll be adjusted
+ // automatically.
+ if (cellData.mColSpan > 0) {
+ NS_WARNING_ASSERTION(cellData.mColSpan > 1,
+ "colspan should be 2 or larger");
+ DebugOnly<nsresult> rvIgnored =
+ SetColSpan(cellData.mElement, cellData.mColSpan - 1);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SetColSpan() failed, but ignored");
+ }
+ if (!cellData.IsSpannedFromOtherColumn()) {
+ // Cell is in column to be deleted, but must have colspan > 1,
+ // so delete contents of cell instead of cell itself (We must have
+ // reset colspan above).
+ DebugOnly<nsresult> rvIgnored =
+ DeleteAllChildrenWithTransaction(*cellData.mElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::DeleteAllChildrenWithTransaction() "
+ "failed, but ignored");
+ }
+ // Skip rows which the removed cell spanned.
+ rowIndex += cellData.NumberOfFollowingRows();
+ continue;
+ }
+
+ // Delete the cell
+ int32_t numberOfCellsInRow =
+ GetNumberOfCellsInRow(aTableElement, cellData.mCurrent.mRow);
+ NS_WARNING_ASSERTION(
+ numberOfCellsInRow > 0,
+ "HTMLEditor::GetNumberOfCellsInRow() failed, but ignored");
+ if (numberOfCellsInRow != 1) {
+ // If removing cell is not the last cell of the row, we can just remove
+ // it.
+ nsresult rv = DeleteNodeWithTransaction(*cellData.mElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ // Skip rows which the removed cell spanned.
+ rowIndex += cellData.NumberOfFollowingRows();
+ continue;
+ }
+
+ // When the cell is the last cell in the row, remove the row instead.
+ Element* parentRow = GetInclusiveAncestorByTagNameInternal(
+ *nsGkAtoms::tr, *cellData.mElement);
+ if (!parentRow) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::tr) "
+ "failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check if its the only row left in the table. If so, we can delete
+ // the table instead.
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, aTableElement);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.inspectErr();
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ if (tableSize.mRowCount == 1) {
+ // We're deleting the last row. So, let's remove the <table> now.
+ nsresult rv = DeleteTableElementAndChildrenWithTransaction(aTableElement);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() failed");
+ return rv;
+ }
+
+ // Delete the row by placing caret in cell we were to delete. We need
+ // to call DeleteTableRowWithTransaction() to handle cells with rowspan.
+ nsresult rv =
+ DeleteTableRowWithTransaction(aTableElement, cellData.mFirst.mRow);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteTableRowWithTransaction() failed");
+ return rv;
+ }
+
+ // Note that we decrement rowIndex since a row was deleted.
+ rowIndex--;
+ }
+
+ // Not reached because for (;;) loop never breaks.
+}
+
+NS_IMETHODIMP HTMLEditor::DeleteTableRow(int32_t aNumberOfRowsToDelete) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eRemoveTableRowElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = DeleteSelectedTableRowsWithTransaction(aNumberOfRowsToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteSelectedTableRowsWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::DeleteSelectedTableRowsWithTransaction(
+ int32_t aNumberOfRowsToDelete) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Element> table;
+ RefPtr<Element> cell;
+ int32_t startRowIndex, startColIndex;
+ nsresult rv =
+ GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr,
+ nullptr, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return rv;
+ }
+ if (!table || !cell) {
+ NS_WARNING(
+ "HTMLEditor::GetCellContext() didn't return <table> and/or cell");
+ // Don't fail if no cell found.
+ return NS_OK;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *table);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.inspectErr();
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ // Prevent rules testing until we're done
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteNode, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return error.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // Shortcut the case of deleting all rows in table
+ if (!startRowIndex && aNumberOfRowsToDelete >= tableSize.mRowCount) {
+ nsresult rv = DeleteTableElementAndChildrenWithTransaction(*table);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::DeleteTableElementAndChildrenWithTransaction() failed");
+ return rv;
+ }
+
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ return NS_ERROR_FAILURE; // XXX Should we just return NS_OK?
+ }
+
+ SelectedTableCellScanner scanner(SelectionRef());
+ if (scanner.IsInTableCellSelectionMode() && SelectionRef().RangeCount() > 1) {
+ // Fetch indexes again - may be different for selected cells
+ const RefPtr<PresShell> presShell{GetPresShell()};
+ // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because `scanner`
+ // grabs it until it's destroyed later.
+ const CellIndexes firstCellIndexes(MOZ_KnownLive(scanner.ElementsRef()[0]),
+ presShell);
+ if (NS_WARN_IF(firstCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ startRowIndex = firstCellIndexes.mRow;
+ startColIndex = firstCellIndexes.mColumn;
+ }
+
+ // We control selection resetting after the insert...
+ AutoSelectionSetterAfterTableEdit setCaret(
+ *this, table, startRowIndex, startColIndex, ePreviousRow, false);
+ // Don't change selection during deletions
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+
+ // XXX Perhaps, the following loops should collect <tr> elements to remove
+ // first, then, remove them from the DOM tree since mutation event
+ // listener may change the DOM tree during the loops.
+
+ // If 2 or more cells are not selected, removing rows starting from
+ // a row which contains first selection range.
+ if (!scanner.IsInTableCellSelectionMode() ||
+ SelectionRef().RangeCount() == 1) {
+ int32_t rowCountToRemove =
+ std::min(aNumberOfRowsToDelete, tableSize.mRowCount - startRowIndex);
+ for (int32_t i = 0; i < rowCountToRemove; i++) {
+ nsresult rv = DeleteTableRowWithTransaction(*table, startRowIndex);
+ // If failed in current row, try the next
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTableRowWithTransaction() failed, but trying "
+ "next...");
+ startRowIndex++;
+ }
+ // Check if there's a cell in the "next" row.
+ cell = GetTableCellElementAt(*table, startRowIndex, startColIndex);
+ if (!cell) {
+ return NS_OK;
+ }
+ }
+ return NS_OK;
+ }
+
+ // If 2 or more cells are selected, remove all rows which contain selected
+ // cells. I.e., we ignore aNumberOfRowsToDelete in this case.
+ const RefPtr<PresShell> presShell{GetPresShell()};
+ for (RefPtr<Element> selectedCellElement = scanner.GetFirstElement();
+ selectedCellElement;) {
+ if (selectedCellElement != scanner.ElementsRef()[0]) {
+ const CellIndexes cellIndexes(*selectedCellElement, presShell);
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ startRowIndex = cellIndexes.mRow;
+ startColIndex = cellIndexes.mColumn;
+ }
+ // Find the next cell in a different row
+ // to continue after we delete this row
+ int32_t nextRow = startRowIndex;
+ while (nextRow == startRowIndex) {
+ selectedCellElement = scanner.GetNextElement();
+ if (!selectedCellElement) {
+ break;
+ }
+ const CellIndexes cellIndexes(*selectedCellElement, presShell);
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ nextRow = cellIndexes.mRow;
+ startColIndex = cellIndexes.mColumn;
+ }
+ // Delete the row containing selected cell(s).
+ nsresult rv = DeleteTableRowWithTransaction(*table, startRowIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteTableRowWithTransaction() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+// Helper that doesn't batch or change the selection
+nsresult HTMLEditor::DeleteTableRowWithTransaction(Element& aTableElement,
+ int32_t aRowIndex) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, aTableElement);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.inspectErr();
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ // Prevent rules testing until we're done
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteNode, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return error.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+ error.SuppressException();
+
+ // Scan through cells in row to do rowspan adjustments
+ // Note that after we delete row, startRowIndex will point to the cells in
+ // the next row to be deleted.
+
+ // The list of cells we will change rowspan in and the new rowspan values
+ // for each.
+ struct MOZ_STACK_CLASS SpanCell final {
+ RefPtr<Element> mElement;
+ int32_t mNewRowSpanValue;
+
+ SpanCell(Element* aSpanCellElement, int32_t aNewRowSpanValue)
+ : mElement(aSpanCellElement), mNewRowSpanValue(aNewRowSpanValue) {}
+ };
+ AutoTArray<SpanCell, 10> spanCellArray;
+ RefPtr<Element> cellInDeleteRow;
+ int32_t columnIndex = 0;
+ while (aRowIndex < tableSize.mRowCount &&
+ columnIndex < tableSize.mColumnCount) {
+ const auto cellData = CellData::AtIndexInTableElement(
+ *this, aTableElement, aRowIndex, columnIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX So, we should distinguish if CellDate returns error or just not
+ // found later.
+ if (!cellData.mElement) {
+ break;
+ }
+
+ // Compensate for cells that don't start or extend below the row we are
+ // deleting.
+ if (cellData.IsSpannedFromOtherRow()) {
+ // If a cell starts in row above us, decrease its rowspan to keep table
+ // rectangular but we don't need to do this if rowspan=0, since it will
+ // be automatically adjusted.
+ if (cellData.mRowSpan > 0) {
+ // Build list of cells to change rowspan. We can't do it now since
+ // it upsets cell map, so we will do it after deleting the row.
+ int32_t newRowSpanValue = std::max(cellData.NumberOfPrecedingRows(),
+ cellData.NumberOfFollowingRows());
+ spanCellArray.AppendElement(
+ SpanCell(cellData.mElement, newRowSpanValue));
+ }
+ } else {
+ if (cellData.mRowSpan > 1) {
+ // Cell spans below row to delete, so we must insert new cells to
+ // keep rows below. Note that we test "rowSpan" so we don't do this
+ // if rowSpan = 0 (automatic readjustment).
+ int32_t aboveRowToInsertNewCellInto =
+ cellData.NumberOfPrecedingRows() + 1;
+ nsresult rv = SplitCellIntoRows(
+ &aTableElement, cellData.mFirst.mRow, cellData.mFirst.mColumn,
+ aboveRowToInsertNewCellInto, cellData.NumberOfFollowingRows(),
+ nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SplitCellIntoRows() failed");
+ return rv;
+ }
+ }
+ if (!cellInDeleteRow) {
+ // Reference cell to find row to delete.
+ cellInDeleteRow = std::move(cellData.mElement);
+ }
+ }
+ // Skip over other columns spanned by this cell
+ columnIndex += cellData.mEffectiveColSpan;
+ }
+
+ // Things are messed up if we didn't find a cell in the row!
+ if (!cellInDeleteRow) {
+ NS_WARNING("There was no cell in deleting row");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Delete the entire row.
+ RefPtr<Element> parentRow =
+ GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::tr, *cellInDeleteRow);
+ if (parentRow) {
+ nsresult rv = DeleteNodeWithTransaction(*parentRow);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::tr) "
+ "failed");
+ return rv;
+ }
+ }
+
+ // Now we can set new rowspans for cells stored above.
+ for (SpanCell& spanCell : spanCellArray) {
+ if (NS_WARN_IF(!spanCell.mElement)) {
+ continue;
+ }
+ nsresult rv =
+ SetRowSpan(MOZ_KnownLive(spanCell.mElement), spanCell.mNewRowSpanValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetRawSpan() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::SelectTable() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSelectTable);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::SelectTable() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Element> table =
+ GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table);
+ if (!table) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::table)"
+ " failed");
+ return NS_OK; // Don't fail if we didn't find a table.
+ }
+
+ rv = ClearSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::ClearSelection() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ rv = AppendContentToSelectionAsRange(*table);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::AppendContentToSelectionAsRange() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::SelectTableCell() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSelectTableCell);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::SelectTableCell() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Element> cell =
+ GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td);
+ if (!cell) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) "
+ "failed");
+ // Don't fail if we didn't find a cell.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ rv = ClearSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::ClearSelection() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ rv = AppendContentToSelectionAsRange(*cell);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::AppendContentToSelectionAsRange() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::SelectAllTableCells() {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eSelectAllTableCells);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::SelectAllTableCells() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Element> cell =
+ GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td);
+ if (!cell) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) "
+ "failed");
+ // Don't fail if we didn't find a cell.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ RefPtr<Element> startCell = cell;
+
+ // Get parent table
+ RefPtr<Element> table =
+ GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::table, *cell);
+ if (!table) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) "
+ "failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *table);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr());
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ // Suppress nsISelectionListener notification
+ // until all selection changes are finished
+ SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__);
+
+ // It is now safe to clear the selection
+ // BE SURE TO RESET IT BEFORE LEAVING!
+ rv = ClearSelection();
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::ClearSelection() caused destroying the editor");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::ClearSelection() failed, but might be ignored");
+
+ // Select all cells in the same column as current cell
+ bool cellSelected = false;
+ // Safety code to select starting cell if nothing else was selected
+ auto AppendContentToStartCell = [&]() MOZ_CAN_RUN_SCRIPT {
+ MOZ_ASSERT(!cellSelected);
+ // XXX In this case, we ignore `NS_ERROR_FAILURE` set by above inner
+ // `for` loop.
+ nsresult rv = AppendContentToSelectionAsRange(*startCell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::AppendContentToSelectionAsRange() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ };
+ for (int32_t row = 0; row < tableSize.mRowCount; row++) {
+ for (int32_t col = 0; col < tableSize.mColumnCount;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *table, row, col);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return !cellSelected ? AppendContentToStartCell() : NS_ERROR_FAILURE;
+ }
+
+ // Skip cells that are spanned from previous rows or columns
+ // XXX So, we should distinguish whether CellData returns error or just
+ // not found later.
+ if (cellData.mElement && !cellData.IsSpannedFromOtherRowOrColumn()) {
+ nsresult rv = AppendContentToSelectionAsRange(*cellData.mElement);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "HTMLEditor::AppendContentToSelectionAsRange() caused "
+ "destroying the editor");
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::AppendContentToSelectionAsRange() failed, but "
+ "might be ignored");
+ return !cellSelected ? AppendContentToStartCell()
+ : EditorBase::ToGenericNSResult(rv);
+ }
+ cellSelected = true;
+ }
+ MOZ_ASSERT(col < cellData.NextColumnIndex());
+ col = cellData.NextColumnIndex();
+ }
+ }
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::SelectTableRow() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSelectTableRow);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::SelectTableRow() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Element> cell =
+ GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td);
+ if (!cell) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) "
+ "failed");
+ // Don't fail if we didn't find a cell.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ RefPtr<Element> startCell = cell;
+
+ // Get table and location of cell:
+ RefPtr<Element> table;
+ int32_t startRowIndex, startColIndex;
+
+ rv = GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr,
+ nullptr, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (!table) {
+ NS_WARNING("HTMLEditor::GetCellContext() didn't return <table> element");
+ return NS_ERROR_FAILURE;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *table);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr());
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ // Note: At this point, we could get first and last cells in row,
+ // then call SelectBlockOfCells, but that would take just
+ // a little less code, so the following is more efficient
+
+ // Suppress nsISelectionListener notification
+ // until all selection changes are finished
+ SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__);
+
+ // It is now safe to clear the selection
+ // BE SURE TO RESET IT BEFORE LEAVING!
+ rv = ClearSelection();
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::ClearSelection() caused destroying the editor");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::ClearSelection() failed, but might be ignored");
+
+ // Select all cells in the same row as current cell
+ bool cellSelected = false;
+ for (int32_t col = 0; col < tableSize.mColumnCount;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *table, startRowIndex, col);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ if (cellSelected) {
+ return NS_ERROR_FAILURE;
+ }
+ // Safety code to select starting cell if nothing else was selected
+ nsresult rv = AppendContentToSelectionAsRange(*startCell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::AppendContentToSelectionAsRange() failed");
+ NS_WARNING_ASSERTION(
+ cellData.isOk() || NS_SUCCEEDED(rv) ||
+ NS_FAILED(EditorBase::ToGenericNSResult(rv)),
+ "CellData::AtIndexInTableElement() failed, but ignored");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Skip cells that are spanned from previous rows or columns
+ // XXX So, we should distinguish whether CellData returns error or just
+ // not found later.
+ if (cellData.mElement && !cellData.IsSpannedFromOtherRowOrColumn()) {
+ nsresult rv = AppendContentToSelectionAsRange(*cellData.mElement);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "HTMLEditor::AppendContentToSelectionAsRange() caused destroying "
+ "the editor");
+ return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ if (cellSelected) {
+ NS_WARNING("HTMLEditor::AppendContentToSelectionAsRange() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ // Safety code to select starting cell if nothing else was selected
+ nsresult rvTryAgain = AppendContentToSelectionAsRange(*startCell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::AppendContentToSelectionAsRange() failed");
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(EditorBase::ToGenericNSResult(rv)) ||
+ NS_SUCCEEDED(rvTryAgain) ||
+ NS_FAILED(EditorBase::ToGenericNSResult(rvTryAgain)),
+ "HTMLEditor::AppendContentToSelectionAsRange(*cellData.mElement) "
+ "failed, but ignored");
+ return EditorBase::ToGenericNSResult(rvTryAgain);
+ }
+ cellSelected = true;
+ }
+ MOZ_ASSERT(col < cellData.NextColumnIndex());
+ col = cellData.NextColumnIndex();
+ }
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::SelectTableColumn() {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eSelectTableColumn);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::SelectTableColumn() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Element> cell =
+ GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td);
+ if (!cell) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::td) "
+ "failed");
+ // Don't fail if we didn't find a cell.
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ RefPtr<Element> startCell = cell;
+
+ // Get location of cell:
+ RefPtr<Element> table;
+ int32_t startRowIndex, startColIndex;
+
+ rv = GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr,
+ nullptr, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (!table) {
+ NS_WARNING("HTMLEditor::GetCellContext() didn't return <table> element");
+ return NS_ERROR_FAILURE;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *table);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr());
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ // Suppress nsISelectionListener notification
+ // until all selection changes are finished
+ SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__);
+
+ // It is now safe to clear the selection
+ // BE SURE TO RESET IT BEFORE LEAVING!
+ rv = ClearSelection();
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::ClearSelection() caused destroying the editor");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::ClearSelection() failed, but might be ignored");
+
+ // Select all cells in the same column as current cell
+ bool cellSelected = false;
+ for (int32_t row = 0; row < tableSize.mRowCount;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *table, row, startColIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ if (cellSelected) {
+ return NS_ERROR_FAILURE;
+ }
+ // Safety code to select starting cell if nothing else was selected
+ nsresult rv = AppendContentToSelectionAsRange(*startCell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::AppendContentToSelectionAsRange() failed");
+ NS_WARNING_ASSERTION(
+ cellData.isOk() || NS_SUCCEEDED(rv) ||
+ NS_FAILED(EditorBase::ToGenericNSResult(rv)),
+ "CellData::AtIndexInTableElement() failed, but ignored");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Skip cells that are spanned from previous rows or columns
+ // XXX So, we should distinguish whether CellData returns error or just
+ // not found later.
+ if (cellData.mElement && !cellData.IsSpannedFromOtherRowOrColumn()) {
+ nsresult rv = AppendContentToSelectionAsRange(*cellData.mElement);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "HTMLEditor::AppendContentToSelectionAsRange() caused destroying "
+ "the editor");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (NS_FAILED(rv)) {
+ if (cellSelected) {
+ NS_WARNING("HTMLEditor::AppendContentToSelectionAsRange() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ // Safety code to select starting cell if nothing else was selected
+ nsresult rvTryAgain = AppendContentToSelectionAsRange(*startCell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::AppendContentToSelectionAsRange() failed");
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(EditorBase::ToGenericNSResult(rv)) ||
+ NS_SUCCEEDED(rvTryAgain) ||
+ NS_FAILED(EditorBase::ToGenericNSResult(rvTryAgain)),
+ "HTMLEditor::AppendContentToSelectionAsRange(*cellData.mElement) "
+ "failed, but ignored");
+ return EditorBase::ToGenericNSResult(rvTryAgain);
+ }
+ cellSelected = true;
+ }
+ MOZ_ASSERT(row < cellData.NextRowIndex());
+ row = cellData.NextRowIndex();
+ }
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP HTMLEditor::SplitTableCell() {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eSplitTableCellElement);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Element> table;
+ RefPtr<Element> cell;
+ int32_t startRowIndex, startColIndex, actualRowSpan, actualColSpan;
+ // Get cell, table, etc. at selection anchor node
+ rv = GetCellContext(getter_AddRefs(table), getter_AddRefs(cell), nullptr,
+ nullptr, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (!table || !cell) {
+ NS_WARNING(
+ "HTMLEditor::GetCellContext() didn't return <table> and/or cell");
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ // We need rowspan and colspan data
+ rv = GetCellSpansAt(table, startRowIndex, startColIndex, actualRowSpan,
+ actualColSpan);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellSpansAt() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Must have some span to split
+ if (actualRowSpan <= 1 && actualColSpan <= 1) {
+ return NS_OK;
+ }
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ // Prevent auto insertion of BR in new cell until we're done
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return EditorBase::ToGenericNSResult(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // We reset selection
+ AutoSelectionSetterAfterTableEdit setCaret(
+ *this, table, startRowIndex, startColIndex, ePreviousColumn, false);
+ //...so suppress Rules System selection munging
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+
+ RefPtr<Element> newCell;
+ int32_t rowIndex = startRowIndex;
+ int32_t rowSpanBelow, colSpanAfter;
+
+ // Split up cell row-wise first into rowspan=1 above, and the rest below,
+ // whittling away at the cell below until no more extra span
+ for (rowSpanBelow = actualRowSpan - 1; rowSpanBelow >= 0; rowSpanBelow--) {
+ // We really split row-wise only if we had rowspan > 1
+ if (rowSpanBelow > 0) {
+ nsresult rv = SplitCellIntoRows(table, rowIndex, startColIndex, 1,
+ rowSpanBelow, getter_AddRefs(newCell));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SplitCellIntoRows() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ DebugOnly<nsresult> rvIgnored = CopyCellBackgroundColor(newCell, cell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::CopyCellBackgroundColor() failed, but ignored");
+ }
+ int32_t colIndex = startColIndex;
+ // Now split the cell with rowspan = 1 into cells if it has colSpan > 1
+ for (colSpanAfter = actualColSpan - 1; colSpanAfter > 0; colSpanAfter--) {
+ nsresult rv = SplitCellIntoColumns(table, rowIndex, colIndex, 1,
+ colSpanAfter, getter_AddRefs(newCell));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SplitCellIntoColumns() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ DebugOnly<nsresult> rvIgnored = CopyCellBackgroundColor(newCell, cell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::CopyCellBackgroundColor() failed, but ignored");
+ colIndex++;
+ }
+ // Point to the new cell and repeat
+ rowIndex++;
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::CopyCellBackgroundColor(Element* aDestCell,
+ Element* aSourceCell) {
+ if (NS_WARN_IF(!aDestCell) || NS_WARN_IF(!aSourceCell)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aSourceCell->HasAttr(nsGkAtoms::bgcolor)) {
+ return NS_OK;
+ }
+
+ // Copy backgournd color to new cell.
+ nsString backgroundColor;
+ aSourceCell->GetAttr(nsGkAtoms::bgcolor, backgroundColor);
+ nsresult rv = SetAttributeWithTransaction(*aDestCell, *nsGkAtoms::bgcolor,
+ backgroundColor);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::bgcolor) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SplitCellIntoColumns(Element* aTable, int32_t aRowIndex,
+ int32_t aColIndex,
+ int32_t aColSpanLeft,
+ int32_t aColSpanRight,
+ Element** aNewCell) {
+ if (NS_WARN_IF(!aTable)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (aNewCell) {
+ *aNewCell = nullptr;
+ }
+
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, aColIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // We can't split!
+ if (cellData.mEffectiveColSpan <= 1 ||
+ aColSpanLeft + aColSpanRight > cellData.mEffectiveColSpan) {
+ return NS_OK;
+ }
+
+ // Reduce colspan of cell to split
+ nsresult rv = SetColSpan(cellData.mElement, aColSpanLeft);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetColSpan() failed");
+ return rv;
+ }
+
+ // Insert new cell after using the remaining span
+ // and always get the new cell so we can copy the background color;
+ RefPtr<Element> newCellElement;
+ rv = InsertCell(cellData.mElement, cellData.mEffectiveRowSpan, aColSpanRight,
+ true, false, getter_AddRefs(newCellElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::InsertCell() failed");
+ return rv;
+ }
+ if (!newCellElement) {
+ return NS_OK;
+ }
+ if (aNewCell) {
+ *aNewCell = do_AddRef(newCellElement).take();
+ }
+ rv = CopyCellBackgroundColor(newCellElement, cellData.mElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::CopyCellBackgroundColor() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::SplitCellIntoRows(Element* aTable, int32_t aRowIndex,
+ int32_t aColIndex, int32_t aRowSpanAbove,
+ int32_t aRowSpanBelow,
+ Element** aNewCell) {
+ if (NS_WARN_IF(!aTable)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (aNewCell) {
+ *aNewCell = nullptr;
+ }
+
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, aColIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // We can't split!
+ if (cellData.mEffectiveRowSpan <= 1 ||
+ aRowSpanAbove + aRowSpanBelow > cellData.mEffectiveRowSpan) {
+ return NS_OK;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *aTable);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.inspectErr();
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ // Find a cell to insert before or after
+ RefPtr<Element> cellElementAtInsertionPoint;
+ RefPtr<Element> lastCellFound;
+ bool insertAfter = (cellData.mFirst.mColumn > 0);
+ for (int32_t colIndex = 0,
+ rowBelowIndex = cellData.mFirst.mRow + aRowSpanAbove;
+ colIndex <= tableSize.mColumnCount;) {
+ const auto cellDataAtInsertionPoint = CellData::AtIndexInTableElement(
+ *this, *aTable, rowBelowIndex, colIndex);
+ // If we fail here, it could be because row has bad rowspan values,
+ // such as all cells having rowspan > 1 (Call FixRowSpan first!).
+ // XXX According to the comment, this does not assume that
+ // FixRowSpan() doesn't work well and user can create non-rectangular
+ // table. So, we should not return error when CellData cannot find
+ // a cell.
+ if (NS_WARN_IF(cellDataAtInsertionPoint.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // FYI: Don't use std::move() here since the following checks will use
+ // utility methods of cellDataAtInsertionPoint, but some of them
+ // check whether its mElement is not nullptr.
+ cellElementAtInsertionPoint = cellDataAtInsertionPoint.mElement;
+
+ // Skip over cells spanned from above (like the one we are splitting!)
+ if (cellDataAtInsertionPoint.mElement &&
+ !cellDataAtInsertionPoint.IsSpannedFromOtherRow()) {
+ if (!insertAfter) {
+ // Inserting before, so stop at first cell in row we want to insert
+ // into.
+ break;
+ }
+ // New cell isn't first in row,
+ // so stop after we find the cell just before new cell's column
+ if (cellDataAtInsertionPoint.NextColumnIndex() ==
+ cellData.mFirst.mColumn) {
+ break;
+ }
+ // If cell found is AFTER desired new cell colum,
+ // we have multiple cells with rowspan > 1 that
+ // prevented us from finding a cell to insert after...
+ if (cellDataAtInsertionPoint.mFirst.mColumn > cellData.mFirst.mColumn) {
+ // ... so instead insert before the cell we found
+ insertAfter = false;
+ break;
+ }
+ // FYI: Don't use std::move() here since
+ // cellDataAtInsertionPoint.NextColumnIndex() needs it.
+ lastCellFound = cellDataAtInsertionPoint.mElement;
+ }
+ MOZ_ASSERT(colIndex < cellDataAtInsertionPoint.NextColumnIndex());
+ colIndex = cellDataAtInsertionPoint.NextColumnIndex();
+ }
+
+ if (!cellElementAtInsertionPoint && lastCellFound) {
+ // Edge case where we didn't find a cell to insert after
+ // or before because column(s) before desired column
+ // and all columns after it are spanned from above.
+ // We can insert after the last cell we found
+ cellElementAtInsertionPoint = std::move(lastCellFound);
+ insertAfter = true; // Should always be true, but let's be sure
+ }
+
+ // Reduce rowspan of cell to split
+ nsresult rv = SetRowSpan(cellData.mElement, aRowSpanAbove);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetRowSpan() failed");
+ return rv;
+ }
+
+ // Insert new cell after using the remaining span
+ // and always get the new cell so we can copy the background color;
+ RefPtr<Element> newCell;
+ rv = InsertCell(cellElementAtInsertionPoint, aRowSpanBelow,
+ cellData.mEffectiveColSpan, insertAfter, false,
+ getter_AddRefs(newCell));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::InsertCell() failed");
+ return rv;
+ }
+ if (!newCell) {
+ return NS_OK;
+ }
+ if (aNewCell) {
+ *aNewCell = do_AddRef(newCell).take();
+ }
+ rv = CopyCellBackgroundColor(newCell, cellElementAtInsertionPoint);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::CopyCellBackgroundColor() failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::SwitchTableCellHeaderType(Element* aSourceCell,
+ Element** aNewCell) {
+ if (NS_WARN_IF(!aSourceCell)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eSetTableCellElementType);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ // Prevent auto insertion of BR in new cell created by
+ // ReplaceContainerAndCloneAttributesWithTransaction().
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return EditorBase::ToGenericNSResult(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // Save current selection to restore when done.
+ // This is needed so ReplaceContainerAndCloneAttributesWithTransaction()
+ // can monitor selection when replacing nodes.
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ // Set to the opposite of current type
+ nsAtom* newCellName =
+ aSourceCell->IsHTMLElement(nsGkAtoms::td) ? nsGkAtoms::th : nsGkAtoms::td;
+
+ // This creates new node, moves children, copies attributes (true)
+ // and manages the selection!
+ Result<CreateElementResult, nsresult> newCellElementOrError =
+ ReplaceContainerAndCloneAttributesWithTransaction(
+ *aSourceCell, MOZ_KnownLive(*newCellName));
+ if (MOZ_UNLIKELY(newCellElementOrError.isErr())) {
+ NS_WARNING(
+ "EditorBase::ReplaceContainerAndCloneAttributesWithTransaction() "
+ "failed");
+ return newCellElementOrError.unwrapErr();
+ }
+ // restoreSelectionLater will change selection
+ newCellElementOrError.inspect().IgnoreCaretPointSuggestion();
+
+ // Return the new cell
+ if (aNewCell) {
+ newCellElementOrError.unwrap().UnwrapNewNode().forget(aNewCell);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::JoinTableCells(bool aMergeNonContiguousContents) {
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eJoinTableCellElements);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ RefPtr<Element> table;
+ RefPtr<Element> targetCell;
+ int32_t startRowIndex, startColIndex;
+
+ // Get cell, table, etc. at selection anchor node
+ rv = GetCellContext(getter_AddRefs(table), getter_AddRefs(targetCell),
+ nullptr, nullptr, &startRowIndex, &startColIndex);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellContext() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (!table || !targetCell) {
+ NS_WARNING(
+ "HTMLEditor::GetCellContext() didn't return <table> and/or cell");
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ return NS_ERROR_FAILURE; // XXX Should we just return NS_OK?
+ }
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ // Don't let Rules System change the selection
+ AutoTransactionsConserveSelection dontChangeSelection(*this);
+
+ // Note: We dont' use AutoSelectionSetterAfterTableEdit here so the selection
+ // is retained after joining. This leaves the target cell selected
+ // as well as the "non-contiguous" cells, so user can see what happened.
+
+ SelectedTableCellScanner scanner(SelectionRef());
+
+ // If only one cell is selected, join with cell to the right
+ if (scanner.ElementsRef().Length() > 1) {
+ // We have selected cells: Join just contiguous cells
+ // and just merge contents if not contiguous
+ Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *table);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return EditorBase::ToGenericNSResult(tableSizeOrError.unwrapErr());
+ }
+ // FYI: Cannot be const because the row count will be updated
+ TableSize tableSize = tableSizeOrError.unwrap();
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ // `MOZ_KnownLive(scanner.ElementsRef()[0])` is safe because `scanner`
+ // grabs it until it's destroyed later.
+ const CellIndexes firstSelectedCellIndexes(
+ MOZ_KnownLive(scanner.ElementsRef()[0]), presShell);
+ if (NS_WARN_IF(firstSelectedCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get spans for cell we will merge into
+ int32_t firstRowSpan, firstColSpan;
+ nsresult rv = GetCellSpansAt(table, firstSelectedCellIndexes.mRow,
+ firstSelectedCellIndexes.mColumn, firstRowSpan,
+ firstColSpan);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetCellSpansAt() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // This defines the last indexes along the "edges"
+ // of the contiguous block of cells, telling us
+ // that we can join adjacent cells to the block
+ // Start with same as the first values,
+ // then expand as we find adjacent selected cells
+ int32_t lastRowIndex = firstSelectedCellIndexes.mRow;
+ int32_t lastColIndex = firstSelectedCellIndexes.mColumn;
+
+ // First pass: Determine boundaries of contiguous rectangular block that
+ // we will join into one cell, favoring adjacent cells in the same row.
+ for (int32_t rowIndex = firstSelectedCellIndexes.mRow;
+ rowIndex <= lastRowIndex; rowIndex++) {
+ int32_t currentRowCount = tableSize.mRowCount;
+ // Be sure each row doesn't have rowspan errors
+ rv = FixBadRowSpan(table, rowIndex, tableSize.mRowCount);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::FixBadRowSpan() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ // Adjust rowcount by number of rows we removed
+ lastRowIndex -= currentRowCount - tableSize.mRowCount;
+
+ bool cellFoundInRow = false;
+ bool lastRowIsSet = false;
+ int32_t lastColInRow = 0;
+ int32_t firstColInRow = firstSelectedCellIndexes.mColumn;
+ int32_t colIndex = firstSelectedCellIndexes.mColumn;
+ for (; colIndex < tableSize.mColumnCount;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *table, rowIndex, colIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (cellData.mIsSelected) {
+ if (!cellFoundInRow) {
+ // We've just found the first selected cell in this row
+ firstColInRow = cellData.mCurrent.mColumn;
+ }
+ if (cellData.mCurrent.mRow > firstSelectedCellIndexes.mRow &&
+ firstColInRow != firstSelectedCellIndexes.mColumn) {
+ // We're in at least the second row,
+ // but left boundary is "ragged" (not the same as 1st row's start)
+ // Let's just end block on previous row
+ // and keep previous lastColIndex
+ // TODO: We could try to find the Maximum firstColInRow
+ // so our block can still extend down more rows?
+ lastRowIndex = std::max(0, cellData.mCurrent.mRow - 1);
+ lastRowIsSet = true;
+ break;
+ }
+ // Save max selected column in this row, including extra colspan
+ lastColInRow = cellData.LastColumnIndex();
+ cellFoundInRow = true;
+ } else if (cellFoundInRow) {
+ // No cell or not selected, but at least one cell in row was found
+ if (cellData.mCurrent.mRow > firstSelectedCellIndexes.mRow + 1 &&
+ cellData.mCurrent.mColumn <= lastColIndex) {
+ // Cell is in a column less than current right border in
+ // the third or higher selected row, so stop block at the previous
+ // row
+ lastRowIndex = std::max(0, cellData.mCurrent.mRow - 1);
+ lastRowIsSet = true;
+ }
+ // We're done with this row
+ break;
+ }
+ MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
+ colIndex = cellData.NextColumnIndex();
+ } // End of column loop
+
+ // Done with this row
+ if (cellFoundInRow) {
+ if (rowIndex == firstSelectedCellIndexes.mRow) {
+ // First row always initializes the right boundary
+ lastColIndex = lastColInRow;
+ }
+
+ // If we didn't determine last row above...
+ if (!lastRowIsSet) {
+ if (colIndex < lastColIndex) {
+ // (don't think we ever get here?)
+ // Cell is in a column less than current right boundary,
+ // so stop block at the previous row
+ lastRowIndex = std::max(0, rowIndex - 1);
+ } else {
+ // Go on to examine next row
+ lastRowIndex = rowIndex + 1;
+ }
+ }
+ // Use the minimum col we found so far for right boundary
+ lastColIndex = std::min(lastColIndex, lastColInRow);
+ } else {
+ // No selected cells in this row -- stop at row above
+ // and leave last column at its previous value
+ lastRowIndex = std::max(0, rowIndex - 1);
+ }
+ }
+
+ // The list of cells we will delete after joining
+ nsTArray<RefPtr<Element>> deleteList;
+
+ // 2nd pass: Do the joining and merging
+ for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) {
+ for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *table, rowIndex, colIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If this is 0, we are past last cell in row, so exit the loop
+ if (!cellData.mEffectiveColSpan) {
+ break;
+ }
+
+ // Merge only selected cells (skip cell we're merging into, of course)
+ if (cellData.mIsSelected &&
+ cellData.mElement != scanner.ElementsRef()[0]) {
+ if (cellData.mCurrent.mRow >= firstSelectedCellIndexes.mRow &&
+ cellData.mCurrent.mRow <= lastRowIndex &&
+ cellData.mCurrent.mColumn >= firstSelectedCellIndexes.mColumn &&
+ cellData.mCurrent.mColumn <= lastColIndex) {
+ // We are within the join region
+ // Problem: It is very tricky to delete cells as we merge,
+ // since that will upset the cellmap
+ // Instead, build a list of cells to delete and do it later
+ NS_ASSERTION(!cellData.IsSpannedFromOtherRow(),
+ "JoinTableCells: StartRowIndex is in row above");
+
+ if (cellData.mEffectiveColSpan > 1) {
+ // Check if cell "hangs" off the boundary because of colspan > 1
+ // Use split methods to chop off excess
+ int32_t extraColSpan = cellData.mFirst.mColumn +
+ cellData.mEffectiveColSpan -
+ (lastColIndex + 1);
+ if (extraColSpan > 0) {
+ nsresult rv = SplitCellIntoColumns(
+ table, cellData.mFirst.mRow, cellData.mFirst.mColumn,
+ cellData.mEffectiveColSpan - extraColSpan, extraColSpan,
+ nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SplitCellIntoColumns() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+ }
+
+ nsresult rv =
+ MergeCells(scanner.ElementsRef()[0], cellData.mElement, false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::MergeCells() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Add cell to list to delete
+ deleteList.AppendElement(cellData.mElement.get());
+ } else if (aMergeNonContiguousContents) {
+ // Cell is outside join region -- just merge the contents
+ nsresult rv =
+ MergeCells(scanner.ElementsRef()[0], cellData.mElement, false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::MergeCells() failed");
+ return rv;
+ }
+ }
+ }
+ MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
+ colIndex = cellData.NextColumnIndex();
+ }
+ }
+
+ // All cell contents are merged. Delete the empty cells we accumulated
+ // Prevent rules testing until we're done
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteNode, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return EditorBase::ToGenericNSResult(error.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() "
+ "failed, but ignored");
+
+ for (uint32_t i = 0, n = deleteList.Length(); i < n; i++) {
+ RefPtr<Element> nodeToBeRemoved = deleteList[i];
+ if (nodeToBeRemoved) {
+ nsresult rv = DeleteNodeWithTransaction(*nodeToBeRemoved);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+ }
+ // Cleanup selection: remove ranges where cells were deleted
+ uint32_t rangeCount = SelectionRef().RangeCount();
+
+ // TODO: Rewriting this with reversed ranged-loop may make it simpler.
+ RefPtr<nsRange> range;
+ for (uint32_t i = 0; i < rangeCount; i++) {
+ range = SelectionRef().GetRangeAt(i);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ Element* deletedCell =
+ HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(*range);
+ if (!deletedCell) {
+ SelectionRef().RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
+ error);
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "Selection::RemoveRangeAndUnselectFramesAndNotifyListeners() "
+ "failed, but ignored");
+ rangeCount--;
+ i--;
+ }
+ }
+
+ // Set spans for the cell everything merged into
+ rv = SetRowSpan(MOZ_KnownLive(scanner.ElementsRef()[0]),
+ lastRowIndex - firstSelectedCellIndexes.mRow + 1);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetRowSpan() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ rv = SetColSpan(MOZ_KnownLive(scanner.ElementsRef()[0]),
+ lastColIndex - firstSelectedCellIndexes.mColumn + 1);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetColSpan() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Fixup disturbances in table layout
+ DebugOnly<nsresult> rvIgnored = NormalizeTableInternal(*table);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::NormalizeTableInternal() failed, but ignored");
+ } else {
+ // Joining with cell to the right -- get rowspan and colspan data of target
+ // cell.
+ const auto leftCellData = CellData::AtIndexInTableElement(
+ *this, *table, startRowIndex, startColIndex);
+ if (NS_WARN_IF(leftCellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get data for cell to the right.
+ const auto rightCellData = CellData::AtIndexInTableElement(
+ *this, *table, leftCellData.mFirst.mRow,
+ leftCellData.mFirst.mColumn + leftCellData.mEffectiveColSpan);
+ if (NS_WARN_IF(rightCellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX So, this does not assume that CellData returns error when just not
+ // found. We need to fix this later.
+ if (!rightCellData.mElement) {
+ return NS_OK; // Don't fail if there's no cell
+ }
+
+ // sanity check
+ NS_ASSERTION(
+ rightCellData.mCurrent.mRow >= rightCellData.mFirst.mRow,
+ "JoinCells: rightCellData.mCurrent.mRow < rightCellData.mFirst.mRow");
+
+ // Figure out span of merged cell starting from target's starting row
+ // to handle case of merged cell starting in a row above
+ int32_t spanAboveMergedCell = rightCellData.NumberOfPrecedingRows();
+ int32_t effectiveRowSpan2 =
+ rightCellData.mEffectiveRowSpan - spanAboveMergedCell;
+ if (effectiveRowSpan2 > leftCellData.mEffectiveRowSpan) {
+ // Cell to the right spans into row below target
+ // Split off portion below target cell's bottom-most row
+ nsresult rv = SplitCellIntoRows(
+ table, rightCellData.mFirst.mRow, rightCellData.mFirst.mColumn,
+ spanAboveMergedCell + leftCellData.mEffectiveRowSpan,
+ effectiveRowSpan2 - leftCellData.mEffectiveRowSpan, nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SplitCellIntoRows() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+
+ // Move contents from cell to the right
+ // Delete the cell now only if it starts in the same row
+ // and has enough row "height"
+ nsresult rv =
+ MergeCells(leftCellData.mElement, rightCellData.mElement,
+ !rightCellData.IsSpannedFromOtherRow() &&
+ effectiveRowSpan2 >= leftCellData.mEffectiveRowSpan);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::MergeCells() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (effectiveRowSpan2 < leftCellData.mEffectiveRowSpan) {
+ // Merged cell is "shorter"
+ // (there are cells(s) below it that are row-spanned by target cell)
+ // We could try splitting those cells, but that's REAL messy,
+ // so the safest thing to do is NOT really join the cells
+ return NS_OK;
+ }
+
+ if (spanAboveMergedCell > 0) {
+ // Cell we merged started in a row above the target cell
+ // Reduce rowspan to give room where target cell will extend its colspan
+ nsresult rv = SetRowSpan(rightCellData.mElement, spanAboveMergedCell);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetRowSpan() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+
+ // Reset target cell's colspan to encompass cell to the right
+ rv = SetColSpan(leftCellData.mElement, leftCellData.mEffectiveColSpan +
+ rightCellData.mEffectiveColSpan);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetColSpan() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::MergeCells(RefPtr<Element> aTargetCell,
+ RefPtr<Element> aCellToMerge,
+ bool aDeleteCellToMerge) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aTargetCell) || NS_WARN_IF(!aCellToMerge)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Prevent rules testing until we're done
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eDeleteNode, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // Don't need to merge if cell is empty
+ if (!IsEmptyCell(aCellToMerge)) {
+ // Get index of last child in target cell
+ // If we fail or don't have children,
+ // we insert at index 0
+ int32_t insertIndex = 0;
+
+ // Start inserting just after last child
+ uint32_t len = aTargetCell->GetChildCount();
+ if (len == 1 && IsEmptyCell(aTargetCell)) {
+ // Delete the empty node
+ nsCOMPtr<nsIContent> cellChild = aTargetCell->GetFirstChild();
+ if (NS_WARN_IF(!cellChild)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = DeleteNodeWithTransaction(*cellChild);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ insertIndex = 0;
+ } else {
+ insertIndex = (int32_t)len;
+ }
+
+ // Move the contents
+ EditorDOMPoint pointToPutCaret;
+ while (aCellToMerge->HasChildren()) {
+ nsCOMPtr<nsIContent> cellChild = aCellToMerge->GetLastChild();
+ if (NS_WARN_IF(!cellChild)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = DeleteNodeWithTransaction(*cellChild);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ Result<CreateContentResult, nsresult> insertChildContentResult =
+ InsertNodeWithTransaction(*cellChild,
+ EditorDOMPoint(aTargetCell, insertIndex));
+ if (MOZ_UNLIKELY(insertChildContentResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertChildContentResult.unwrapErr();
+ }
+ CreateContentResult unwrappedInsertChildContentResult =
+ insertChildContentResult.unwrap();
+ unwrappedInsertChildContentResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+
+ if (!aDeleteCellToMerge) {
+ return NS_OK;
+ }
+
+ // Delete cells whose contents were moved.
+ nsresult rv = DeleteNodeWithTransaction(*aCellToMerge);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::FixBadRowSpan(Element* aTable, int32_t aRowIndex,
+ int32_t& aNewRowCount) {
+ if (NS_WARN_IF(!aTable)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *aTable);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.inspectErr();
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ int32_t minRowSpan = -1;
+ for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, colIndex);
+ // NOTE: This is a *real* failure.
+ // CellData passes if cell is missing from cellmap
+ // XXX If <table> has large rowspan value or colspan value than actual
+ // cells, we may hit error. So, this method is always failed to
+ // "fix" the rowspan...
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX So, this does not assume that CellData returns error when just not
+ // found. We need to fix this later.
+ if (!cellData.mElement) {
+ break;
+ }
+
+ if (cellData.mRowSpan > 0 && !cellData.IsSpannedFromOtherRow() &&
+ (cellData.mRowSpan < minRowSpan || minRowSpan == -1)) {
+ minRowSpan = cellData.mRowSpan;
+ }
+ MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
+ colIndex = cellData.NextColumnIndex();
+ }
+
+ if (minRowSpan > 1) {
+ // The amount to reduce everyone's rowspan
+ // so at least one cell has rowspan = 1
+ int32_t rowsReduced = minRowSpan - 1;
+ for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, colIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Fixup rowspans only for cells starting in current row
+ // XXX So, this does not assume that CellData returns error when just
+ // not found a cell. Fix this later.
+ if (cellData.mElement && cellData.mRowSpan > 0 &&
+ !cellData.IsSpannedFromOtherRowOrColumn()) {
+ nsresult rv =
+ SetRowSpan(cellData.mElement, cellData.mRowSpan - rowsReduced);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetRawSpan() failed");
+ return rv;
+ }
+ }
+ MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
+ colIndex = cellData.NextColumnIndex();
+ }
+ }
+ const Result<TableSize, nsresult> newTableSizeOrError =
+ TableSize::Create(*this, *aTable);
+ if (NS_WARN_IF(newTableSizeOrError.isErr())) {
+ return newTableSizeOrError.inspectErr();
+ }
+ aNewRowCount = newTableSizeOrError.inspect().mRowCount;
+ return NS_OK;
+}
+
+nsresult HTMLEditor::FixBadColSpan(Element* aTable, int32_t aColIndex,
+ int32_t& aNewColCount) {
+ if (NS_WARN_IF(!aTable)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *aTable);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.inspectErr();
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ int32_t minColSpan = -1;
+ for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *aTable, rowIndex, aColIndex);
+ // NOTE: This is a *real* failure.
+ // CellData passes if cell is missing from cellmap
+ // XXX If <table> has large rowspan value or colspan value than actual
+ // cells, we may hit error. So, this method is always failed to
+ // "fix" the colspan...
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX So, this does not assume that CellData returns error when just
+ // not found a cell. Fix this later.
+ if (!cellData.mElement) {
+ break;
+ }
+ if (cellData.mColSpan > 0 && !cellData.IsSpannedFromOtherColumn() &&
+ (cellData.mColSpan < minColSpan || minColSpan == -1)) {
+ minColSpan = cellData.mColSpan;
+ }
+ MOZ_ASSERT(rowIndex < cellData.NextRowIndex());
+ rowIndex = cellData.NextRowIndex();
+ }
+
+ if (minColSpan > 1) {
+ // The amount to reduce everyone's colspan
+ // so at least one cell has colspan = 1
+ int32_t colsReduced = minColSpan - 1;
+ for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *aTable, rowIndex, aColIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Fixup colspans only for cells starting in current column
+ // XXX So, this does not assume that CellData returns error when just
+ // not found a cell. Fix this later.
+ if (cellData.mElement && cellData.mColSpan > 0 &&
+ !cellData.IsSpannedFromOtherRowOrColumn()) {
+ nsresult rv =
+ SetColSpan(cellData.mElement, cellData.mColSpan - colsReduced);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetColSpan() failed");
+ return rv;
+ }
+ }
+ MOZ_ASSERT(rowIndex < cellData.NextRowIndex());
+ rowIndex = cellData.NextRowIndex();
+ }
+ }
+ const Result<TableSize, nsresult> newTableSizeOrError =
+ TableSize::Create(*this, *aTable);
+ if (NS_WARN_IF(newTableSizeOrError.isErr())) {
+ return newTableSizeOrError.inspectErr();
+ }
+ aNewColCount = newTableSizeOrError.inspect().mColumnCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::NormalizeTable(Element* aTableOrElementInTable) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNormalizeTable);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (!aTableOrElementInTable) {
+ aTableOrElementInTable =
+ GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table);
+ if (!aTableOrElementInTable) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::"
+ "table) failed");
+ return NS_OK; // Don't throw error even if the element is not in <table>.
+ }
+ }
+ rv = NormalizeTableInternal(*aTableOrElementInTable);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::NormalizeTableInternal() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult HTMLEditor::NormalizeTableInternal(Element& aTableOrElementInTable) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Element> tableElement;
+ if (aTableOrElementInTable.NodeInfo()->NameAtom() == nsGkAtoms::table) {
+ tableElement = &aTableOrElementInTable;
+ } else {
+ tableElement = GetInclusiveAncestorByTagNameInternal(
+ *nsGkAtoms::table, aTableOrElementInTable);
+ if (!tableElement) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) "
+ "failed");
+ return NS_OK; // Don't throw error even if the element is not in <table>.
+ }
+ }
+
+ Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *tableElement);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return tableSizeOrError.unwrapErr();
+ }
+ // FYI: Cannot be const because the row/column count will be updated
+ TableSize tableSize = tableSizeOrError.unwrap();
+
+ // Save current selection
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ AutoPlaceholderBatch treateAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ // Prevent auto insertion of BR in new cell until we're done
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertNode, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return error.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // XXX If there is a cell which has bigger or smaller "rowspan" or "colspan"
+ // values, FixBadRowSpan() will return error. So, we can do nothing
+ // if the table needs normalization...
+ // Scan all cells in each row to detect bad rowspan values
+ for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) {
+ nsresult rv = FixBadRowSpan(tableElement, rowIndex, tableSize.mRowCount);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::FixBadRowSpan() failed");
+ return rv;
+ }
+ }
+ // and same for colspans
+ for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount; colIndex++) {
+ nsresult rv = FixBadColSpan(tableElement, colIndex, tableSize.mColumnCount);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::FixBadColSpan() failed");
+ return rv;
+ }
+ }
+
+ // Fill in missing cellmap locations with empty cells
+ for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) {
+ RefPtr<Element> previousCellElementInRow;
+ for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount; colIndex++) {
+ const auto cellData = CellData::AtIndexInTableElement(
+ *this, *tableElement, rowIndex, colIndex);
+ // NOTE: This is a *real* failure.
+ // CellData passes if cell is missing from cellmap
+ // XXX So, this method assumes that CellData won't return error when
+ // just not found. Fix this later.
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (cellData.mElement) {
+ // Save the last cell found in the same row we are scanning
+ if (!cellData.IsSpannedFromOtherRow()) {
+ previousCellElementInRow = std::move(cellData.mElement);
+ }
+ continue;
+ }
+
+ // We are missing a cell at a cellmap location.
+ // Add a cell after the previous cell element in the current row.
+ if (NS_WARN_IF(!previousCellElementInRow)) {
+ // We don't have any cells in this row -- We are really messed up!
+ return NS_ERROR_FAILURE;
+ }
+
+ // Insert a new cell after (true), and return the new cell to us
+ RefPtr<Element> newCellElement;
+ nsresult rv = InsertCell(previousCellElementInRow, 1, 1, true, false,
+ getter_AddRefs(newCellElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::InsertCell() failed");
+ return rv;
+ }
+
+ if (newCellElement) {
+ previousCellElementInRow = std::move(newCellElement);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetCellIndexes(Element* aCellElement,
+ int32_t* aRowIndex,
+ int32_t* aColumnIndex) {
+ if (NS_WARN_IF(!aRowIndex) || NS_WARN_IF(!aColumnIndex)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eGetCellIndexes);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::GetCellIndexes() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ *aRowIndex = 0;
+ *aColumnIndex = 0;
+
+ if (!aCellElement) {
+ // Use cell element which contains anchor of Selection when aCellElement is
+ // nullptr.
+ const CellIndexes cellIndexes(*this, SelectionRef());
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ *aRowIndex = cellIndexes.mRow;
+ *aColumnIndex = cellIndexes.mColumn;
+ return NS_OK;
+ }
+
+ const RefPtr<PresShell> presShell{GetPresShell()};
+ const CellIndexes cellIndexes(*aCellElement, presShell);
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ *aRowIndex = cellIndexes.mRow;
+ *aColumnIndex = cellIndexes.mColumn;
+ return NS_OK;
+}
+
+// static
+nsTableWrapperFrame* HTMLEditor::GetTableFrame(const Element* aTableElement) {
+ if (NS_WARN_IF(!aTableElement)) {
+ return nullptr;
+ }
+ return do_QueryFrame(aTableElement->GetPrimaryFrame());
+}
+
+// Return actual number of cells (a cell with colspan > 1 counts as just 1)
+int32_t HTMLEditor::GetNumberOfCellsInRow(Element& aTableElement,
+ int32_t aRowIndex) {
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, aTableElement);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return -1;
+ }
+
+ int32_t numberOfCells = 0;
+ for (int32_t columnIndex = 0;
+ columnIndex < tableSizeOrError.inspect().mColumnCount;) {
+ const auto cellData = CellData::AtIndexInTableElement(
+ *this, aTableElement, aRowIndex, columnIndex);
+ // Failure means that there is no more cell in the row. In this case,
+ // we shouldn't return error since we just reach the end of the row.
+ // XXX So, this method assumes that CellData won't return error when
+ // just not found. Fix this later.
+ if (cellData.FailedOrNotFound()) {
+ break;
+ }
+
+ // Only count cells that start in row we are working with
+ if (cellData.mElement && !cellData.IsSpannedFromOtherRow()) {
+ numberOfCells++;
+ }
+ MOZ_ASSERT(columnIndex < cellData.NextColumnIndex());
+ columnIndex = cellData.NextColumnIndex();
+ }
+ return numberOfCells;
+}
+
+NS_IMETHODIMP HTMLEditor::GetTableSize(Element* aTableOrElementInTable,
+ int32_t* aRowCount,
+ int32_t* aColumnCount) {
+ if (NS_WARN_IF(!aRowCount) || NS_WARN_IF(!aColumnCount)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eGetTableSize);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::GetTableSize() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ *aRowCount = 0;
+ *aColumnCount = 0;
+
+ Element* tableOrElementInTable = aTableOrElementInTable;
+ if (!tableOrElementInTable) {
+ tableOrElementInTable =
+ GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table);
+ if (!tableOrElementInTable) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::"
+ "table) failed");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *tableOrElementInTable);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr());
+ }
+ *aRowCount = tableSizeOrError.inspect().mRowCount;
+ *aColumnCount = tableSizeOrError.inspect().mColumnCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetCellDataAt(
+ Element* aTableElement, int32_t aRowIndex, int32_t aColumnIndex,
+ Element** aCellElement, int32_t* aStartRowIndex, int32_t* aStartColumnIndex,
+ int32_t* aRowSpan, int32_t* aColSpan, int32_t* aEffectiveRowSpan,
+ int32_t* aEffectiveColSpan, bool* aIsSelected) {
+ if (NS_WARN_IF(!aCellElement) || NS_WARN_IF(!aStartRowIndex) ||
+ NS_WARN_IF(!aStartColumnIndex) || NS_WARN_IF(!aRowSpan) ||
+ NS_WARN_IF(!aColSpan) || NS_WARN_IF(!aEffectiveRowSpan) ||
+ NS_WARN_IF(!aEffectiveColSpan) || NS_WARN_IF(!aIsSelected)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eGetCellDataAt);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::GetCellDataAt() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ *aStartRowIndex = 0;
+ *aStartColumnIndex = 0;
+ *aRowSpan = 0;
+ *aColSpan = 0;
+ *aEffectiveRowSpan = 0;
+ *aEffectiveColSpan = 0;
+ *aIsSelected = false;
+ *aCellElement = nullptr;
+
+ // Let's keep the table element with strong pointer since editor developers
+ // may not handle layout code of <table>, however, this method depends on
+ // them.
+ RefPtr<Element> table = aTableElement;
+ if (!table) {
+ // Get the selected table or the table enclosing the selection anchor.
+ table = GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table);
+ if (!table) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::"
+ "table) failed");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ const CellData cellData =
+ CellData::AtIndexInTableElement(*this, *table, aRowIndex, aColumnIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return NS_ERROR_FAILURE;
+ }
+ NS_ADDREF(*aCellElement = cellData.mElement.get());
+ *aIsSelected = cellData.mIsSelected;
+ *aStartRowIndex = cellData.mFirst.mRow;
+ *aStartColumnIndex = cellData.mFirst.mColumn;
+ *aRowSpan = cellData.mRowSpan;
+ *aColSpan = cellData.mColSpan;
+ *aEffectiveRowSpan = cellData.mEffectiveRowSpan;
+ *aEffectiveColSpan = cellData.mEffectiveColSpan;
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetCellAt(Element* aTableElement, int32_t aRowIndex,
+ int32_t aColumnIndex,
+ Element** aCellElement) {
+ if (NS_WARN_IF(!aCellElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eGetCellAt);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::GetCellAt() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ *aCellElement = nullptr;
+
+ Element* tableElement = aTableElement;
+ if (!tableElement) {
+ // Get the selected table or the table enclosing the selection anchor.
+ tableElement = GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table);
+ if (!tableElement) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::"
+ "table) failed");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ RefPtr<Element> cellElement =
+ GetTableCellElementAt(*tableElement, aRowIndex, aColumnIndex);
+ cellElement.forget(aCellElement);
+ return NS_OK;
+}
+
+Element* HTMLEditor::GetTableCellElementAt(Element& aTableElement,
+ int32_t aRowIndex,
+ int32_t aColumnIndex) const {
+ // Let's grab the <table> element while we're retrieving layout API since
+ // editor developers do not watch all layout API changes. So, it may
+ // become unsafe.
+ OwningNonNull<Element> tableElement(aTableElement);
+ nsTableWrapperFrame* tableFrame = HTMLEditor::GetTableFrame(tableElement);
+ if (!tableFrame) {
+ NS_WARNING("There was no table layout information");
+ return nullptr;
+ }
+ nsIContent* cell = tableFrame->GetCellAt(aRowIndex, aColumnIndex);
+ return Element::FromNodeOrNull(cell);
+}
+
+// When all you want are the rowspan and colspan (not exposed in nsITableEditor)
+nsresult HTMLEditor::GetCellSpansAt(Element* aTable, int32_t aRowIndex,
+ int32_t aColIndex, int32_t& aActualRowSpan,
+ int32_t& aActualColSpan) {
+ nsTableWrapperFrame* tableFrame = HTMLEditor::GetTableFrame(aTable);
+ if (!tableFrame) {
+ NS_WARNING("There was no table layout information");
+ return NS_ERROR_FAILURE;
+ }
+ aActualRowSpan = tableFrame->GetEffectiveRowSpanAt(aRowIndex, aColIndex);
+ aActualColSpan = tableFrame->GetEffectiveColSpanAt(aRowIndex, aColIndex);
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::GetCellContext(Element** aTable, Element** aCell,
+ nsINode** aCellParent, int32_t* aCellOffset,
+ int32_t* aRowIndex, int32_t* aColumnIndex) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Initialize return pointers
+ if (aTable) {
+ *aTable = nullptr;
+ }
+ if (aCell) {
+ *aCell = nullptr;
+ }
+ if (aCellParent) {
+ *aCellParent = nullptr;
+ }
+ if (aCellOffset) {
+ *aCellOffset = 0;
+ }
+ if (aRowIndex) {
+ *aRowIndex = 0;
+ }
+ if (aColumnIndex) {
+ *aColumnIndex = 0;
+ }
+
+ RefPtr<Element> table;
+ RefPtr<Element> cell;
+
+ // Caller may supply the cell...
+ if (aCell && *aCell) {
+ cell = *aCell;
+ }
+
+ // ...but if not supplied,
+ // get cell if it's the child of selection anchor node,
+ // or get the enclosing by a cell
+ if (!cell) {
+ // Find a selected or enclosing table element
+ Result<RefPtr<Element>, nsresult> cellOrRowOrTableElementOrError =
+ GetSelectedOrParentTableElement();
+ if (cellOrRowOrTableElementOrError.isErr()) {
+ NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed");
+ return cellOrRowOrTableElementOrError.unwrapErr();
+ }
+ if (!cellOrRowOrTableElementOrError.inspect()) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+ if (HTMLEditUtils::IsTable(cellOrRowOrTableElementOrError.inspect())) {
+ // We have a selected table, not a cell
+ if (aTable) {
+ cellOrRowOrTableElementOrError.unwrap().forget(aTable);
+ }
+ return NS_OK;
+ }
+ if (!HTMLEditUtils::IsTableCell(cellOrRowOrTableElementOrError.inspect())) {
+ return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+ }
+
+ // We found a cell
+ cell = cellOrRowOrTableElementOrError.unwrap();
+ }
+ if (aCell) {
+ // we don't want to cell.forget() here, because we use it below.
+ *aCell = do_AddRef(cell).take();
+ }
+
+ // Get containing table
+ table = GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::table, *cell);
+ if (!table) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) "
+ "failed");
+ // Cell must be in a table, so fail if not found
+ return NS_ERROR_FAILURE;
+ }
+ if (aTable) {
+ table.forget(aTable);
+ }
+
+ // Get the rest of the related data only if requested
+ if (aRowIndex || aColumnIndex) {
+ const RefPtr<PresShell> presShell{GetPresShell()};
+ const CellIndexes cellIndexes(*cell, presShell);
+ if (NS_WARN_IF(cellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ if (aRowIndex) {
+ *aRowIndex = cellIndexes.mRow;
+ }
+ if (aColumnIndex) {
+ *aColumnIndex = cellIndexes.mColumn;
+ }
+ }
+ if (aCellParent) {
+ // Get the immediate parent of the cell
+ EditorRawDOMPoint atCellElement(cell);
+ if (NS_WARN_IF(!atCellElement.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aCellOffset) {
+ *aCellOffset = atCellElement.Offset();
+ }
+
+ // Now it's safe to hand over the reference to cellParent, since
+ // we don't need it anymore.
+ *aCellParent = do_AddRef(atCellElement.GetContainer()).take();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetSelectedCells(
+ nsTArray<RefPtr<Element>>& aOutSelectedCellElements) {
+ MOZ_ASSERT(aOutSelectedCellElements.IsEmpty());
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eGetSelectedCells);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::GetSelectedCells() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ SelectedTableCellScanner scanner(SelectionRef());
+ if (!scanner.IsInTableCellSelectionMode()) {
+ return NS_OK;
+ }
+
+ aOutSelectedCellElements.SetCapacity(scanner.ElementsRef().Length());
+ for (const OwningNonNull<Element>& cellElement : scanner.ElementsRef()) {
+ aOutSelectedCellElements.AppendElement(cellElement);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP HTMLEditor::GetFirstSelectedCellInTable(int32_t* aRowIndex,
+ int32_t* aColumnIndex,
+ Element** aCellElement) {
+ if (NS_WARN_IF(!aRowIndex) || NS_WARN_IF(!aColumnIndex) ||
+ NS_WARN_IF(!aCellElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eGetFirstSelectedCellInTable);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING(
+ "HTMLEditor::GetFirstSelectedCellInTable() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ return NS_ERROR_FAILURE; // XXX Should return NS_OK?
+ }
+
+ *aRowIndex = 0;
+ *aColumnIndex = 0;
+ *aCellElement = nullptr;
+ RefPtr<Element> firstSelectedCellElement =
+ HTMLEditUtils::GetFirstSelectedTableCellElement(SelectionRef());
+ if (!firstSelectedCellElement) {
+ return NS_OK;
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ const CellIndexes indexes(*firstSelectedCellElement, presShell);
+ if (NS_WARN_IF(indexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ firstSelectedCellElement.forget(aCellElement);
+ *aRowIndex = indexes.mRow;
+ *aColumnIndex = indexes.mColumn;
+ return NS_OK;
+}
+
+void HTMLEditor::SetSelectionAfterTableEdit(Element* aTable, int32_t aRow,
+ int32_t aCol, int32_t aDirection,
+ bool aSelected) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aTable) || NS_WARN_IF(Destroyed())) {
+ return;
+ }
+
+ RefPtr<Element> cell;
+ bool done = false;
+ do {
+ cell = GetTableCellElementAt(*aTable, aRow, aCol);
+ if (cell) {
+ if (aSelected) {
+ // Reselect the cell
+ DebugOnly<nsresult> rv = SelectContentInternal(*cell);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::SelectContentInternal() failed, but ignored");
+ return;
+ }
+
+ // Set the caret to deepest first child
+ // but don't go into nested tables
+ // TODO: Should we really be placing the caret at the END
+ // of the cell content?
+ CollapseSelectionToDeepestNonTableFirstChild(cell);
+ return;
+ }
+
+ // Setup index to find another cell in the
+ // direction requested, but move in other direction if already at
+ // beginning of row or column
+ switch (aDirection) {
+ case ePreviousColumn:
+ if (!aCol) {
+ if (aRow > 0) {
+ aRow--;
+ } else {
+ done = true;
+ }
+ } else {
+ aCol--;
+ }
+ break;
+ case ePreviousRow:
+ if (!aRow) {
+ if (aCol > 0) {
+ aCol--;
+ } else {
+ done = true;
+ }
+ } else {
+ aRow--;
+ }
+ break;
+ default:
+ done = true;
+ }
+ } while (!done);
+
+ // We didn't find a cell
+ // Set selection to just before the table
+ if (aTable->GetParentNode()) {
+ EditorRawDOMPoint atTable(aTable);
+ if (NS_WARN_IF(!atTable.IsSetAndValid())) {
+ return;
+ }
+ DebugOnly<nsresult> rvIgnored = CollapseSelectionTo(atTable);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return;
+ }
+ // Last resort: Set selection to start of doc
+ // (it's very bad to not have a valid selection!)
+ DebugOnly<nsresult> rvIgnored = SetSelectionAtDocumentStart();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::SetSelectionAtDocumentStart() failed, but ignored");
+}
+
+NS_IMETHODIMP HTMLEditor::GetSelectedOrParentTableElement(
+ nsAString& aTagName, int32_t* aSelectedCount,
+ Element** aCellOrRowOrTableElement) {
+ if (NS_WARN_IF(!aSelectedCount) || NS_WARN_IF(!aCellOrRowOrTableElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aTagName.Truncate();
+ *aCellOrRowOrTableElement = nullptr;
+ *aSelectedCount = 0;
+
+ AutoEditActionDataSetter editActionData(
+ *this, EditAction::eGetSelectedOrParentTableElement);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING(
+ "HTMLEditor::GetSelectedOrParentTableElement() couldn't handle the "
+ "job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ bool isCellSelected = false;
+ Result<RefPtr<Element>, nsresult> cellOrRowOrTableElementOrError =
+ GetSelectedOrParentTableElement(&isCellSelected);
+ if (cellOrRowOrTableElementOrError.isErr()) {
+ NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed");
+ return EditorBase::ToGenericNSResult(
+ cellOrRowOrTableElementOrError.unwrapErr());
+ }
+ if (!cellOrRowOrTableElementOrError.inspect()) {
+ return NS_OK;
+ }
+ RefPtr<Element> cellOrRowOrTableElement =
+ cellOrRowOrTableElementOrError.unwrap();
+
+ if (isCellSelected) {
+ aTagName.AssignLiteral("td");
+ *aSelectedCount = SelectionRef().RangeCount();
+ cellOrRowOrTableElement.forget(aCellOrRowOrTableElement);
+ return NS_OK;
+ }
+
+ if (HTMLEditUtils::IsTableCell(cellOrRowOrTableElement)) {
+ aTagName.AssignLiteral("td");
+ // Keep *aSelectedCount as 0.
+ cellOrRowOrTableElement.forget(aCellOrRowOrTableElement);
+ return NS_OK;
+ }
+
+ if (HTMLEditUtils::IsTable(cellOrRowOrTableElement)) {
+ aTagName.AssignLiteral("table");
+ *aSelectedCount = 1;
+ cellOrRowOrTableElement.forget(aCellOrRowOrTableElement);
+ return NS_OK;
+ }
+
+ if (HTMLEditUtils::IsTableRow(cellOrRowOrTableElement)) {
+ aTagName.AssignLiteral("tr");
+ *aSelectedCount = 1;
+ cellOrRowOrTableElement.forget(aCellOrRowOrTableElement);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Which element was returned?");
+ return NS_ERROR_UNEXPECTED;
+}
+
+Result<RefPtr<Element>, nsresult> HTMLEditor::GetSelectedOrParentTableElement(
+ bool* aIsCellSelected /* = nullptr */) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (aIsCellSelected) {
+ *aIsCellSelected = false;
+ }
+
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ return Err(NS_ERROR_FAILURE); // XXX Shouldn't throw an exception?
+ }
+
+ // Try to get the first selected cell, first.
+ RefPtr<Element> cellElement =
+ HTMLEditUtils::GetFirstSelectedTableCellElement(SelectionRef());
+ if (cellElement) {
+ if (aIsCellSelected) {
+ *aIsCellSelected = true;
+ }
+ return cellElement;
+ }
+
+ const RangeBoundary& anchorRef = SelectionRef().AnchorRef();
+ if (NS_WARN_IF(!anchorRef.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // If anchor selects a <td>, <table> or <tr>, return it.
+ if (anchorRef.Container()->HasChildNodes()) {
+ nsIContent* selectedContent = anchorRef.GetChildAtOffset();
+ if (selectedContent) {
+ // XXX Why do we ignore <th> element in this case?
+ if (selectedContent->IsHTMLElement(nsGkAtoms::td)) {
+ // FYI: If first range selects a <tr> element, but the other selects
+ // a <td> element, you can reach here.
+ // Each cell is in its own selection range in this case.
+ // XXX Although, other ranges may not select cells, though.
+ if (aIsCellSelected) {
+ *aIsCellSelected = true;
+ }
+ return RefPtr<Element>(selectedContent->AsElement());
+ }
+ if (selectedContent->IsAnyOfHTMLElements(nsGkAtoms::table,
+ nsGkAtoms::tr)) {
+ return RefPtr<Element>(selectedContent->AsElement());
+ }
+ }
+ }
+
+ if (NS_WARN_IF(!anchorRef.Container()->IsContent())) {
+ return RefPtr<Element>();
+ }
+
+ // Then, look for a cell element (either <td> or <th>) which contains
+ // the anchor container.
+ cellElement = GetInclusiveAncestorByTagNameInternal(
+ *nsGkAtoms::td, *anchorRef.Container()->AsContent());
+ if (!cellElement) {
+ return RefPtr<Element>(); // Not in table.
+ }
+ // Don't set *aIsCellSelected to true in this case because it does NOT
+ // select a cell, just in a cell.
+ return cellElement;
+}
+
+Result<RefPtr<Element>, nsresult>
+HTMLEditor::GetFirstSelectedCellElementInTable() const {
+ Result<RefPtr<Element>, nsresult> cellOrRowOrTableElementOrError =
+ GetSelectedOrParentTableElement();
+ if (cellOrRowOrTableElementOrError.isErr()) {
+ NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed");
+ return cellOrRowOrTableElementOrError;
+ }
+
+ if (!cellOrRowOrTableElementOrError.inspect()) {
+ return cellOrRowOrTableElementOrError;
+ }
+
+ const RefPtr<Element>& element = cellOrRowOrTableElementOrError.inspect();
+ if (!HTMLEditUtils::IsTableCell(element)) {
+ return RefPtr<Element>();
+ }
+
+ if (!HTMLEditUtils::IsTableRow(element->GetParentNode())) {
+ NS_WARNING("There was no parent <tr> element for the found cell");
+ return RefPtr<Element>();
+ }
+
+ if (!HTMLEditUtils::GetClosestAncestorTableElement(*element)) {
+ NS_WARNING("There was no ancestor <table> element for the found cell");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return cellOrRowOrTableElementOrError;
+}
+
+NS_IMETHODIMP HTMLEditor::GetSelectedCellsType(Element* aElement,
+ uint32_t* aSelectionType) {
+ if (NS_WARN_IF(!aSelectionType)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aSelectionType = 0;
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eGetSelectedCellsType);
+ nsresult rv = editActionData.CanHandleAndFlushPendingNotifications();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("HTMLEditor::GetSelectedCellsType() couldn't handle the job");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ return NS_ERROR_FAILURE; // XXX Should we just return NS_OK?
+ }
+
+ // Be sure we have a table element
+ // (if aElement is null, this uses selection's anchor node)
+ RefPtr<Element> table;
+ if (aElement) {
+ table = GetInclusiveAncestorByTagNameInternal(*nsGkAtoms::table, *aElement);
+ if (!table) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameInternal(nsGkAtoms::table) "
+ "failed");
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ table = GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::table);
+ if (!table) {
+ NS_WARNING(
+ "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(nsGkAtoms::"
+ "table) failed");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ const Result<TableSize, nsresult> tableSizeOrError =
+ TableSize::Create(*this, *table);
+ if (NS_WARN_IF(tableSizeOrError.isErr())) {
+ return EditorBase::ToGenericNSResult(tableSizeOrError.inspectErr());
+ }
+ const TableSize& tableSize = tableSizeOrError.inspect();
+
+ // Traverse all selected cells
+ SelectedTableCellScanner scanner(SelectionRef());
+ if (!scanner.IsInTableCellSelectionMode()) {
+ return NS_OK;
+ }
+
+ // We have at least one selected cell, so set return value
+ *aSelectionType = static_cast<uint32_t>(TableSelectionMode::Cell);
+
+ // Store indexes of each row/col to avoid duplication of searches
+ nsTArray<int32_t> indexArray;
+
+ const RefPtr<PresShell> presShell{GetPresShell()};
+ bool allCellsInRowAreSelected = false;
+ for (const OwningNonNull<Element>& selectedCellElement :
+ scanner.ElementsRef()) {
+ // `MOZ_KnownLive(selectedCellElement)` is safe because `scanner` grabs
+ // it until it's destroyed later.
+ const CellIndexes selectedCellIndexes(MOZ_KnownLive(selectedCellElement),
+ presShell);
+ if (NS_WARN_IF(selectedCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!indexArray.Contains(selectedCellIndexes.mColumn)) {
+ indexArray.AppendElement(selectedCellIndexes.mColumn);
+ allCellsInRowAreSelected = AllCellsInRowSelected(
+ table, selectedCellIndexes.mRow, tableSize.mColumnCount);
+ // We're done as soon as we fail for any row
+ if (!allCellsInRowAreSelected) {
+ break;
+ }
+ }
+ }
+
+ if (allCellsInRowAreSelected) {
+ *aSelectionType = static_cast<uint32_t>(TableSelectionMode::Row);
+ return NS_OK;
+ }
+ // Test for columns
+
+ // Empty the indexArray
+ indexArray.Clear();
+
+ // Start at first cell again
+ bool allCellsInColAreSelected = false;
+ for (const OwningNonNull<Element>& selectedCellElement :
+ scanner.ElementsRef()) {
+ // `MOZ_KnownLive(selectedCellElement)` is safe because `scanner` grabs
+ // it until it's destroyed later.
+ const CellIndexes selectedCellIndexes(MOZ_KnownLive(selectedCellElement),
+ presShell);
+ if (NS_WARN_IF(selectedCellIndexes.isErr())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!indexArray.Contains(selectedCellIndexes.mRow)) {
+ indexArray.AppendElement(selectedCellIndexes.mColumn);
+ allCellsInColAreSelected = AllCellsInColumnSelected(
+ table, selectedCellIndexes.mColumn, tableSize.mRowCount);
+ // We're done as soon as we fail for any column
+ if (!allCellsInRowAreSelected) {
+ break;
+ }
+ }
+ }
+ if (allCellsInColAreSelected) {
+ *aSelectionType = static_cast<uint32_t>(TableSelectionMode::Column);
+ }
+
+ return NS_OK;
+}
+
+bool HTMLEditor::AllCellsInRowSelected(Element* aTable, int32_t aRowIndex,
+ int32_t aNumberOfColumns) {
+ if (NS_WARN_IF(!aTable)) {
+ return false;
+ }
+
+ for (int32_t col = 0; col < aNumberOfColumns;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *aTable, aRowIndex, col);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return false;
+ }
+
+ // If no cell, we may have a "ragged" right edge, so return TRUE only if
+ // we already found a cell in the row.
+ // XXX So, this does not assume that CellData returns error when just
+ // not found a cell. Fix this later.
+ if (!cellData.mElement) {
+ NS_WARNING("CellData didn't set mElement");
+ return cellData.mCurrent.mColumn > 0;
+ }
+
+ // Return as soon as a non-selected cell is found.
+ // XXX Odd, this is testing if each cell element is selected. Why do
+ // we need to warn if it's false??
+ if (!cellData.mIsSelected) {
+ NS_WARNING("CellData didn't set mIsSelected");
+ return false;
+ }
+
+ MOZ_ASSERT(col < cellData.NextColumnIndex());
+ col = cellData.NextColumnIndex();
+ }
+ return true;
+}
+
+bool HTMLEditor::AllCellsInColumnSelected(Element* aTable, int32_t aColIndex,
+ int32_t aNumberOfRows) {
+ if (NS_WARN_IF(!aTable)) {
+ return false;
+ }
+
+ for (int32_t row = 0; row < aNumberOfRows;) {
+ const auto cellData =
+ CellData::AtIndexInTableElement(*this, *aTable, row, aColIndex);
+ if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+ return false;
+ }
+
+ // If no cell, we must have a "ragged" right edge on the last column so
+ // return TRUE only if we already found a cell in the row.
+ // XXX So, this does not assume that CellData returns error when just
+ // not found a cell. Fix this later.
+ if (!cellData.mElement) {
+ NS_WARNING("CellData didn't set mElement");
+ return cellData.mCurrent.mRow > 0;
+ }
+
+ // Return as soon as a non-selected cell is found.
+ // XXX Odd, this is testing if each cell element is selected. Why do
+ // we need to warn if it's false??
+ if (!cellData.mIsSelected) {
+ NS_WARNING("CellData didn't set mIsSelected");
+ return false;
+ }
+
+ MOZ_ASSERT(row < cellData.NextRowIndex());
+ row = cellData.NextRowIndex();
+ }
+ return true;
+}
+
+bool HTMLEditor::IsEmptyCell(dom::Element* aCell) {
+ MOZ_ASSERT(aCell);
+
+ // Check if target only contains empty text node or <br>
+ nsCOMPtr<nsINode> cellChild = aCell->GetFirstChild();
+ if (!cellChild) {
+ return false;
+ }
+
+ nsCOMPtr<nsINode> nextChild = cellChild->GetNextSibling();
+ if (nextChild) {
+ return false;
+ }
+
+ // We insert a single break into a cell by default
+ // to have some place to locate a cursor -- it is dispensable
+ if (cellChild->IsHTMLElement(nsGkAtoms::br)) {
+ return true;
+ }
+
+ // Or check if no real content
+ return HTMLEditUtils::IsEmptyNode(
+ *cellChild, {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible});
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/InsertNodeTransaction.cpp b/editor/libeditor/InsertNodeTransaction.cpp
new file mode 100644
index 0000000000..50e6b32653
--- /dev/null
+++ b/editor/libeditor/InsertNodeTransaction.cpp
@@ -0,0 +1,184 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InsertNodeTransaction.h"
+
+#include "EditorBase.h" // for EditorBase
+#include "EditorDOMPoint.h" // for EditorDOMPoint
+#include "HTMLEditor.h" // for HTMLEditor
+#include "TextEditor.h" // for TextEditor
+
+#include "mozilla/Logging.h"
+#include "mozilla/ToString.h"
+
+#include "nsAString.h"
+#include "nsDebug.h" // for NS_WARNING, etc.
+#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc.
+#include "nsIContent.h" // for nsIContent
+#include "nsReadableUtils.h" // for ToNewCString
+#include "nsString.h" // for nsString
+
+namespace mozilla {
+
+using namespace dom;
+
+template already_AddRefed<InsertNodeTransaction> InsertNodeTransaction::Create(
+ EditorBase& aEditorBase, nsIContent& aContentToInsert,
+ const EditorDOMPoint& aPointToInsert);
+template already_AddRefed<InsertNodeTransaction> InsertNodeTransaction::Create(
+ EditorBase& aEditorBase, nsIContent& aContentToInsert,
+ const EditorRawDOMPoint& aPointToInsert);
+
+// static
+template <typename PT, typename CT>
+already_AddRefed<InsertNodeTransaction> InsertNodeTransaction::Create(
+ EditorBase& aEditorBase, nsIContent& aContentToInsert,
+ const EditorDOMPointBase<PT, CT>& aPointToInsert) {
+ RefPtr<InsertNodeTransaction> transaction =
+ new InsertNodeTransaction(aEditorBase, aContentToInsert, aPointToInsert);
+ return transaction.forget();
+}
+
+template <typename PT, typename CT>
+InsertNodeTransaction::InsertNodeTransaction(
+ EditorBase& aEditorBase, nsIContent& aContentToInsert,
+ const EditorDOMPointBase<PT, CT>& aPointToInsert)
+ : mContentToInsert(&aContentToInsert),
+ mPointToInsert(aPointToInsert.template To<EditorDOMPoint>()),
+ mEditorBase(&aEditorBase) {
+ MOZ_ASSERT(mPointToInsert.IsSetAndValid());
+ // Ensure mPointToInsert stores child at offset.
+ Unused << mPointToInsert.GetChild();
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const InsertNodeTransaction& aTransaction) {
+ aStream << "{ mContentToInsert=" << aTransaction.mContentToInsert.get();
+ if (aTransaction.mContentToInsert) {
+ if (aTransaction.mContentToInsert->IsText()) {
+ nsAutoString data;
+ aTransaction.mContentToInsert->AsText()->GetData(data);
+ aStream << " (#text \"" << NS_ConvertUTF16toUTF8(data).get() << "\")";
+ } else {
+ aStream << " (" << *aTransaction.mContentToInsert << ")";
+ }
+ }
+ aStream << ", mPointToInsert=" << aTransaction.mPointToInsert
+ << ", mEditorBase=" << aTransaction.mEditorBase.get() << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(InsertNodeTransaction, EditTransactionBase,
+ mEditorBase, mContentToInsert,
+ mPointToInsert)
+
+NS_IMPL_ADDREF_INHERITED(InsertNodeTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(InsertNodeTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(InsertNodeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP InsertNodeTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p InsertNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mContentToInsert) ||
+ NS_WARN_IF(!mPointToInsert.IsSet())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ MOZ_ASSERT_IF(mEditorBase->IsTextEditor(), !mContentToInsert->IsText());
+
+ if (!mPointToInsert.IsSetAndValid()) {
+ // It seems that DOM tree has been changed after first DoTransaction()
+ // and current RedoTranaction() call.
+ if (mPointToInsert.GetChild()) {
+ EditorDOMPoint newPointToInsert(mPointToInsert.GetChild());
+ if (!newPointToInsert.IsSet()) {
+ // The insertion point has been removed from the DOM tree.
+ // In this case, we should append the node to the container instead.
+ newPointToInsert.SetToEndOf(mPointToInsert.GetContainer());
+ if (NS_WARN_IF(!newPointToInsert.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ mPointToInsert = newPointToInsert;
+ } else {
+ mPointToInsert.SetToEndOf(mPointToInsert.GetContainer());
+ if (NS_WARN_IF(!mPointToInsert.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<nsIContent> contentToInsert = *mContentToInsert;
+ OwningNonNull<nsINode> container = *mPointToInsert.GetContainer();
+ nsCOMPtr<nsIContent> refChild = mPointToInsert.GetChild();
+ if (contentToInsert->IsElement()) {
+ nsresult rv = editorBase->MarkElementDirty(
+ MOZ_KnownLive(*contentToInsert->AsElement()));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::MarkElementDirty() failed, but ignored");
+ }
+
+ IgnoredErrorResult error;
+ container->InsertBefore(contentToInsert, refChild, error);
+ // InsertBefore() may call MightThrowJSException() even if there is no
+ // error. We don't need the flag here.
+ error.WouldReportJSException();
+ if (error.Failed()) {
+ NS_WARNING("nsINode::InsertBefore() failed");
+ return error.StealNSResult();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP InsertNodeTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p InsertNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mContentToInsert) ||
+ NS_WARN_IF(!mPointToInsert.IsSet())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ // XXX If the inserted node has been moved to different container node or
+ // just removed from the DOM tree, this always fails.
+ OwningNonNull<nsINode> container = *mPointToInsert.GetContainer();
+ OwningNonNull<nsIContent> contentToInsert = *mContentToInsert;
+ ErrorResult error;
+ container->RemoveChild(contentToInsert, error);
+ NS_WARNING_ASSERTION(!error.Failed(), "nsINode::RemoveChild() failed");
+ return error.StealNSResult();
+}
+
+NS_IMETHODIMP InsertNodeTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p InsertNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+ nsresult rv = DoTransaction();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING("InsertNodeTransaction::RedoTransaction() failed");
+ return rv;
+ }
+
+ if (!mEditorBase->AllowsTransactionsToChangeSelection()) {
+ return NS_OK;
+ }
+
+ OwningNonNull<EditorBase> editorBase(*mEditorBase);
+ rv = editorBase->CollapseSelectionTo(
+ SuggestPointToPutCaret<EditorRawDOMPoint>());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/InsertNodeTransaction.h b/editor/libeditor/InsertNodeTransaction.h
new file mode 100644
index 0000000000..671b3a7c1b
--- /dev/null
+++ b/editor/libeditor/InsertNodeTransaction.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef InsertNodeTransaction_h
+#define InsertNodeTransaction_h
+
+#include "EditTransactionBase.h" // for EditTransactionBase, etc.
+
+#include "EditorDOMPoint.h" // for EditorDOMPoint
+#include "EditorForwards.h"
+
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsIContent.h" // for nsIContent
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS_INHERITED
+
+namespace mozilla {
+
+class EditorBase;
+
+/**
+ * A transaction that inserts a single element
+ */
+class InsertNodeTransaction final : public EditTransactionBase {
+ protected:
+ template <typename PT, typename CT>
+ InsertNodeTransaction(EditorBase& aEditorBase, nsIContent& aContentToInsert,
+ const EditorDOMPointBase<PT, CT>& aPointToInsert);
+
+ public:
+ /**
+ * Create a transaction for inserting aContentToInsert before the child
+ * at aPointToInsert.
+ *
+ * @param aEditorBase The editor which manages the transaction.
+ * @param aContentToInsert The node to be inserted.
+ * @param aPointToInsert The insertion point of aContentToInsert.
+ * If this refers end of the container, the
+ * transaction will append the node to the
+ * container. Otherwise, will insert the node
+ * before child node referred by this.
+ * @return A InsertNodeTranaction which was initialized
+ * with the arguments.
+ */
+ template <typename PT, typename CT>
+ static already_AddRefed<InsertNodeTransaction> Create(
+ EditorBase& aEditorBase, nsIContent& aContentToInsert,
+ const EditorDOMPointBase<PT, CT>& aPointToInsert);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(InsertNodeTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(InsertNodeTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+
+ /**
+ * SuggestPointToPutCaret() suggests a point after doing or redoing the
+ * transaction.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType SuggestPointToPutCaret() const {
+ if (MOZ_UNLIKELY(!mPointToInsert.IsSet() || !mContentToInsert)) {
+ return EditorDOMPointType();
+ }
+ return EditorDOMPointType::After(mContentToInsert);
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const InsertNodeTransaction& aTransaction);
+
+ protected:
+ virtual ~InsertNodeTransaction() = default;
+
+ // The element to insert.
+ nsCOMPtr<nsIContent> mContentToInsert;
+
+ // The DOM point we will insert mContentToInsert.
+ EditorDOMPoint mPointToInsert;
+
+ // The editor for this transaction.
+ RefPtr<EditorBase> mEditorBase;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef InsertNodeTransaction_h
diff --git a/editor/libeditor/InsertTextTransaction.cpp b/editor/libeditor/InsertTextTransaction.cpp
new file mode 100644
index 0000000000..3a308dd487
--- /dev/null
+++ b/editor/libeditor/InsertTextTransaction.cpp
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InsertTextTransaction.h"
+
+#include "ErrorList.h"
+#include "mozilla/EditorBase.h" // mEditorBase
+#include "mozilla/Logging.h"
+#include "mozilla/SelectionState.h" // RangeUpdater
+#include "mozilla/ToString.h"
+#include "mozilla/dom/Selection.h" // Selection local var
+#include "mozilla/dom/Text.h" // mTextNode
+
+#include "nsAString.h" // nsAString parameter
+#include "nsDebug.h" // for NS_ASSERTION, etc.
+#include "nsError.h" // for NS_OK, etc.
+#include "nsQueryObject.h" // for do_QueryObject
+
+namespace mozilla {
+
+using namespace dom;
+
+// static
+already_AddRefed<InsertTextTransaction> InsertTextTransaction::Create(
+ EditorBase& aEditorBase, const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert) {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+ RefPtr<InsertTextTransaction> transaction =
+ new InsertTextTransaction(aEditorBase, aStringToInsert, aPointToInsert);
+ return transaction.forget();
+}
+
+InsertTextTransaction::InsertTextTransaction(
+ EditorBase& aEditorBase, const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert)
+ : mTextNode(aPointToInsert.ContainerAs<Text>()),
+ mOffset(aPointToInsert.Offset()),
+ mStringToInsert(aStringToInsert),
+ mEditorBase(&aEditorBase) {}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const InsertTextTransaction& aTransaction) {
+ aStream << "{ mTextNode=" << aTransaction.mTextNode.get();
+ if (aTransaction.mTextNode) {
+ aStream << " (" << *aTransaction.mTextNode << ")";
+ }
+ aStream << ", mOffset=" << aTransaction.mOffset << ", mStringToInsert=\""
+ << NS_ConvertUTF16toUTF8(aTransaction.mStringToInsert).get() << "\""
+ << ", mEditorBase=" << aTransaction.mEditorBase.get() << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(InsertTextTransaction, EditTransactionBase,
+ mEditorBase, mTextNode)
+
+NS_IMPL_ADDREF_INHERITED(InsertTextTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(InsertTextTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(InsertTextTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP InsertTextTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p InsertTextTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<Text> textNode = *mTextNode;
+
+ ErrorResult error;
+ editorBase->DoInsertText(textNode, mOffset, mStringToInsert, error);
+ if (error.Failed()) {
+ NS_WARNING("EditorBase::DoInsertText() failed");
+ return error.StealNSResult();
+ }
+
+ editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
+ mStringToInsert.Length());
+ return NS_OK;
+}
+
+NS_IMETHODIMP InsertTextTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p InsertTextTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<Text> textNode = *mTextNode;
+ ErrorResult error;
+ editorBase->DoDeleteText(textNode, mOffset, mStringToInsert.Length(), error);
+ NS_WARNING_ASSERTION(!error.Failed(), "EditorBase::DoDeleteText() failed");
+ return error.StealNSResult();
+}
+
+NS_IMETHODIMP InsertTextTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p InsertTextTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+ nsresult rv = DoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("InsertTextTransaction::DoTransaction() failed");
+ return rv;
+ }
+ if (RefPtr<EditorBase> editorBase = mEditorBase) {
+ nsresult rv = editorBase->CollapseSelectionTo(
+ SuggestPointToPutCaret<EditorRawDOMPoint>());
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP InsertTextTransaction::Merge(nsITransaction* aOtherTransaction,
+ bool* aDidMerge) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p InsertTextTransaction::%s(aOtherTransaction=%p) this=%s", this,
+ __FUNCTION__, aOtherTransaction, ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!aOtherTransaction) || NS_WARN_IF(!aDidMerge)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ // Set out param default value
+ *aDidMerge = false;
+
+ RefPtr<EditTransactionBase> otherTransactionBase =
+ aOtherTransaction->GetAsEditTransactionBase();
+ if (!otherTransactionBase) {
+ MOZ_LOG(
+ GetLogModule(), LogLevel::Debug,
+ ("%p InsertTextTransaction::%s(aOtherTransaction=%p) returned false",
+ this, __FUNCTION__, aOtherTransaction));
+ return NS_OK;
+ }
+
+ // If aTransaction is a InsertTextTransaction, and if the selection hasn't
+ // changed, then absorb it.
+ InsertTextTransaction* otherInsertTextTransaction =
+ otherTransactionBase->GetAsInsertTextTransaction();
+ if (!otherInsertTextTransaction ||
+ !IsSequentialInsert(*otherInsertTextTransaction)) {
+ MOZ_LOG(
+ GetLogModule(), LogLevel::Debug,
+ ("%p InsertTextTransaction::%s(aOtherTransaction=%p) returned false",
+ this, __FUNCTION__, aOtherTransaction));
+ return NS_OK;
+ }
+
+ mStringToInsert += otherInsertTextTransaction->GetData();
+ *aDidMerge = true;
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p InsertTextTransaction::%s(aOtherTransaction=%p) returned true",
+ this, __FUNCTION__, aOtherTransaction));
+ return NS_OK;
+}
+
+/* ============ private methods ================== */
+
+bool InsertTextTransaction::IsSequentialInsert(
+ InsertTextTransaction& aOtherTransaction) const {
+ return aOtherTransaction.mTextNode == mTextNode &&
+ aOtherTransaction.mOffset == mOffset + mStringToInsert.Length();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/InsertTextTransaction.h b/editor/libeditor/InsertTextTransaction.h
new file mode 100644
index 0000000000..8411d87abc
--- /dev/null
+++ b/editor/libeditor/InsertTextTransaction.h
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef InsertTextTransaction_h
+#define InsertTextTransaction_h
+
+#include "EditTransactionBase.h" // base class
+
+#include "EditorDOMPoint.h"
+#include "EditorForwards.h"
+
+#include "nsCycleCollectionParticipant.h" // various macros
+#include "nsID.h" // NS_DECLARE_STATIC_IID_ACCESSOR
+#include "nsISupportsImpl.h" // NS_DECL_ISUPPORTS_INHERITED
+#include "nsString.h" // nsString members
+#include "nscore.h" // NS_IMETHOD, nsAString
+
+namespace mozilla {
+namespace dom {
+class Text;
+} // namespace dom
+
+/**
+ * A transaction that inserts text into a content node.
+ */
+class InsertTextTransaction final : public EditTransactionBase {
+ protected:
+ InsertTextTransaction(EditorBase& aEditorBase,
+ const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert);
+
+ public:
+ /**
+ * Creates new InsertTextTransaction instance. This never returns nullptr.
+ *
+ * @param aEditorBase The editor which manages the transaction.
+ * @param aPointToInsert The insertion point.
+ * @param aStringToInsert The new string to insert.
+ */
+ static already_AddRefed<InsertTextTransaction> Create(
+ EditorBase& aEditorBase, const nsAString& aStringToInsert,
+ const EditorDOMPointInText& aPointToInsert);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(InsertTextTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(InsertTextTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+ NS_IMETHOD Merge(nsITransaction* aOtherTransaction, bool* aDidMerge) override;
+
+ /**
+ * Return the string data associated with this transaction.
+ */
+ const nsString& GetData() const { return mStringToInsert; }
+
+ template <typename EditorDOMPointType>
+ EditorDOMPointType SuggestPointToPutCaret() const {
+ if (NS_WARN_IF(!mTextNode)) {
+ return EditorDOMPointType();
+ }
+ return EditorDOMPointType(mTextNode, mOffset + mStringToInsert.Length());
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const InsertTextTransaction& aTransaction);
+
+ private:
+ virtual ~InsertTextTransaction() = default;
+
+ // Return true if aOtherTransaction immediately follows this transaction.
+ bool IsSequentialInsert(InsertTextTransaction& aOtherTransaction) const;
+
+ // The Text node to operate upon.
+ RefPtr<dom::Text> mTextNode;
+
+ // The offset into mTextNode where the insertion is to take place.
+ uint32_t mOffset;
+
+ // The text to insert into mTextNode at mOffset.
+ nsString mStringToInsert;
+
+ // The editor, which we'll need to get the selection.
+ RefPtr<EditorBase> mEditorBase;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef InsertTextTransaction_h
diff --git a/editor/libeditor/InternetCiter.cpp b/editor/libeditor/InternetCiter.cpp
new file mode 100644
index 0000000000..330bff4aa0
--- /dev/null
+++ b/editor/libeditor/InternetCiter.cpp
@@ -0,0 +1,293 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InternetCiter.h"
+
+#include "mozilla/Casting.h"
+#include "mozilla/intl/Segmenter.h"
+#include "HTMLEditUtils.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsCRT.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsStringIterator.h"
+
+namespace mozilla {
+
+/**
+ * Mail citations using the Internet style: > This is a citation.
+ */
+
+void InternetCiter::GetCiteString(const nsAString& aInString,
+ nsAString& aOutString) {
+ aOutString.Truncate();
+ char16_t uch = HTMLEditUtils::kNewLine;
+
+ // Strip trailing new lines which will otherwise turn up
+ // as ugly quoted empty lines.
+ nsReadingIterator<char16_t> beginIter, endIter;
+ aInString.BeginReading(beginIter);
+ aInString.EndReading(endIter);
+ while (beginIter != endIter && (*endIter == HTMLEditUtils::kCarriageReturn ||
+ *endIter == HTMLEditUtils::kNewLine)) {
+ --endIter;
+ }
+
+ // Loop over the string:
+ while (beginIter != endIter) {
+ if (uch == HTMLEditUtils::kNewLine) {
+ aOutString.Append(HTMLEditUtils::kGreaterThan);
+ // No space between >: this is ">>> " style quoting, for
+ // compatibility with RFC 2646 and format=flowed.
+ if (*beginIter != HTMLEditUtils::kGreaterThan) {
+ aOutString.Append(HTMLEditUtils::kSpace);
+ }
+ }
+
+ uch = *beginIter;
+ ++beginIter;
+
+ aOutString += uch;
+ }
+
+ if (uch != HTMLEditUtils::kNewLine) {
+ aOutString += HTMLEditUtils::kNewLine;
+ }
+}
+
+static void AddCite(nsAString& aOutString, int32_t citeLevel) {
+ for (int32_t i = 0; i < citeLevel; ++i) {
+ aOutString.Append(HTMLEditUtils::kGreaterThan);
+ }
+ if (citeLevel > 0) {
+ aOutString.Append(HTMLEditUtils::kSpace);
+ }
+}
+
+static inline void BreakLine(nsAString& aOutString, uint32_t& outStringCol,
+ uint32_t citeLevel) {
+ aOutString.Append(HTMLEditUtils::kNewLine);
+ if (citeLevel > 0) {
+ AddCite(aOutString, citeLevel);
+ outStringCol = citeLevel + 1;
+ } else {
+ outStringCol = 0;
+ }
+}
+
+static inline bool IsSpace(char16_t c) {
+ return (nsCRT::IsAsciiSpace(c) || (c == HTMLEditUtils::kNewLine) ||
+ (c == HTMLEditUtils::kCarriageReturn) || (c == HTMLEditUtils::kNBSP));
+}
+
+void InternetCiter::Rewrap(const nsAString& aInString, uint32_t aWrapCol,
+ uint32_t aFirstLineOffset, bool aRespectNewlines,
+ nsAString& aOutString) {
+ // There shouldn't be returns in this string, only dom newlines.
+ // Check to make sure:
+#ifdef DEBUG
+ int32_t crPosition = aInString.FindChar(HTMLEditUtils::kCarriageReturn);
+ NS_ASSERTION(crPosition < 0, "Rewrap: CR in string gotten from DOM!\n");
+#endif /* DEBUG */
+
+ aOutString.Truncate();
+
+ // Loop over lines in the input string, rewrapping each one.
+ uint32_t posInString = 0;
+ uint32_t outStringCol = 0;
+ uint32_t citeLevel = 0;
+ const nsPromiseFlatString& tString = PromiseFlatString(aInString);
+ const uint32_t length = tString.Length();
+ while (posInString < length) {
+ // Get the new cite level here since we're at the beginning of a line
+ uint32_t newCiteLevel = 0;
+ while (posInString < length &&
+ tString[posInString] == HTMLEditUtils::kGreaterThan) {
+ ++newCiteLevel;
+ ++posInString;
+ while (posInString < length &&
+ tString[posInString] == HTMLEditUtils::kSpace) {
+ ++posInString;
+ }
+ }
+ if (posInString >= length) {
+ break;
+ }
+
+ // Special case: if this is a blank line, maintain a blank line
+ // (retain the original paragraph breaks)
+ if (tString[posInString] == HTMLEditUtils::kNewLine &&
+ !aOutString.IsEmpty()) {
+ if (aOutString.Last() != HTMLEditUtils::kNewLine) {
+ aOutString.Append(HTMLEditUtils::kNewLine);
+ }
+ AddCite(aOutString, newCiteLevel);
+ aOutString.Append(HTMLEditUtils::kNewLine);
+
+ ++posInString;
+ outStringCol = 0;
+ continue;
+ }
+
+ // If the cite level has changed, then start a new line with the
+ // new cite level (but if we're at the beginning of the string,
+ // don't bother).
+ if (newCiteLevel != citeLevel && posInString > newCiteLevel + 1 &&
+ outStringCol) {
+ BreakLine(aOutString, outStringCol, 0);
+ }
+ citeLevel = newCiteLevel;
+
+ // Prepend the quote level to the out string if appropriate
+ if (!outStringCol) {
+ AddCite(aOutString, citeLevel);
+ outStringCol = citeLevel + (citeLevel ? 1 : 0);
+ }
+ // If it's not a cite, and we're not at the beginning of a line in
+ // the output string, add a space to separate new text from the
+ // previous text.
+ else if (outStringCol > citeLevel) {
+ aOutString.Append(HTMLEditUtils::kSpace);
+ ++outStringCol;
+ }
+
+ // find the next newline -- don't want to go farther than that
+ int32_t nextNewline =
+ tString.FindChar(HTMLEditUtils::kNewLine, posInString);
+ if (nextNewline < 0) {
+ nextNewline = length;
+ }
+
+ // For now, don't wrap unquoted lines at all.
+ // This is because the plaintext edit window has already wrapped them
+ // by the time we get them for rewrap, yet when we call the line
+ // breaker, it will refuse to break backwards, and we'll end up
+ // with a line that's too long and gets displayed as a lone word
+ // on a line by itself. Need special logic to detect this case
+ // and break it ourselves without resorting to the line breaker.
+ if (!citeLevel) {
+ aOutString.Append(
+ Substring(tString, posInString, nextNewline - posInString));
+ outStringCol += nextNewline - posInString;
+ if (nextNewline != (int32_t)length) {
+ aOutString.Append(HTMLEditUtils::kNewLine);
+ outStringCol = 0;
+ }
+ posInString = nextNewline + 1;
+ continue;
+ }
+
+ // Otherwise we have to use the line breaker and loop
+ // over this line of the input string to get all of it:
+ while ((int32_t)posInString < nextNewline) {
+ // Skip over initial spaces:
+ while ((int32_t)posInString < nextNewline &&
+ nsCRT::IsAsciiSpace(tString[posInString])) {
+ ++posInString;
+ }
+
+ // If this is a short line, just append it and continue:
+ if (outStringCol + nextNewline - posInString <=
+ aWrapCol - citeLevel - 1) {
+ // If this short line is the final one in the in string,
+ // then we need to include the final newline, if any:
+ if (nextNewline + 1 == (int32_t)length &&
+ tString[nextNewline - 1] == HTMLEditUtils::kNewLine) {
+ ++nextNewline;
+ }
+ // Trim trailing spaces:
+ int32_t lastRealChar = nextNewline;
+ while ((uint32_t)lastRealChar > posInString &&
+ nsCRT::IsAsciiSpace(tString[lastRealChar - 1])) {
+ --lastRealChar;
+ }
+
+ aOutString +=
+ Substring(tString, posInString, lastRealChar - posInString);
+ outStringCol += lastRealChar - posInString;
+ posInString = nextNewline + 1;
+ continue;
+ }
+
+ int32_t eol = posInString + aWrapCol - citeLevel - outStringCol;
+ // eol is the prospective end of line.
+ // If it's already less than our current position,
+ // then our line is already too long, so break now.
+ if (eol <= (int32_t)posInString) {
+ BreakLine(aOutString, outStringCol, citeLevel);
+ continue; // continue inner loop, with outStringCol now at bol
+ }
+
+ MOZ_ASSERT(eol >= 0 && eol - posInString > 0);
+
+ uint32_t breakPt = 0;
+ Maybe<uint32_t> nextBreakPt;
+ intl::LineBreakIteratorUtf16 lineBreakIter(Span<const char16_t>(
+ tString.get() + posInString, length - posInString));
+ while (true) {
+ nextBreakPt = lineBreakIter.Next();
+ if (!nextBreakPt ||
+ *nextBreakPt > AssertedCast<uint32_t>(eol) - posInString) {
+ break;
+ }
+ breakPt = *nextBreakPt;
+ }
+
+ if (breakPt == 0) {
+ // If we couldn't find a breakpoint within the eol upper bound, and
+ // we're not starting a new line, then end this line and loop around
+ // again:
+ if (outStringCol > citeLevel + 1) {
+ BreakLine(aOutString, outStringCol, citeLevel);
+ continue; // continue inner loop, with outStringCol now at bol
+ }
+
+ MOZ_ASSERT(nextBreakPt.isSome(),
+ "Next() always treats end-of-text as a break");
+ breakPt = *nextBreakPt;
+ }
+
+ // Special case: maybe we should have wrapped last time.
+ // If the first breakpoint here makes the current line too long,
+ // then if we already have text on the current line,
+ // break and loop around again.
+ // If we're at the beginning of the current line, though,
+ // don't force a break since the long word might be a url
+ // and breaking it would make it unclickable on the other end.
+ const int SLOP = 6;
+ if (outStringCol + breakPt > aWrapCol + SLOP &&
+ outStringCol > citeLevel + 1) {
+ BreakLine(aOutString, outStringCol, citeLevel);
+ continue;
+ }
+
+ nsAutoString sub(Substring(tString, posInString, breakPt));
+ // skip newlines or white-space at the end of the string
+ int32_t subend = sub.Length();
+ while (subend > 0 && IsSpace(sub[subend - 1])) {
+ --subend;
+ }
+ sub.Left(sub, subend);
+ aOutString += sub;
+ outStringCol += sub.Length();
+ // Advance past the white-space which caused the wrap:
+ posInString += breakPt;
+ while (posInString < length && IsSpace(tString[posInString])) {
+ ++posInString;
+ }
+
+ // Add a newline and the quote level to the out string
+ if (posInString < length) { // not for the last line, though
+ BreakLine(aOutString, outStringCol, citeLevel);
+ }
+ } // end inner loop within one line of aInString
+ } // end outer loop over lines of aInString
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/InternetCiter.h b/editor/libeditor/InternetCiter.h
new file mode 100644
index 0000000000..2d92c6887e
--- /dev/null
+++ b/editor/libeditor/InternetCiter.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef InternetCiter_h
+#define InternetCiter_h
+
+#include "nscore.h"
+#include "nsStringFwd.h"
+
+namespace mozilla {
+
+/**
+ * Mail citations using standard Internet style.
+ */
+class InternetCiter final {
+ public:
+ static void GetCiteString(const nsAString& aInString, nsAString& aOutString);
+
+ static void Rewrap(const nsAString& aInString, uint32_t aWrapCol,
+ uint32_t aFirstLineOffset, bool aRespectNewlines,
+ nsAString& aOutString);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef InternetCiter_h
diff --git a/editor/libeditor/JoinNodesTransaction.cpp b/editor/libeditor/JoinNodesTransaction.cpp
new file mode 100644
index 0000000000..094a41a5c9
--- /dev/null
+++ b/editor/libeditor/JoinNodesTransaction.cpp
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "JoinNodesTransaction.h"
+
+#include "EditorDOMPoint.h" // for EditorDOMPoint, etc.
+#include "HTMLEditHelpers.h" // for SplitNodeResult
+#include "HTMLEditor.h" // for HTMLEditor
+#include "HTMLEditorInlines.h"
+#include "HTMLEditUtils.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/ToString.h"
+#include "mozilla/dom/Text.h"
+
+#include "nsAString.h"
+#include "nsDebug.h" // for NS_ASSERTION, etc.
+#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc.
+#include "nsIContent.h" // for nsIContent
+#include "nsISupportsImpl.h" // for QueryInterface, etc.
+
+namespace mozilla {
+
+using namespace dom;
+
+// static
+already_AddRefed<JoinNodesTransaction> JoinNodesTransaction::MaybeCreate(
+ HTMLEditor& aHTMLEditor, nsIContent& aLeftContent,
+ nsIContent& aRightContent) {
+ RefPtr<JoinNodesTransaction> transaction =
+ new JoinNodesTransaction(aHTMLEditor, aLeftContent, aRightContent);
+ if (NS_WARN_IF(!transaction->CanDoIt())) {
+ return nullptr;
+ }
+ return transaction.forget();
+}
+
+JoinNodesTransaction::JoinNodesTransaction(HTMLEditor& aHTMLEditor,
+ nsIContent& aLeftContent,
+ nsIContent& aRightContent)
+ : mHTMLEditor(&aHTMLEditor),
+ mRemovedContent(&aRightContent),
+ mKeepingContent(&aLeftContent) {
+ // printf("JoinNodesTransaction size: %zu\n", sizeof(JoinNodesTransaction));
+ static_assert(sizeof(JoinNodesTransaction) <= 64,
+ "Transaction classes may be created a lot and may be alive "
+ "long so that keep the foot print smaller as far as possible");
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const JoinNodesTransaction& aTransaction) {
+ aStream << "{ mParentNode=" << aTransaction.mParentNode.get();
+ if (aTransaction.mParentNode) {
+ aStream << " (" << *aTransaction.mParentNode << ")";
+ }
+ aStream << ", mRemovedContent=" << aTransaction.mRemovedContent.get();
+ if (aTransaction.mRemovedContent) {
+ aStream << " (" << *aTransaction.mRemovedContent << ")";
+ }
+ aStream << ", mKeepingContent=" << aTransaction.mKeepingContent.get();
+ if (aTransaction.mKeepingContent) {
+ aStream << " (" << *aTransaction.mKeepingContent << ")";
+ }
+ aStream << ", mJoinedOffset=" << aTransaction.mJoinedOffset
+ << ", mHTMLEditor=" << aTransaction.mHTMLEditor.get() << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(JoinNodesTransaction, EditTransactionBase,
+ mHTMLEditor, mParentNode, mRemovedContent,
+ mKeepingContent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JoinNodesTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+bool JoinNodesTransaction::CanDoIt() const {
+ if (NS_WARN_IF(!mKeepingContent) || NS_WARN_IF(!mRemovedContent) ||
+ NS_WARN_IF(!mHTMLEditor) ||
+ NS_WARN_IF(mRemovedContent->IsBeingRemoved()) ||
+ !mKeepingContent->IsInComposedDoc()) {
+ return false;
+ }
+ return HTMLEditUtils::IsRemovableFromParentNode(*mRemovedContent);
+}
+
+// After DoTransaction() and RedoTransaction(), the left node is removed from
+// the content tree and right node remains.
+NS_IMETHODIMP JoinNodesTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p JoinNodesTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ return DoTransactionInternal(RedoingTransaction::No);
+}
+
+nsresult JoinNodesTransaction::DoTransactionInternal(
+ RedoingTransaction aRedoingTransaction) {
+ if (MOZ_UNLIKELY(NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(!mKeepingContent) ||
+ NS_WARN_IF(!mRemovedContent) ||
+ NS_WARN_IF(mRemovedContent->IsBeingRemoved()))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsINode* removingContentParentNode = mRemovedContent->GetParentNode();
+ if (MOZ_UNLIKELY(NS_WARN_IF(!removingContentParentNode))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Verify that the joining content nodes have the same parent
+ if (MOZ_UNLIKELY(removingContentParentNode !=
+ mKeepingContent->GetParentNode())) {
+ NS_ASSERTION(false, "Nodes do not have same parent");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Set this instance's mParentNode. Other methods will see a non-null
+ // mParentNode and know all is well
+ mParentNode = removingContentParentNode;
+ // For now, setting mJoinedOffset to removed content length so that
+ // CreateJoinedPoint returns a point in mKeepingContent whose offset is
+ // the result if all content in mRemovedContent are moved to start or end of
+ // mKeepingContent without any intervention. The offset will be adjusted
+ // below.
+ mJoinedOffset = mKeepingContent->Length();
+
+ const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor;
+ const OwningNonNull<nsIContent> removingContent = *mRemovedContent;
+ const OwningNonNull<nsIContent> keepingContent = *mKeepingContent;
+ nsresult rv;
+ // Let's try to get actual joined point with the tacker.
+ auto joinNodesPoint = EditorDOMPoint::AtEndOf(keepingContent);
+ {
+ AutoTrackDOMPoint trackJoinNodePoint(htmlEditor->RangeUpdaterRef(),
+ &joinNodesPoint);
+ rv = htmlEditor->DoJoinNodes(keepingContent, removingContent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::DoJoinNodes() failed");
+ }
+ // Adjust join node offset to the actual offset where the original first
+ // content of the right node is.
+ mJoinedOffset = joinNodesPoint.Offset();
+
+ if (aRedoingTransaction == RedoingTransaction::No) {
+ htmlEditor->DidJoinNodesTransaction(*this, rv);
+ }
+
+ return rv;
+}
+
+// XXX: What if instead of split, we just deleted the unneeded children of
+// mRight and re-inserted mLeft?
+NS_IMETHODIMP JoinNodesTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p JoinNodesTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mParentNode) || NS_WARN_IF(!mKeepingContent) ||
+ NS_WARN_IF(!mRemovedContent) || NS_WARN_IF(!mHTMLEditor)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor;
+ const OwningNonNull<nsIContent> removedContent = *mRemovedContent;
+
+ Result<SplitNodeResult, nsresult> splitNodeResult = htmlEditor->DoSplitNode(
+ CreateJoinedPoint<EditorDOMPoint>(), removedContent);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::DoSplitNode() failed");
+ return splitNodeResult.unwrapErr();
+ }
+ // When adding caret suggestion to SplitNodeResult, here didn't change
+ // selection so that just ignore it.
+ splitNodeResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+NS_IMETHODIMP JoinNodesTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p JoinNodesTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+ return DoTransactionInternal(RedoingTransaction::Yes);
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/JoinNodesTransaction.h b/editor/libeditor/JoinNodesTransaction.h
new file mode 100644
index 0000000000..e762cec473
--- /dev/null
+++ b/editor/libeditor/JoinNodesTransaction.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef JoinNodesTransaction_h
+#define JoinNodesTransaction_h
+
+#include "EditTransactionBase.h" // for EditTransactionBase, etc.
+
+#include "EditorDOMPoint.h" // for EditorDOMPoint, etc.
+#include "EditorForwards.h"
+
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h" // for REFNSIID
+#include "nscore.h" // for NS_IMETHOD
+
+class nsIContent;
+class nsINode;
+
+namespace mozilla {
+
+/**
+ * A transaction that joins two nodes E1 (left node) and E2 (right node) into a
+ * single node E. The children of E are the children of E1 followed by the
+ * children of E2. After DoTransaction() and RedoTransaction(), E1 is removed
+ * from the content tree and E2 remains.
+ */
+class JoinNodesTransaction final : public EditTransactionBase {
+ protected:
+ JoinNodesTransaction(HTMLEditor& aHTMLEditor, nsIContent& aLeftContent,
+ nsIContent& aRightContent);
+
+ public:
+ /**
+ * Creates a join node transaction. This returns nullptr if cannot join the
+ * nodes.
+ *
+ * @param aHTMLEditor The provider of core editing operations.
+ * @param aLeftContent The first of two nodes to join.
+ * @param aRightContent The second of two nodes to join.
+ */
+ static already_AddRefed<JoinNodesTransaction> MaybeCreate(
+ HTMLEditor& aHTMLEditor, nsIContent& aLeftContent,
+ nsIContent& aRightContent);
+
+ /**
+ * CanDoIt() returns true if there are enough members and can join or
+ * restore the nodes. Otherwise, false.
+ */
+ bool CanDoIt() const;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(JoinNodesTransaction,
+ EditTransactionBase)
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(JoinNodesTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+
+ /**
+ * GetExistingContent() and GetRemovedContent() never returns nullptr
+ * unless the cycle collector clears them out.
+ */
+ nsIContent* GetExistingContent() const { return mKeepingContent; }
+ nsIContent* GetRemovedContent() const { return mRemovedContent; }
+ nsINode* GetParentNode() const { return mParentNode; }
+
+ template <typename EditorDOMPointType>
+ EditorDOMPointType CreateJoinedPoint() const {
+ if (MOZ_UNLIKELY(!mKeepingContent)) {
+ return EditorDOMPointType();
+ }
+ return EditorDOMPointType(
+ mKeepingContent, std::min(mJoinedOffset, mKeepingContent->Length()));
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const JoinNodesTransaction& aTransaction);
+
+ protected:
+ virtual ~JoinNodesTransaction() = default;
+
+ enum class RedoingTransaction { No, Yes };
+ MOZ_CAN_RUN_SCRIPT nsresult DoTransactionInternal(RedoingTransaction);
+
+ RefPtr<HTMLEditor> mHTMLEditor;
+
+ // The original parent of the left/right nodes.
+ nsCOMPtr<nsINode> mParentNode;
+
+ // Removed content after joined.
+ nsCOMPtr<nsIContent> mRemovedContent;
+
+ // The keeping content node which contains ex-children of mRemovedContent.
+ nsCOMPtr<nsIContent> mKeepingContent;
+
+ // Offset where the original first content is in mKeepingContent after
+ // doing or redoing.
+ uint32_t mJoinedOffset = 0u;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef JoinNodesTransaction_h
diff --git a/editor/libeditor/ManualNAC.h b/editor/libeditor/ManualNAC.h
new file mode 100644
index 0000000000..17d0e4d273
--- /dev/null
+++ b/editor/libeditor/ManualNAC.h
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_ManualNAC_h
+#define mozilla_ManualNAC_h
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+// 16 seems to be the maximum number of manual Native Anonymous Content (NAC)
+// nodes that editor creates for a given element.
+//
+// These need to be manually removed by the machinery that sets the NAC,
+// otherwise we'll leak.
+using ManualNACArray = AutoTArray<RefPtr<dom::Element>, 16>;
+
+/**
+ * Smart pointer class to own "manual" Native Anonymous Content, and perform
+ * the necessary registration and deregistration on the parent element.
+ */
+class ManualNACPtr final {
+ public:
+ ManualNACPtr() = default;
+ MOZ_IMPLICIT ManualNACPtr(decltype(nullptr)) {}
+ explicit ManualNACPtr(already_AddRefed<dom::Element> aNewNAC)
+ : mPtr(aNewNAC) {
+ if (!mPtr) {
+ return;
+ }
+
+ // Record the NAC on the element, so that AllChildrenIterator can find it.
+ nsIContent* parentContent = mPtr->GetParent();
+ auto nac = static_cast<ManualNACArray*>(
+ parentContent->GetProperty(nsGkAtoms::manualNACProperty));
+ if (!nac) {
+ nac = new ManualNACArray();
+ parentContent->SetProperty(nsGkAtoms::manualNACProperty, nac,
+ nsINode::DeleteProperty<ManualNACArray>);
+ }
+ nac->AppendElement(mPtr);
+ }
+
+ // We use move semantics, and delete the copy-constructor and operator=.
+ ManualNACPtr(ManualNACPtr&& aOther) : mPtr(std::move(aOther.mPtr)) {}
+ ManualNACPtr(ManualNACPtr& aOther) = delete;
+ ManualNACPtr& operator=(ManualNACPtr&& aOther) {
+ mPtr = std::move(aOther.mPtr);
+ return *this;
+ }
+ ManualNACPtr& operator=(ManualNACPtr& aOther) = delete;
+
+ ~ManualNACPtr() { Reset(); }
+
+ void Reset() {
+ if (!mPtr) {
+ return;
+ }
+
+ RefPtr<dom::Element> ptr = std::move(mPtr);
+ RemoveContentFromNACArray(ptr);
+ }
+
+ static bool IsManualNAC(nsIContent* aAnonContent) {
+ MOZ_ASSERT(aAnonContent->IsRootOfNativeAnonymousSubtree());
+ MOZ_ASSERT(aAnonContent->IsInComposedDoc());
+
+ auto* nac = static_cast<ManualNACArray*>(
+ aAnonContent->GetParent()->GetProperty(nsGkAtoms::manualNACProperty));
+ return nac && nac->Contains(aAnonContent);
+ }
+
+ static void RemoveContentFromNACArray(nsIContent* aAnonymousContent) {
+ nsIContent* parentContent = aAnonymousContent->GetParent();
+ if (!parentContent) {
+ NS_WARNING("Potentially leaking manual NAC");
+ return;
+ }
+
+ // Remove reference from the parent element.
+ auto* nac = static_cast<ManualNACArray*>(
+ parentContent->GetProperty(nsGkAtoms::manualNACProperty));
+ // Document::AdoptNode might remove all properties before destroying editor.
+ // So we have to consider that NAC could be already removed.
+ if (nac) {
+ nac->RemoveElement(aAnonymousContent);
+ if (nac->IsEmpty()) {
+ parentContent->RemoveProperty(nsGkAtoms::manualNACProperty);
+ }
+ }
+
+ aAnonymousContent->UnbindFromTree();
+ }
+
+ dom::Element* get() const { return mPtr.get(); }
+ dom::Element* operator->() const { return get(); }
+ operator dom::Element*() const& { return get(); }
+
+ private:
+ RefPtr<dom::Element> mPtr;
+};
+
+} // namespace mozilla
+
+inline void ImplCycleCollectionUnlink(mozilla::ManualNACPtr& field) {
+ field.Reset();
+}
+
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& callback,
+ const mozilla::ManualNACPtr& field, const char* name, uint32_t flags = 0) {
+ CycleCollectionNoteChild(callback, field.get(), name, flags);
+}
+
+#endif // #ifndef mozilla_ManualNAC_h
diff --git a/editor/libeditor/MoveNodeTransaction.cpp b/editor/libeditor/MoveNodeTransaction.cpp
new file mode 100644
index 0000000000..a64e5d5ec0
--- /dev/null
+++ b/editor/libeditor/MoveNodeTransaction.cpp
@@ -0,0 +1,300 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MoveNodeTransaction.h"
+
+#include "EditorBase.h" // for EditorBase
+#include "EditorDOMPoint.h" // for EditorDOMPoint
+#include "HTMLEditor.h" // for HTMLEditor
+#include "HTMLEditUtils.h" // for HTMLEditUtils
+
+#include "mozilla/Likely.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ToString.h"
+
+#include "nsDebug.h" // for NS_WARNING, etc.
+#include "nsError.h" // for NS_ERROR_NULL_POINTER, etc.
+#include "nsIContent.h" // for nsIContent
+#include "nsString.h" // for nsString
+
+namespace mozilla {
+
+using namespace dom;
+
+template already_AddRefed<MoveNodeTransaction> MoveNodeTransaction::MaybeCreate(
+ HTMLEditor& aHTMLEditor, nsIContent& aContentToMove,
+ const EditorDOMPoint& aPointToInsert);
+template already_AddRefed<MoveNodeTransaction> MoveNodeTransaction::MaybeCreate(
+ HTMLEditor& aHTMLEditor, nsIContent& aContentToMove,
+ const EditorRawDOMPoint& aPointToInsert);
+
+// static
+template <typename PT, typename CT>
+already_AddRefed<MoveNodeTransaction> MoveNodeTransaction::MaybeCreate(
+ HTMLEditor& aHTMLEditor, nsIContent& aContentToMove,
+ const EditorDOMPointBase<PT, CT>& aPointToInsert) {
+ if (NS_WARN_IF(!aContentToMove.GetParentNode()) ||
+ NS_WARN_IF(!aPointToInsert.IsSet())) {
+ return nullptr;
+ }
+ // TODO: We should not allow to move a node to improper container element.
+ // However, this is currently used to move invalid parent while
+ // processing the nodes. Therefore, treating the case as error breaks
+ // a lot.
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(aContentToMove)) ||
+ // The destination should be editable, but it may be in an orphan node or
+ // sub-tree to reduce number of DOM mutation events. In such case, we're
+ // okay to move a node into the non-editable content because we can assume
+ // that the caller will insert it into an editable element.
+ NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(
+ *aPointToInsert.GetContainer()) &&
+ aPointToInsert.GetContainer()->IsInComposedDoc())) {
+ return nullptr;
+ }
+ RefPtr<MoveNodeTransaction> transaction =
+ new MoveNodeTransaction(aHTMLEditor, aContentToMove, aPointToInsert);
+ return transaction.forget();
+}
+
+template <typename PT, typename CT>
+MoveNodeTransaction::MoveNodeTransaction(
+ HTMLEditor& aHTMLEditor, nsIContent& aContentToMove,
+ const EditorDOMPointBase<PT, CT>& aPointToInsert)
+ : mContentToMove(&aContentToMove),
+ mContainer(aPointToInsert.GetContainer()),
+ mReference(aPointToInsert.GetChild()),
+ mOldContainer(aContentToMove.GetParentNode()),
+ mOldNextSibling(aContentToMove.GetNextSibling()),
+ mHTMLEditor(&aHTMLEditor) {
+ MOZ_ASSERT(mContainer);
+ MOZ_ASSERT(mOldContainer);
+ MOZ_ASSERT_IF(mReference, mReference->GetParentNode() == mContainer);
+ MOZ_ASSERT_IF(mOldNextSibling,
+ mOldNextSibling->GetParentNode() == mOldContainer);
+ // printf("MoveNodeTransaction size: %zu\n", sizeof(MoveNodeTransaction));
+ static_assert(sizeof(MoveNodeTransaction) <= 72,
+ "Transaction classes may be created a lot and may be alive "
+ "long so that keep the foot print smaller as far as possible");
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const MoveNodeTransaction& aTransaction) {
+ auto DumpNodeDetails = [&](const nsINode* aNode) {
+ if (aNode) {
+ if (aNode->IsText()) {
+ nsAutoString data;
+ aNode->AsText()->GetData(data);
+ aStream << " (#text \"" << NS_ConvertUTF16toUTF8(data).get() << "\")";
+ } else {
+ aStream << " (" << *aNode << ")";
+ }
+ }
+ };
+ aStream << "{ mContentToMove=" << aTransaction.mContentToMove.get();
+ DumpNodeDetails(aTransaction.mContentToMove);
+ aStream << ", mContainer=" << aTransaction.mContainer.get();
+ DumpNodeDetails(aTransaction.mContainer);
+ aStream << ", mReference=" << aTransaction.mReference.get();
+ DumpNodeDetails(aTransaction.mReference);
+ aStream << ", mOldContainer=" << aTransaction.mOldContainer.get();
+ DumpNodeDetails(aTransaction.mOldContainer);
+ aStream << ", mOldNextSibling=" << aTransaction.mOldNextSibling.get();
+ DumpNodeDetails(aTransaction.mOldNextSibling);
+ aStream << ", mHTMLEditor=" << aTransaction.mHTMLEditor.get() << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MoveNodeTransaction, EditTransactionBase,
+ mHTMLEditor, mContentToMove, mContainer,
+ mReference, mOldContainer, mOldNextSibling)
+
+NS_IMPL_ADDREF_INHERITED(MoveNodeTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(MoveNodeTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MoveNodeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP MoveNodeTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p MoveNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+ return DoTransactionInternal();
+}
+
+nsresult MoveNodeTransaction::DoTransactionInternal() {
+ MOZ_DIAGNOSTIC_ASSERT(mHTMLEditor);
+ MOZ_DIAGNOSTIC_ASSERT(mContentToMove);
+ MOZ_DIAGNOSTIC_ASSERT(mContainer);
+ MOZ_DIAGNOSTIC_ASSERT(mOldContainer);
+
+ OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor;
+ OwningNonNull<nsIContent> contentToMove = *mContentToMove;
+ OwningNonNull<nsINode> container = *mContainer;
+ nsCOMPtr<nsIContent> newNextSibling = mReference;
+ if (contentToMove->IsElement()) {
+ nsresult rv = htmlEditor->MarkElementDirty(
+ MOZ_KnownLive(*contentToMove->AsElement()));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::MarkElementDirty() failed, but ignored");
+ }
+
+ {
+ AutoMoveNodeSelNotify notifyStoredRanges(
+ htmlEditor->RangeUpdaterRef(), EditorRawDOMPoint(contentToMove),
+ newNextSibling ? EditorRawDOMPoint(newNextSibling)
+ : EditorRawDOMPoint::AtEndOf(container));
+ IgnoredErrorResult error;
+ container->InsertBefore(contentToMove, newNextSibling, error);
+ // InsertBefore() may call MightThrowJSException() even if there is no
+ // error. We don't need the flag here.
+ error.WouldReportJSException();
+ if (error.Failed()) {
+ NS_WARNING("nsINode::InsertBefore() failed");
+ return error.StealNSResult();
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP MoveNodeTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p MoveNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(!mContentToMove) ||
+ NS_WARN_IF(!mOldContainer)) {
+ // Perhaps, nulled-out by the cycle collector.
+ return NS_ERROR_FAILURE;
+ }
+
+ // If the original point has been changed, refer mOldNextSibling if it's
+ // renasonable. Otherwise, use end of the old container.
+ if (mOldNextSibling && mOldContainer != mOldNextSibling->GetParentNode()) {
+ // TODO: Check whether the new container is proper one for containing
+ // mContentToMove. However, there are few testcases so that we
+ // shouldn't change here without creating a lot of undo tests.
+ if (mOldNextSibling->GetParentNode() &&
+ (mOldNextSibling->IsInComposedDoc() ||
+ !mOldContainer->IsInComposedDoc())) {
+ mOldContainer = mOldNextSibling->GetParentNode();
+ } else {
+ mOldNextSibling = nullptr; // end of mOldContainer
+ }
+ }
+
+ if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(*mContentToMove))) {
+ NS_WARNING(
+ "MoveNodeTransaction::UndoTransaction() couldn't move the "
+ "content due to not removable from its current container");
+ return NS_ERROR_FAILURE;
+ }
+ if (MOZ_UNLIKELY(!HTMLEditUtils::IsSimplyEditableNode(*mOldContainer))) {
+ NS_WARNING(
+ "MoveNodeTransaction::UndoTransaction() couldn't move the "
+ "content into the old container due to non-editable one");
+ return NS_ERROR_FAILURE;
+ }
+
+ // And store the latest node which should be referred at redoing.
+ mContainer = mContentToMove->GetParentNode();
+ mReference = mContentToMove->GetNextSibling();
+
+ OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor;
+ OwningNonNull<nsINode> oldContainer = *mOldContainer;
+ OwningNonNull<nsIContent> contentToMove = *mContentToMove;
+ nsCOMPtr<nsIContent> oldNextSibling = mOldNextSibling;
+ if (contentToMove->IsElement()) {
+ nsresult rv = htmlEditor->MarkElementDirty(
+ MOZ_KnownLive(*contentToMove->AsElement()));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::MarkElementDirty() failed, but ignored");
+ }
+
+ {
+ AutoMoveNodeSelNotify notifyStoredRanges(
+ htmlEditor->RangeUpdaterRef(), EditorRawDOMPoint(contentToMove),
+ oldNextSibling ? EditorRawDOMPoint(oldNextSibling)
+ : EditorRawDOMPoint::AtEndOf(oldContainer));
+ IgnoredErrorResult error;
+ oldContainer->InsertBefore(contentToMove, oldNextSibling, error);
+ // InsertBefore() may call MightThrowJSException() even if there is no
+ // error. We don't need the flag here.
+ error.WouldReportJSException();
+ if (error.Failed()) {
+ NS_WARNING("nsINode::InsertBefore() failed");
+ return error.StealNSResult();
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP MoveNodeTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p MoveNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(!mContentToMove) ||
+ NS_WARN_IF(!mContainer)) {
+ // Perhaps, nulled-out by the cycle collector.
+ return NS_ERROR_FAILURE;
+ }
+
+ // If the inserting point has been changed, refer mReference if it's
+ // renasonable. Otherwise, use end of the container.
+ if (mReference && mContainer != mReference->GetParentNode()) {
+ // TODO: Check whether the new container is proper one for containing
+ // mContentToMove. However, there are few testcases so that we
+ // shouldn't change here without creating a lot of redo tests.
+ if (mReference->GetParentNode() &&
+ (mReference->IsInComposedDoc() || !mContainer->IsInComposedDoc())) {
+ mContainer = mReference->GetParentNode();
+ } else {
+ mReference = nullptr; // end of mContainer
+ }
+ }
+
+ if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(*mContentToMove))) {
+ NS_WARNING(
+ "MoveNodeTransaction::RedoTransaction() couldn't move the "
+ "content due to not removable from its current container");
+ return NS_ERROR_FAILURE;
+ }
+ if (MOZ_UNLIKELY(!HTMLEditUtils::IsSimplyEditableNode(*mContainer))) {
+ NS_WARNING(
+ "MoveNodeTransaction::RedoTransaction() couldn't move the "
+ "content into the new container due to non-editable one");
+ return NS_ERROR_FAILURE;
+ }
+
+ // And store the latest node which should be back.
+ mOldContainer = mContentToMove->GetParentNode();
+ mOldNextSibling = mContentToMove->GetNextSibling();
+
+ nsresult rv = DoTransactionInternal();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeTransaction::DoTransactionInternal() failed");
+ return rv;
+ }
+
+ if (!mHTMLEditor->AllowsTransactionsToChangeSelection()) {
+ return NS_OK;
+ }
+
+ OwningNonNull<HTMLEditor> htmlEditor(*mHTMLEditor);
+ rv = htmlEditor->CollapseSelectionTo(
+ SuggestPointToPutCaret<EditorRawDOMPoint>());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/MoveNodeTransaction.h b/editor/libeditor/MoveNodeTransaction.h
new file mode 100644
index 0000000000..8ab1cd5b8b
--- /dev/null
+++ b/editor/libeditor/MoveNodeTransaction.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MoveNodeTransaction_h
+#define MoveNodeTransaction_h
+
+#include "EditTransactionBase.h" // for EditTransactionBase, etc.
+
+#include "EditorForwards.h"
+
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsIContent.h" // for nsIContent
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS_INHERITED
+
+namespace mozilla {
+
+/**
+ * A transaction that moves a content node to a specified point.
+ */
+class MoveNodeTransaction final : public EditTransactionBase {
+ protected:
+ template <typename PT, typename CT>
+ MoveNodeTransaction(HTMLEditor& aHTMLEditor, nsIContent& aContentToMove,
+ const EditorDOMPointBase<PT, CT>& aPointToInsert);
+
+ public:
+ /**
+ * Create a transaction for moving aContentToMove before the child at
+ * aPointToInsert.
+ *
+ * @param aHTMLEditor The editor which manages the transaction.
+ * @param aContentToMove The node to be moved.
+ * @param aPointToInsert The insertion point of aContentToMove.
+ * @return A MoveNodeTransaction which was initialized
+ * with the arguments.
+ */
+ template <typename PT, typename CT>
+ static already_AddRefed<MoveNodeTransaction> MaybeCreate(
+ HTMLEditor& aHTMLEditor, nsIContent& aContentToMove,
+ const EditorDOMPointBase<PT, CT>& aPointToInsert);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MoveNodeTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(MoveNodeTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+
+ /**
+ * SuggestPointToPutCaret() suggests a point after doing or redoing the
+ * transaction.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType SuggestPointToPutCaret() const {
+ if (MOZ_UNLIKELY(!mContainer || !mContentToMove)) {
+ return EditorDOMPointType();
+ }
+ return EditorDOMPointType::After(mContentToMove);
+ }
+
+ /**
+ * Suggest next insertion point if the caller wants to move another content
+ * node around the insertion point.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType SuggestNextInsertionPoint() const {
+ if (MOZ_UNLIKELY(!mContainer)) {
+ return EditorDOMPointType();
+ }
+ if (!mReference) {
+ return EditorDOMPointType::AtEndOf(mContainer);
+ }
+ if (MOZ_UNLIKELY(mReference->GetParentNode() != mContainer)) {
+ if (MOZ_LIKELY(mContentToMove->GetParentNode() == mContainer)) {
+ return EditorDOMPointType(mContentToMove).NextPoint();
+ }
+ return EditorDOMPointType::AtEndOf(mContainer);
+ }
+ return EditorDOMPointType(mReference);
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const MoveNodeTransaction& aTransaction);
+
+ protected:
+ virtual ~MoveNodeTransaction() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult DoTransactionInternal();
+
+ // The content which will be or was moved from mOldContainer to mContainer.
+ nsCOMPtr<nsIContent> mContentToMove;
+
+ // mContainer is new container of mContentToInsert after (re-)doing the
+ // transaction.
+ nsCOMPtr<nsINode> mContainer;
+
+ // mReference is the child content where mContentToMove should be or was
+ // inserted into the mContainer. This is typically the next sibling of
+ // mContentToMove after moving.
+ nsCOMPtr<nsIContent> mReference;
+
+ // mOldContainer is the original container of mContentToMove before moving.
+ nsCOMPtr<nsINode> mOldContainer;
+
+ // mOldNextSibling is the next sibling of mCOntentToMove before moving.
+ nsCOMPtr<nsIContent> mOldNextSibling;
+
+ // The editor for this transaction.
+ RefPtr<HTMLEditor> mHTMLEditor;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef MoveNodeTransaction_h
diff --git a/editor/libeditor/PendingStyles.cpp b/editor/libeditor/PendingStyles.cpp
new file mode 100644
index 0000000000..12666c5289
--- /dev/null
+++ b/editor/libeditor/PendingStyles.cpp
@@ -0,0 +1,503 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PendingStyles.h"
+
+#include <stddef.h>
+
+#include "EditAction.h"
+#include "EditorBase.h"
+#include "HTMLEditHelpers.h" // for EditorInlineStyle, EditorInlineStyleAndValue
+#include "HTMLEditor.h"
+#include "HTMLEditUtils.h"
+
+#include "mozilla/mozalloc.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/Selection.h"
+
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsINode.h"
+#include "nsISupports.h"
+#include "nsISupportsImpl.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+/********************************************************************
+ * mozilla::PendingStyle
+ *******************************************************************/
+
+EditorInlineStyle PendingStyle::ToInlineStyle() const {
+ return mTag ? EditorInlineStyle(*mTag, mAttribute)
+ : EditorInlineStyle::RemoveAllStyles();
+}
+
+EditorInlineStyleAndValue PendingStyle::ToInlineStyleAndValue() const {
+ MOZ_ASSERT(mTag);
+ return mAttribute ? EditorInlineStyleAndValue(*mTag, *mAttribute,
+ mAttributeValueOrCSSValue)
+ : EditorInlineStyleAndValue(*mTag);
+}
+
+/********************************************************************
+ * mozilla::PendingStyleCache
+ *******************************************************************/
+
+EditorInlineStyle PendingStyleCache::ToInlineStyle() const {
+ return EditorInlineStyle(mTag, mAttribute);
+}
+
+/********************************************************************
+ * mozilla::PendingStyles
+ *******************************************************************/
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PendingStyles)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PendingStyles)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastSelectionPoint)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PendingStyles)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastSelectionPoint)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+nsresult PendingStyles::UpdateSelState(const HTMLEditor& aHTMLEditor) {
+ if (!aHTMLEditor.SelectionRef().IsCollapsed()) {
+ return NS_OK;
+ }
+
+ mLastSelectionPoint =
+ aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (!mLastSelectionPoint.IsSet()) {
+ return NS_ERROR_FAILURE;
+ }
+ // We need to store only offset because referring child may be removed by
+ // we'll check the point later.
+ AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint);
+ return NS_OK;
+}
+
+void PendingStyles::PreHandleMouseEvent(const MouseEvent& aMouseDownOrUpEvent) {
+ MOZ_ASSERT(aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown ||
+ aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseUp);
+ bool& eventFiredInLinkElement =
+ aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown
+ ? mMouseDownFiredInLinkElement
+ : mMouseUpFiredInLinkElement;
+ eventFiredInLinkElement = false;
+ if (aMouseDownOrUpEvent.DefaultPrevented()) {
+ return;
+ }
+ // If mouse button is down or up in a link element, we shouldn't unlink
+ // it when we get a notification of selection change.
+ EventTarget* target = aMouseDownOrUpEvent.GetExplicitOriginalTarget();
+ if (NS_WARN_IF(!target)) {
+ return;
+ }
+ nsIContent* targetContent = nsIContent::FromEventTarget(target);
+ if (NS_WARN_IF(!targetContent)) {
+ return;
+ }
+ eventFiredInLinkElement =
+ HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent);
+}
+
+void PendingStyles::PreHandleSelectionChangeCommand(Command aCommand) {
+ mLastSelectionCommand = aCommand;
+}
+
+void PendingStyles::PostHandleSelectionChangeCommand(
+ const HTMLEditor& aHTMLEditor, Command aCommand) {
+ if (mLastSelectionCommand != aCommand) {
+ return;
+ }
+
+ // If `OnSelectionChange()` hasn't been called for `mLastSelectionCommand`,
+ // it means that it didn't cause selection change.
+ if (!aHTMLEditor.SelectionRef().IsCollapsed() ||
+ !aHTMLEditor.SelectionRef().RangeCount()) {
+ return;
+ }
+
+ const auto caretPoint =
+ aHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!caretPoint.IsSet())) {
+ return;
+ }
+
+ if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint)) {
+ return;
+ }
+
+ // If all styles are cleared or link style is explicitly set, we
+ // shouldn't reset them without caret move.
+ if (AreAllStylesCleared() || IsLinkStyleSet()) {
+ return;
+ }
+ // And if non-link styles are cleared or some styles are set, we
+ // shouldn't reset them too, but we may need to change the link
+ // style.
+ if (AreSomeStylesSet() ||
+ (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
+ ClearLinkAndItsSpecifiedStyle();
+ return;
+ }
+
+ Reset();
+ ClearLinkAndItsSpecifiedStyle();
+}
+
+void PendingStyles::OnSelectionChange(const HTMLEditor& aHTMLEditor,
+ int16_t aReason) {
+ // XXX: Selection currently generates bogus selection changed notifications
+ // XXX: (bug 140303). It can notify us when the selection hasn't actually
+ // XXX: changed, and it notifies us more than once for the same change.
+ // XXX:
+ // XXX: The following code attempts to work around the bogus notifications,
+ // XXX: and should probably be removed once bug 140303 is fixed.
+ // XXX:
+ // XXX: This code temporarily fixes the problem where clicking the mouse in
+ // XXX: the same location clears the type-in-state.
+
+ const bool causedByFrameSelectionMoveCaret =
+ (aReason & (nsISelectionListener::KEYPRESS_REASON |
+ nsISelectionListener::COLLAPSETOSTART_REASON |
+ nsISelectionListener::COLLAPSETOEND_REASON)) &&
+ !(aReason & nsISelectionListener::JS_REASON);
+
+ Command lastSelectionCommand = mLastSelectionCommand;
+ if (causedByFrameSelectionMoveCaret) {
+ mLastSelectionCommand = Command::DoNothing;
+ }
+
+ bool mouseEventFiredInLinkElement = false;
+ if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
+ nsISelectionListener::MOUSEUP_REASON)) {
+ MOZ_ASSERT((aReason & (nsISelectionListener::MOUSEDOWN_REASON |
+ nsISelectionListener::MOUSEUP_REASON)) !=
+ (nsISelectionListener::MOUSEDOWN_REASON |
+ nsISelectionListener::MOUSEUP_REASON));
+ bool& eventFiredInLinkElement =
+ aReason & nsISelectionListener::MOUSEDOWN_REASON
+ ? mMouseDownFiredInLinkElement
+ : mMouseUpFiredInLinkElement;
+ mouseEventFiredInLinkElement = eventFiredInLinkElement;
+ eventFiredInLinkElement = false;
+ }
+
+ bool unlink = false;
+ bool resetAllStyles = true;
+ if (aHTMLEditor.SelectionRef().IsCollapsed() &&
+ aHTMLEditor.SelectionRef().RangeCount()) {
+ const auto selectionStartPoint =
+ aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (MOZ_UNLIKELY(NS_WARN_IF(!selectionStartPoint.IsSet()))) {
+ return;
+ }
+
+ if (mLastSelectionPoint == selectionStartPoint) {
+ // If all styles are cleared or link style is explicitly set, we
+ // shouldn't reset them without caret move.
+ if (AreAllStylesCleared() || IsLinkStyleSet()) {
+ return;
+ }
+ // And if non-link styles are cleared or some styles are set, we
+ // shouldn't reset them too, but we may need to change the link
+ // style.
+ if (AreSomeStylesSet() ||
+ (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
+ resetAllStyles = false;
+ }
+ }
+
+ RefPtr<Element> linkElement;
+ if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint,
+ getter_AddRefs(linkElement))) {
+ // If caret comes from outside of <a href> element, we should clear "link"
+ // style after reset.
+ if (causedByFrameSelectionMoveCaret) {
+ MOZ_ASSERT(!(aReason & (nsISelectionListener::MOUSEDOWN_REASON |
+ nsISelectionListener::MOUSEUP_REASON)));
+ // If caret is moves in a link per character, we should keep inserting
+ // new text to the link because user may want to keep extending the link
+ // text. Otherwise, e.g., using `End` or `Home` key. we should insert
+ // new text outside the link because it should be possible to user
+ // choose it, and this is similar to the other browsers.
+ switch (lastSelectionCommand) {
+ case Command::CharNext:
+ case Command::CharPrevious:
+ case Command::MoveLeft:
+ case Command::MoveLeft2:
+ case Command::MoveRight:
+ case Command::MoveRight2:
+ // If selection becomes collapsed, we should unlink new text.
+ if (!mLastSelectionPoint.IsSet()) {
+ unlink = true;
+ break;
+ }
+ // Special case, if selection isn't moved, it means that caret is
+ // positioned at start or end of an editing host. In this case,
+ // we can unlink it even with arrow key press.
+ // TODO: This does not work as expected for `ArrowLeft` key press
+ // at start of an editing host.
+ if (mLastSelectionPoint == selectionStartPoint) {
+ unlink = true;
+ break;
+ }
+ // Otherwise, if selection is moved in a link element, we should
+ // keep inserting new text into the link. Note that this is our
+ // traditional behavior, but different from the other browsers.
+ // If this breaks some web apps, we should change our behavior,
+ // but let's wait a report because our traditional behavior allows
+ // user to type text into start/end of a link only when user
+ // moves caret inside the link with arrow keys.
+ unlink =
+ !mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf(
+ linkElement);
+ break;
+ default:
+ // If selection is moved without arrow keys, e.g., `Home` and
+ // `End`, we should not insert new text into the link element.
+ // This is important for web-compat especially when the link is
+ // the last content in the block.
+ unlink = true;
+ break;
+ }
+ } else if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
+ nsISelectionListener::MOUSEUP_REASON)) {
+ // If the corresponding mouse event is fired in a link element,
+ // we should keep treating inputting content as content in the link,
+ // but otherwise, i.e., clicked outside the link, we should stop
+ // treating inputting content as content in the link.
+ unlink = !mouseEventFiredInLinkElement;
+ } else if (aReason & nsISelectionListener::JS_REASON) {
+ // If this is caused by a call of Selection API or something similar
+ // API, we should not contain new inserting content to the link.
+ unlink = true;
+ } else {
+ switch (aHTMLEditor.GetEditAction()) {
+ case EditAction::eDeleteBackward:
+ case EditAction::eDeleteForward:
+ case EditAction::eDeleteSelection:
+ case EditAction::eDeleteToBeginningOfSoftLine:
+ case EditAction::eDeleteToEndOfSoftLine:
+ case EditAction::eDeleteWordBackward:
+ case EditAction::eDeleteWordForward:
+ // This selection change is caused by the editor and the edit
+ // action is deleting content at edge of a link, we shouldn't
+ // keep the link style for new inserted content.
+ unlink = true;
+ break;
+ default:
+ break;
+ }
+ }
+ } else if (mLastSelectionPoint == selectionStartPoint) {
+ return;
+ }
+
+ mLastSelectionPoint = selectionStartPoint;
+ // We need to store only offset because referring child may be removed by
+ // we'll check the point later.
+ AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint);
+ } else {
+ if (aHTMLEditor.SelectionRef().RangeCount()) {
+ // If selection starts from a link, we shouldn't preserve the link style
+ // unless the range is entirely in the link.
+ EditorRawDOMRange firstRange(*aHTMLEditor.SelectionRef().GetRangeAt(0));
+ if (firstRange.StartRef().IsInContentNode() &&
+ HTMLEditUtils::IsContentInclusiveDescendantOfLink(
+ *firstRange.StartRef().ContainerAs<nsIContent>())) {
+ unlink = !HTMLEditUtils::IsRangeEntirelyInLink(firstRange);
+ }
+ }
+ mLastSelectionPoint.Clear();
+ }
+
+ if (resetAllStyles) {
+ Reset();
+ if (unlink) {
+ ClearLinkAndItsSpecifiedStyle();
+ }
+ return;
+ }
+
+ if (unlink == IsExplicitlyLinkStyleCleared()) {
+ return;
+ }
+
+ // Even if we shouldn't touch existing style, we need to set/clear only link
+ // style in some cases.
+ if (unlink) {
+ ClearLinkAndItsSpecifiedStyle();
+ return;
+ }
+ CancelClearingStyle(*nsGkAtoms::a, nullptr);
+}
+
+void PendingStyles::PreserveStyles(
+ const nsTArray<EditorInlineStyleAndValue>& aStylesToPreserve) {
+ for (const EditorInlineStyleAndValue& styleToPreserve : aStylesToPreserve) {
+ PreserveStyle(styleToPreserve.HTMLPropertyRef(), styleToPreserve.mAttribute,
+ styleToPreserve.mAttributeValue);
+ }
+}
+
+void PendingStyles::PreserveStyle(nsStaticAtom& aHTMLProperty,
+ nsAtom* aAttribute,
+ const nsAString& aAttributeValueOrCSSValue) {
+ // special case for big/small, these nest
+ if (nsGkAtoms::big == &aHTMLProperty) {
+ mRelativeFontSize++;
+ return;
+ }
+ if (nsGkAtoms::small == &aHTMLProperty) {
+ mRelativeFontSize--;
+ return;
+ }
+
+ Maybe<size_t> index = IndexOfPreservingStyle(aHTMLProperty, aAttribute);
+ if (index.isSome()) {
+ // If it's already set, update the value
+ mPreservingStyles[index.value()]->UpdateAttributeValueOrCSSValue(
+ aAttributeValueOrCSSValue);
+ return;
+ }
+
+ // font-size and font-family need to be applied outer-most because height of
+ // outer inline elements of them are computed without these styles. E.g.,
+ // background-color may be applied bottom-half of the text. Therefore, we
+ // need to apply the font styles first.
+ UniquePtr<PendingStyle> style = MakeUnique<PendingStyle>(
+ &aHTMLProperty, aAttribute, aAttributeValueOrCSSValue);
+ if (&aHTMLProperty == nsGkAtoms::font && aAttribute != nsGkAtoms::bgcolor) {
+ MOZ_ASSERT(aAttribute == nsGkAtoms::color ||
+ aAttribute == nsGkAtoms::face || aAttribute == nsGkAtoms::size);
+ mPreservingStyles.InsertElementAt(0, std::move(style));
+ } else {
+ mPreservingStyles.AppendElement(std::move(style));
+ }
+
+ CancelClearingStyle(aHTMLProperty, aAttribute);
+}
+
+void PendingStyles::ClearStyles(
+ const nsTArray<EditorInlineStyle>& aStylesToClear) {
+ for (const EditorInlineStyle& styleToClear : aStylesToClear) {
+ if (styleToClear.IsStyleToClearAllInlineStyles()) {
+ ClearAllStyles();
+ return;
+ }
+ if (styleToClear.mHTMLProperty == nsGkAtoms::href ||
+ styleToClear.mHTMLProperty == nsGkAtoms::name) {
+ ClearStyleInternal(nsGkAtoms::a, nullptr);
+ } else {
+ ClearStyleInternal(styleToClear.mHTMLProperty, styleToClear.mAttribute);
+ }
+ }
+}
+
+void PendingStyles::ClearStyleInternal(
+ nsStaticAtom* aHTMLProperty, nsAtom* aAttribute,
+ SpecifiedStyle aSpecifiedStyle /* = SpecifiedStyle::Preserve */) {
+ if (IsStyleCleared(aHTMLProperty, aAttribute)) {
+ return;
+ }
+
+ CancelPreservingStyle(aHTMLProperty, aAttribute);
+
+ mClearingStyles.AppendElement(MakeUnique<PendingStyle>(
+ aHTMLProperty, aAttribute, u""_ns, aSpecifiedStyle));
+}
+
+void PendingStyles::TakeAllPreservedStyles(
+ nsTArray<EditorInlineStyleAndValue>& aOutStylesAndValues) {
+ aOutStylesAndValues.SetCapacity(aOutStylesAndValues.Length() +
+ mPreservingStyles.Length());
+ for (const UniquePtr<PendingStyle>& preservedStyle : mPreservingStyles) {
+ aOutStylesAndValues.AppendElement(
+ preservedStyle->GetAttribute()
+ ? EditorInlineStyleAndValue(
+ *preservedStyle->GetTag(), *preservedStyle->GetAttribute(),
+ preservedStyle->AttributeValueOrCSSValueRef())
+ : EditorInlineStyleAndValue(*preservedStyle->GetTag()));
+ }
+ mPreservingStyles.Clear();
+}
+
+/**
+ * TakeRelativeFontSize() hands back relative font value, which is then
+ * cleared out.
+ */
+int32_t PendingStyles::TakeRelativeFontSize() {
+ int32_t relSize = mRelativeFontSize;
+ mRelativeFontSize = 0;
+ return relSize;
+}
+
+PendingStyleState PendingStyles::GetStyleState(
+ nsStaticAtom& aHTMLProperty, nsAtom* aAttribute /* = nullptr */,
+ nsString* aOutNewAttributeValueOrCSSValue /* = nullptr */) const {
+ if (IndexOfPreservingStyle(aHTMLProperty, aAttribute,
+ aOutNewAttributeValueOrCSSValue)
+ .isSome()) {
+ return PendingStyleState::BeingPreserved;
+ }
+
+ if (IsStyleCleared(&aHTMLProperty, aAttribute)) {
+ return PendingStyleState::BeingCleared;
+ }
+
+ return PendingStyleState::NotUpdated;
+}
+
+void PendingStyles::CancelPreservingStyle(nsStaticAtom* aHTMLProperty,
+ nsAtom* aAttribute) {
+ if (!aHTMLProperty) {
+ mPreservingStyles.Clear();
+ mRelativeFontSize = 0;
+ return;
+ }
+ Maybe<size_t> index = IndexOfPreservingStyle(*aHTMLProperty, aAttribute);
+ if (index.isSome()) {
+ mPreservingStyles.RemoveElementAt(index.value());
+ }
+}
+
+void PendingStyles::CancelClearingStyle(nsStaticAtom& aHTMLProperty,
+ nsAtom* aAttribute) {
+ Maybe<size_t> index =
+ IndexOfStyleInArray(&aHTMLProperty, aAttribute, nullptr, mClearingStyles);
+ if (index.isSome()) {
+ mClearingStyles.RemoveElementAt(index.value());
+ }
+}
+
+Maybe<size_t> PendingStyles::IndexOfStyleInArray(
+ nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, nsAString* aOutValue,
+ const nsTArray<UniquePtr<PendingStyle>>& aArray) {
+ if (aAttribute == nsGkAtoms::_empty) {
+ aAttribute = nullptr;
+ }
+ for (size_t i : IntegerRange(aArray.Length())) {
+ const UniquePtr<PendingStyle>& item = aArray[i];
+ if (item->GetTag() == aHTMLProperty && item->GetAttribute() == aAttribute) {
+ if (aOutValue) {
+ *aOutValue = item->AttributeValueOrCSSValueRef();
+ }
+ return Some(i);
+ }
+ }
+ return Nothing();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/PendingStyles.h b/editor/libeditor/PendingStyles.h
new file mode 100644
index 0000000000..b41e383e80
--- /dev/null
+++ b/editor/libeditor/PendingStyles.h
@@ -0,0 +1,339 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_PendingStyles_h
+#define mozilla_PendingStyles_h
+
+#include "mozilla/EditorDOMPoint.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+#include "nsAtom.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsGkAtoms.h"
+#include "nsISupportsImpl.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+class nsINode;
+
+namespace mozilla {
+namespace dom {
+class MouseEvent;
+class Selection;
+} // namespace dom
+
+enum class SpecifiedStyle : uint8_t { Preserve, Discard };
+
+class PendingStyle final {
+ public:
+ PendingStyle() = delete;
+ PendingStyle(nsStaticAtom* aTag, nsAtom* aAttribute, const nsAString& aValue,
+ SpecifiedStyle aSpecifiedStyle = SpecifiedStyle::Preserve)
+ : mTag(aTag),
+ mAttribute(aAttribute != nsGkAtoms::_empty ? aAttribute : nullptr),
+ mAttributeValueOrCSSValue(aValue),
+ mSpecifiedStyle(aSpecifiedStyle) {
+ MOZ_COUNT_CTOR(PendingStyle);
+ }
+ MOZ_COUNTED_DTOR(PendingStyle)
+
+ MOZ_KNOWN_LIVE nsStaticAtom* GetTag() const { return mTag; }
+ MOZ_KNOWN_LIVE nsAtom* GetAttribute() const { return mAttribute; }
+ const nsString& AttributeValueOrCSSValueRef() const {
+ return mAttributeValueOrCSSValue;
+ }
+ void UpdateAttributeValueOrCSSValue(const nsAString& aNewValue) {
+ mAttributeValueOrCSSValue = aNewValue;
+ }
+ SpecifiedStyle GetSpecifiedStyle() const { return mSpecifiedStyle; }
+
+ EditorInlineStyle ToInlineStyle() const;
+ EditorInlineStyleAndValue ToInlineStyleAndValue() const;
+
+ private:
+ MOZ_KNOWN_LIVE nsStaticAtom* const mTag = nullptr;
+ // TODO: Once we stop using `HTMLEditor::SetInlinePropertiesAsSubAction` to
+ // add any attributes of <a href>, we can make this `nsStaticAtom*`.
+ MOZ_KNOWN_LIVE const RefPtr<nsAtom> mAttribute;
+ // If the editor is in CSS mode, this value is the property value.
+ // If the editor is in HTML mode, this value is not empty only when
+ // - mAttribute is not nullptr
+ // - mTag is CSS invertible style and "-moz-editor-invert-value"
+ nsString mAttributeValueOrCSSValue;
+ // Whether the class and style attribute should be preserved or discarded.
+ const SpecifiedStyle mSpecifiedStyle = SpecifiedStyle::Preserve;
+};
+
+class PendingStyleCache final {
+ public:
+ PendingStyleCache() = delete;
+ PendingStyleCache(const nsStaticAtom& aTag, const nsStaticAtom* aAttribute,
+ const nsAString& aValue)
+ // Needs const_cast hack here because the this class users may want
+ // non-const nsStaticAtom reference/pointer due to bug 1794954
+ : mTag(const_cast<nsStaticAtom&>(aTag)),
+ mAttribute(const_cast<nsStaticAtom*>(aAttribute)),
+ mAttributeValueOrCSSValue(aValue) {}
+ PendingStyleCache(const nsStaticAtom& aTag, const nsStaticAtom* aAttribute,
+ nsAString&& aValue)
+ // Needs const_cast hack here because the this class users may want
+ // non-const nsStaticAtom reference/pointer due to bug 1794954
+ : mTag(const_cast<nsStaticAtom&>(aTag)),
+ mAttribute(const_cast<nsStaticAtom*>(aAttribute)),
+ mAttributeValueOrCSSValue(std::move(aValue)) {}
+
+ MOZ_KNOWN_LIVE nsStaticAtom& TagRef() const { return mTag; }
+ MOZ_KNOWN_LIVE nsStaticAtom* GetAttribute() const { return mAttribute; }
+ const nsString& AttributeValueOrCSSValueRef() const {
+ return mAttributeValueOrCSSValue;
+ }
+
+ EditorInlineStyle ToInlineStyle() const;
+
+ private:
+ MOZ_KNOWN_LIVE nsStaticAtom& mTag;
+ MOZ_KNOWN_LIVE nsStaticAtom* const mAttribute;
+ const nsString mAttributeValueOrCSSValue;
+};
+
+class MOZ_STACK_CLASS AutoPendingStyleCacheArray final
+ : public AutoTArray<PendingStyleCache, 21> {
+ public:
+ [[nodiscard]] index_type IndexOf(const nsStaticAtom& aTag,
+ const nsStaticAtom* aAttribute) const {
+ for (index_type index = 0; index < Length(); ++index) {
+ const PendingStyleCache& styleCache = ElementAt(index);
+ if (&styleCache.TagRef() == &aTag &&
+ styleCache.GetAttribute() == aAttribute) {
+ return index;
+ }
+ }
+ return NoIndex;
+ }
+
+ [[nodiscard]] bool Contains(const nsStaticAtom& aTag,
+ const nsStaticAtom* aAttribute) const {
+ return IndexOf(aTag, aAttribute) != NoIndex;
+ }
+};
+
+enum class PendingStyleState {
+ // Will be not changed in new content
+ NotUpdated,
+ // Will be applied to new content
+ BeingPreserved,
+ // Will be cleared from new content
+ BeingCleared,
+};
+
+/******************************************************************************
+ * PendingStyles stores pending inline styles which WILL be applied to new
+ * content when it'll be inserted. I.e., updating styles of this class means
+ * that it does NOT update the DOM tree immediately.
+ ******************************************************************************/
+class PendingStyles final {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PendingStyles)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PendingStyles)
+
+ PendingStyles() = default;
+
+ void Reset() {
+ mClearingStyles.Clear();
+ mPreservingStyles.Clear();
+ }
+
+ nsresult UpdateSelState(const HTMLEditor& aHTMLEditor);
+
+ /**
+ * PreHandleMouseEvent() is called when `HTMLEditorEventListener` receives
+ * "mousedown" and "mouseup" events. Note that `aMouseDownOrUpEvent` may not
+ * be acceptable event for the `HTMLEditor`, but this is called even in
+ * the case because the event may cause a following `OnSelectionChange()`
+ * call.
+ */
+ void PreHandleMouseEvent(const dom::MouseEvent& aMouseDownOrUpEvent);
+
+ void PreHandleSelectionChangeCommand(Command aCommand);
+ void PostHandleSelectionChangeCommand(const HTMLEditor& aHTMLEditor,
+ Command aCommand);
+
+ void OnSelectionChange(const HTMLEditor& aHTMLEditor, int16_t aReason);
+
+ /**
+ * Preserve the style for next inserting content. E.g., when user types next
+ * character, an inline element which provides the style will be inserted
+ * and the typing character will appear in it.
+ *
+ * @param aHTMLProperty The HTML tag name which represents the style.
+ * For example, nsGkAtoms::b for bold text.
+ * @param aAttribute nullptr or attribute name which represents the
+ * style with aHTMLProperty. E.g., nsGkAtoms::size
+ * for nsGkAtoms::font.
+ * @param aAttributeValueOrCSSValue
+ * New value of aAttribute or new CSS value if the
+ * editor is in the CSS mode.
+ */
+ void PreserveStyle(nsStaticAtom& aHTMLProperty, nsAtom* aAttribute,
+ const nsAString& aAttributeValueOrCSSValue);
+
+ /**
+ * Preserve the styles with new values in aStylesToPreserve.
+ * See above for the detail.
+ */
+ void PreserveStyles(
+ const nsTArray<EditorInlineStyleAndValue>& aStylesToPreserve);
+
+ /**
+ * Preserve the style with new value specified with aStyleToPreserve.
+ * See above for the detail.
+ */
+ void PreserveStyle(const PendingStyleCache& aStyleToPreserve) {
+ PreserveStyle(aStyleToPreserve.TagRef(), aStyleToPreserve.GetAttribute(),
+ aStyleToPreserve.AttributeValueOrCSSValueRef());
+ }
+
+ /**
+ * Clear the style when next content is inserted. E.g., when user types next
+ * character, it'll will be inserted after parent element which provides the
+ * style and clones element which represents other styles in the parent
+ * element.
+ *
+ * @param aHTMLProperty The HTML tag name which represents the style.
+ * For example, nsGkAtoms::b for bold text.
+ * @param aAttribute nullptr or attribute name which represents the
+ * style with aHTMLProperty. E.g., nsGkAtoms::size
+ * for nsGkAtoms::font.
+ */
+ void ClearStyle(nsStaticAtom& aHTMLProperty, nsAtom* aAttribute) {
+ ClearStyleInternal(&aHTMLProperty, aAttribute);
+ }
+
+ /**
+ * Clear all styles specified by aStylesToClear when next content is inserted.
+ * See above for the detail.
+ */
+ void ClearStyles(const nsTArray<EditorInlineStyle>& aStylesToClear);
+
+ /**
+ * Clear all styles when next inserting content. E.g., when user types next
+ * character, it will be inserted outside any inline parents which provides
+ * current text style.
+ */
+ void ClearAllStyles() {
+ // XXX Why don't we clear mClearingStyles first?
+ ClearStyleInternal(nullptr, nullptr);
+ }
+
+ /**
+ * Clear <a> element and discard styles which is applied by it.
+ */
+ void ClearLinkAndItsSpecifiedStyle() {
+ ClearStyleInternal(nsGkAtoms::a, nullptr, SpecifiedStyle::Discard);
+ }
+
+ /**
+ * TakeClearingStyle() hands back next property item on the clearing styles.
+ * This must be used only for handling to clear the styles from inserting
+ * content.
+ */
+ UniquePtr<PendingStyle> TakeClearingStyle() {
+ if (mClearingStyles.IsEmpty()) {
+ return nullptr;
+ }
+ return mClearingStyles.PopLastElement();
+ }
+
+ /**
+ * TakeAllPreservedStyles() moves all preserved styles and values to
+ * aOutStylesAndValues.
+ */
+ void TakeAllPreservedStyles(
+ nsTArray<EditorInlineStyleAndValue>& aOutStylesAndValues);
+
+ /**
+ * TakeRelativeFontSize() hands back relative font value, which is then
+ * cleared out.
+ */
+ int32_t TakeRelativeFontSize();
+
+ /**
+ * GetStyleState() returns whether the style will be applied to new content,
+ * removed from new content or not changed.
+ *
+ * @param aHTMLProperty The HTML tag name which represents the style.
+ * For example, nsGkAtoms::b for bold text.
+ * @param aAttribute nullptr or attribute name which represents the
+ * style with aHTMLProperty. E.g., nsGkAtoms::size
+ * for nsGkAtoms::font.
+ * @param aOutNewAttributeValueOrCSSValue
+ * [out, optional] If applied to new content, this
+ * is set to the new value.
+ */
+ PendingStyleState GetStyleState(
+ nsStaticAtom& aHTMLProperty, nsAtom* aAttribute = nullptr,
+ nsString* aOutNewAttributeValueOrCSSValue = nullptr) const;
+
+ protected:
+ virtual ~PendingStyles() { Reset(); };
+
+ void ClearStyleInternal(
+ nsStaticAtom* aHTMLProperty, nsAtom* aAttribute,
+ SpecifiedStyle aSpecifiedStyle = SpecifiedStyle::Preserve);
+
+ void CancelPreservingStyle(nsStaticAtom* aHTMLProperty, nsAtom* aAttribute);
+ void CancelClearingStyle(nsStaticAtom& aHTMLProperty, nsAtom* aAttribute);
+
+ Maybe<size_t> IndexOfPreservingStyle(nsStaticAtom& aHTMLProperty,
+ nsAtom* aAttribute,
+ nsAString* aOutValue = nullptr) const {
+ return IndexOfStyleInArray(&aHTMLProperty, aAttribute, aOutValue,
+ mPreservingStyles);
+ }
+ Maybe<size_t> IndexOfClearingStyle(nsStaticAtom* aHTMLProperty,
+ nsAtom* aAttribute) const {
+ return IndexOfStyleInArray(aHTMLProperty, aAttribute, nullptr,
+ mClearingStyles);
+ }
+
+ bool IsLinkStyleSet() const {
+ return IndexOfPreservingStyle(*nsGkAtoms::a, nullptr).isSome();
+ }
+ bool IsExplicitlyLinkStyleCleared() const {
+ return IndexOfClearingStyle(nsGkAtoms::a, nullptr).isSome();
+ }
+ bool IsOnlyLinkStyleCleared() const {
+ return mClearingStyles.Length() == 1 && IsExplicitlyLinkStyleCleared();
+ }
+ bool IsStyleCleared(nsStaticAtom* aHTMLProperty, nsAtom* aAttribute) const {
+ return IndexOfClearingStyle(aHTMLProperty, aAttribute).isSome() ||
+ AreAllStylesCleared();
+ }
+ bool AreAllStylesCleared() const {
+ return IndexOfClearingStyle(nullptr, nullptr).isSome();
+ }
+ bool AreSomeStylesSet() const { return !mPreservingStyles.IsEmpty(); }
+ bool AreSomeStylesCleared() const { return !mClearingStyles.IsEmpty(); }
+
+ static Maybe<size_t> IndexOfStyleInArray(
+ nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, nsAString* aOutValue,
+ const nsTArray<UniquePtr<PendingStyle>>& aArray);
+
+ nsTArray<UniquePtr<PendingStyle>> mPreservingStyles;
+ nsTArray<UniquePtr<PendingStyle>> mClearingStyles;
+ EditorDOMPoint mLastSelectionPoint;
+ int32_t mRelativeFontSize = 0;
+ Command mLastSelectionCommand = Command::DoNothing;
+ bool mMouseDownFiredInLinkElement = false;
+ bool mMouseUpFiredInLinkElement = false;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_PendingStyles_h
diff --git a/editor/libeditor/PlaceholderTransaction.cpp b/editor/libeditor/PlaceholderTransaction.cpp
new file mode 100644
index 0000000000..70fe4189ea
--- /dev/null
+++ b/editor/libeditor/PlaceholderTransaction.cpp
@@ -0,0 +1,351 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PlaceholderTransaction.h"
+
+#include <utility>
+
+#include "CompositionTransaction.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ToString.h"
+#include "mozilla/dom/Selection.h"
+#include "nsGkAtoms.h"
+#include "nsQueryObject.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+PlaceholderTransaction::PlaceholderTransaction(
+ EditorBase& aEditorBase, nsStaticAtom& aName,
+ Maybe<SelectionState>&& aSelState)
+ : mEditorBase(&aEditorBase),
+ mCompositionTransaction(nullptr),
+ mStartSel(*std::move(aSelState)),
+ mAbsorb(true),
+ mCommitted(false) {
+ mName = &aName;
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PlaceholderTransaction)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PlaceholderTransaction,
+ EditAggregateTransaction)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditorBase);
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mStartSel);
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndSel);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PlaceholderTransaction,
+ EditAggregateTransaction)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditorBase);
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStartSel);
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndSel);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PlaceholderTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditAggregateTransaction)
+
+NS_IMPL_ADDREF_INHERITED(PlaceholderTransaction, EditAggregateTransaction)
+NS_IMPL_RELEASE_INHERITED(PlaceholderTransaction, EditAggregateTransaction)
+
+void PlaceholderTransaction::AppendChild(EditTransactionBase& aTransaction) {
+ mChildren.AppendElement(aTransaction);
+}
+
+NS_IMETHODIMP PlaceholderTransaction::DoTransaction() {
+ MOZ_LOG(
+ GetLogModule(), LogLevel::Info,
+ ("%p PlaceholderTransaction::%s this={ mName=%s }", this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP PlaceholderTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p PlaceholderTransaction::%s this={ mName=%s } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+
+ if (NS_WARN_IF(!mEditorBase)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Undo transactions.
+ nsresult rv = EditAggregateTransaction::UndoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditAggregateTransaction::UndoTransaction() failed");
+ return rv;
+ }
+
+ // now restore selection
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+ rv = mStartSel.RestoreSelection(*selection);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "SelectionState::RestoreSelection() failed");
+
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p PlaceholderTransaction::%s this={ mName=%s } "
+ "End==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return rv;
+}
+
+NS_IMETHODIMP PlaceholderTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p PlaceholderTransaction::%s this={ mName=%s } "
+ "Start==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+
+ if (NS_WARN_IF(!mEditorBase)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Redo transactions.
+ nsresult rv = EditAggregateTransaction::RedoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditAggregateTransaction::RedoTransaction() failed");
+ return rv;
+ }
+
+ // now restore selection
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+ rv = mEndSel.RestoreSelection(*selection);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "SelectionState::RestoreSelection() failed");
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p PlaceholderTransaction::%s this={ mName=%s } "
+ "End==============================",
+ this, __FUNCTION__,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return rv;
+}
+
+NS_IMETHODIMP PlaceholderTransaction::Merge(nsITransaction* aOtherTransaction,
+ bool* aDidMerge) {
+ if (NS_WARN_IF(!aDidMerge) || NS_WARN_IF(!aOtherTransaction)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // set out param default value
+ *aDidMerge = false;
+
+ if (mForwardingTransaction) {
+ MOZ_ASSERT_UNREACHABLE(
+ "tried to merge into a placeholder that was in "
+ "forwarding mode!");
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<EditTransactionBase> otherTransactionBase =
+ aOtherTransaction->GetAsEditTransactionBase();
+ if (!otherTransactionBase) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p PlaceholderTransaction::%s(aOtherTransaction=%p) this={ "
+ "mName=%s } returned false due to non edit transaction",
+ this, __FUNCTION__, aOtherTransaction,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+ }
+
+ // We are absorbing all transactions if mAbsorb is lit.
+ if (mAbsorb) {
+ if (CompositionTransaction* otherCompositionTransaction =
+ otherTransactionBase->GetAsCompositionTransaction()) {
+ // special handling for CompositionTransaction's: they need to merge with
+ // any previous CompositionTransaction in this placeholder, if possible.
+ if (!mCompositionTransaction) {
+ // this is the first IME txn in the placeholder
+ mCompositionTransaction = otherCompositionTransaction;
+ AppendChild(*otherCompositionTransaction);
+ } else {
+ bool didMerge;
+ mCompositionTransaction->Merge(otherCompositionTransaction, &didMerge);
+ if (!didMerge) {
+ // it wouldn't merge. Earlier IME txn is already committed and will
+ // not absorb further IME txns. So just stack this one after it
+ // and remember it as a candidate for further merges.
+ mCompositionTransaction = otherCompositionTransaction;
+ AppendChild(*otherCompositionTransaction);
+ }
+ }
+ } else {
+ PlaceholderTransaction* otherPlaceholderTransaction =
+ otherTransactionBase->GetAsPlaceholderTransaction();
+ if (!otherPlaceholderTransaction) {
+ // See bug 171243: just drop incoming placeholders on the floor.
+ // Their children will be swallowed by this preexisting one.
+ AppendChild(*otherTransactionBase);
+ }
+ }
+ *aDidMerge = true;
+ // RememberEndingSelection();
+ // efficiency hack: no need to remember selection here, as we haven't yet
+ // finished the initial batch and we know we will be told when the batch
+ // ends. we can remeber the selection then.
+ return NS_OK;
+ }
+
+ // merge typing or IME or deletion transactions if the selection matches
+ if (mCommitted ||
+ (mName != nsGkAtoms::TypingTxnName && mName != nsGkAtoms::IMETxnName &&
+ mName != nsGkAtoms::DeleteTxnName)) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p PlaceholderTransaction::%s(aOtherTransaction=%p) this={ "
+ "mName=%s } returned false due to non mergable transaction",
+ this, __FUNCTION__, aOtherTransaction,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+ }
+
+ PlaceholderTransaction* otherPlaceholderTransaction =
+ otherTransactionBase->GetAsPlaceholderTransaction();
+ if (!otherPlaceholderTransaction) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p PlaceholderTransaction::%s(aOtherTransaction=%p) this={ "
+ "mName=%s } returned false due to non placeholder transaction",
+ this, __FUNCTION__, aOtherTransaction,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+ }
+
+ RefPtr<nsAtom> otherTransactionName = otherPlaceholderTransaction->GetName();
+ if (!otherTransactionName || otherTransactionName == nsGkAtoms::_empty ||
+ otherTransactionName != mName) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p PlaceholderTransaction::%s(aOtherTransaction=%p) this={ "
+ "mName=%s } returned false due to non mergable placeholder "
+ "transaction",
+ this, __FUNCTION__, aOtherTransaction,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+ }
+
+ // check if start selection of next placeholder matches
+ // end selection of this placeholder
+ // XXX Theese checks seem wrong. The ending selection is initialized with
+ // actual Selection rather than expected Selection. Therefore, even when
+ // web apps modifies Selection, we don't merge mergable transactions.
+
+ // If the new transaction's starting Selection is not a caret, we shouldn't be
+ // merged with it because it's probably caused deleting the selection.
+ if (!otherPlaceholderTransaction->mStartSel.HasOnlyCollapsedRange()) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p PlaceholderTransaction::%s(aOtherTransaction=%p) this={ "
+ "mName=%s } returned false due to not collapsed selection at "
+ "start of new transactions",
+ this, __FUNCTION__, aOtherTransaction,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+ }
+
+ // If our ending Selection is not a caret, we should not be merged with it
+ // because we probably changed format of a block or style of text.
+ if (!mEndSel.HasOnlyCollapsedRange()) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p PlaceholderTransaction::%s(aOtherTransaction=%p) this={ "
+ "mName=%s } returned false due to not collapsed selection at end "
+ "of previous transactions",
+ this, __FUNCTION__, aOtherTransaction,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+ }
+
+ // If the caret positions are now in different root nodes, e.g., the previous
+ // caret position was removed from the DOM tree, this merge should not be
+ // done.
+ const bool isPreviousCaretPointInSameRootOfNewCaretPoint = [&]() {
+ nsINode* previousRootInCurrentDOMTree = mEndSel.GetCommonRootNode();
+ return previousRootInCurrentDOMTree &&
+ previousRootInCurrentDOMTree ==
+ otherPlaceholderTransaction->mStartSel.GetCommonRootNode();
+ }();
+ if (!isPreviousCaretPointInSameRootOfNewCaretPoint) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p PlaceholderTransaction::%s(aOtherTransaction=%p) this={ "
+ "mName=%s } returned false due to the caret points are in "
+ "different root nodes",
+ this, __FUNCTION__, aOtherTransaction,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+ }
+
+ // If the caret points of end of us and start of new transaction are not same,
+ // we shouldn't merge them.
+ if (!otherPlaceholderTransaction->mStartSel.Equals(mEndSel)) {
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p PlaceholderTransaction::%s(aOtherTransaction=%p) this={ "
+ "mName=%s } returned false due to caret positions were different",
+ this, __FUNCTION__, aOtherTransaction,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+ }
+
+ mAbsorb = true; // we need to start absorbing again
+ otherPlaceholderTransaction->ForwardEndBatchTo(*this);
+ // AppendChild(editTransactionBase);
+ // see bug 171243: we don't need to merge placeholders
+ // into placeholders. We just reactivate merging in the
+ // pre-existing placeholder and drop the new one on the floor. The
+ // EndPlaceHolderBatch() call on the new placeholder will be
+ // forwarded to this older one.
+ DebugOnly<nsresult> rvIgnored = RememberEndingSelection();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "PlaceholderTransaction::RememberEndingSelection() failed, but "
+ "ignored");
+ *aDidMerge = true;
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p PlaceholderTransaction::%s(aOtherTransaction=%p) this={ "
+ "mName=%s } returned true",
+ this, __FUNCTION__, aOtherTransaction,
+ nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
+ return NS_OK;
+}
+
+nsresult PlaceholderTransaction::EndPlaceHolderBatch() {
+ mAbsorb = false;
+
+ if (mForwardingTransaction) {
+ if (mForwardingTransaction) {
+ DebugOnly<nsresult> rvIgnored =
+ mForwardingTransaction->EndPlaceHolderBatch();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "PlaceholderTransaction::EndPlaceHolderBatch() failed, but ignored");
+ }
+ }
+ // remember our selection state.
+ nsresult rv = RememberEndingSelection();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "PlaceholderTransaction::RememberEndingSelection() failed");
+ return rv;
+}
+
+nsresult PlaceholderTransaction::RememberEndingSelection() {
+ if (NS_WARN_IF(!mEditorBase)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ RefPtr<Selection> selection = mEditorBase->GetSelection();
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+ mEndSel.SaveSelection(*selection);
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/PlaceholderTransaction.h b/editor/libeditor/PlaceholderTransaction.h
new file mode 100644
index 0000000000..8201d797fe
--- /dev/null
+++ b/editor/libeditor/PlaceholderTransaction.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef PlaceholderTransaction_h
+#define PlaceholderTransaction_h
+
+#include "EditAggregateTransaction.h"
+#include "EditorForwards.h"
+#include "SelectionState.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/WeakPtr.h"
+
+namespace mozilla {
+
+/**
+ * An aggregate transaction that knows how to absorb all subsequent
+ * transactions with the same name. This transaction does not "Do" anything.
+ * But it absorbs other transactions via merge, and can undo/redo the
+ * transactions it has absorbed.
+ */
+
+class PlaceholderTransaction final : public EditAggregateTransaction,
+ public SupportsWeakPtr {
+ protected:
+ PlaceholderTransaction(EditorBase& aEditorBase, nsStaticAtom& aName,
+ Maybe<SelectionState>&& aSelState);
+
+ public:
+ /**
+ * Creates a placeholder transaction. This never returns nullptr.
+ *
+ * @param aEditorBase The editor.
+ * @param aName The name of creating transaction.
+ * @param aSelState The selection state of aEditorBase.
+ */
+ static already_AddRefed<PlaceholderTransaction> Create(
+ EditorBase& aEditorBase, nsStaticAtom& aName,
+ Maybe<SelectionState>&& aSelState) {
+ // Make sure to move aSelState into a local variable to null out the
+ // original Maybe<SelectionState> variable.
+ Maybe<SelectionState> selState(std::move(aSelState));
+ RefPtr<PlaceholderTransaction> transaction =
+ new PlaceholderTransaction(aEditorBase, aName, std::move(selState));
+ return transaction.forget();
+ }
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PlaceholderTransaction,
+ EditAggregateTransaction)
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(PlaceholderTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override;
+
+ void AppendChild(EditTransactionBase& aTransaction);
+
+ nsresult EndPlaceHolderBatch();
+
+ void ForwardEndBatchTo(PlaceholderTransaction& aForwardingTransaction) {
+ mForwardingTransaction = &aForwardingTransaction;
+ }
+
+ void Commit() { mCommitted = true; }
+
+ nsresult RememberEndingSelection();
+
+ protected:
+ virtual ~PlaceholderTransaction() = default;
+
+ // The editor for this transaction.
+ RefPtr<EditorBase> mEditorBase;
+
+ WeakPtr<PlaceholderTransaction> mForwardingTransaction;
+
+ // First IME txn in this placeholder - used for IME merging.
+ WeakPtr<CompositionTransaction> mCompositionTransaction;
+
+ // These next two members store the state of the selection in a safe way.
+ // Selection at the start of the transaction is stored, as is the selection
+ // at the end. This is so that UndoTransaction() and RedoTransaction() can
+ // restore the selection properly.
+
+ SelectionState mStartSel;
+ SelectionState mEndSel;
+
+ // Do we auto absorb any and all transaction?
+ bool mAbsorb;
+ // Do we stop auto absorbing any matching placeholder transactions?
+ bool mCommitted;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef PlaceholderTransaction_h
diff --git a/editor/libeditor/ReplaceTextTransaction.cpp b/editor/libeditor/ReplaceTextTransaction.cpp
new file mode 100644
index 0000000000..6887fc6011
--- /dev/null
+++ b/editor/libeditor/ReplaceTextTransaction.cpp
@@ -0,0 +1,186 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ReplaceTextTransaction.h"
+
+#include "HTMLEditUtils.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/ToString.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+std::ostream& operator<<(std::ostream& aStream,
+ const ReplaceTextTransaction& aTransaction) {
+ aStream << "{ mTextNode=" << aTransaction.mTextNode.get();
+ if (aTransaction.mTextNode) {
+ aStream << " (" << *aTransaction.mTextNode << ")";
+ }
+ aStream << ", mStringToInsert=\""
+ << NS_ConvertUTF16toUTF8(aTransaction.mStringToInsert).get() << "\""
+ << ", mStringToBeReplaced=\""
+ << NS_ConvertUTF16toUTF8(aTransaction.mStringToBeReplaced).get()
+ << "\", mOffset=" << aTransaction.mOffset
+ << ", mEditorBase=" << aTransaction.mEditorBase.get() << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ReplaceTextTransaction, EditTransactionBase,
+ mEditorBase, mTextNode)
+
+NS_IMPL_ADDREF_INHERITED(ReplaceTextTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(ReplaceTextTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReplaceTextTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP ReplaceTextTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p ReplaceTextTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (MOZ_UNLIKELY(
+ NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*mTextNode)))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<Text> textNode = *mTextNode;
+
+ IgnoredErrorResult error;
+ editorBase->DoReplaceText(textNode, mOffset, mStringToBeReplaced.Length(),
+ mStringToInsert, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("EditorBase::DoReplaceText() failed");
+ return error.StealNSResult();
+ }
+ // XXX What should we do if mutation event listener changed the node?
+ editorBase->RangeUpdaterRef().SelAdjReplaceText(textNode, mOffset,
+ mStringToBeReplaced.Length(),
+ mStringToInsert.Length());
+ return NS_OK;
+}
+
+NS_IMETHODIMP ReplaceTextTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p ReplaceTextTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (MOZ_UNLIKELY(
+ NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*mTextNode)))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ IgnoredErrorResult error;
+ nsAutoString insertedString;
+ mTextNode->SubstringData(mOffset, mStringToInsert.Length(), insertedString,
+ error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("CharacterData::SubstringData() failed");
+ return error.StealNSResult();
+ }
+ if (MOZ_UNLIKELY(insertedString != mStringToInsert)) {
+ NS_WARNING(
+ "ReplaceTextTransaction::UndoTransaction() did nothing due to "
+ "unexpected text");
+ return NS_OK;
+ }
+
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<Text> textNode = *mTextNode;
+
+ editorBase->DoReplaceText(textNode, mOffset, mStringToInsert.Length(),
+ mStringToBeReplaced, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("EditorBase::DoReplaceText() failed");
+ return error.StealNSResult();
+ }
+ // XXX What should we do if mutation event listener changed the node?
+ editorBase->RangeUpdaterRef().SelAdjReplaceText(textNode, mOffset,
+ mStringToInsert.Length(),
+ mStringToBeReplaced.Length());
+
+ if (!editorBase->AllowsTransactionsToChangeSelection()) {
+ return NS_OK;
+ }
+
+ // XXX Should we stop setting selection when mutation event listener
+ // modifies the text node?
+ editorBase->CollapseSelectionTo(
+ EditorRawDOMPoint(textNode, mOffset + mStringToBeReplaced.Length()),
+ error);
+ if (MOZ_UNLIKELY(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_ASSERTION(!error.Failed(),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return NS_OK;
+}
+
+NS_IMETHODIMP ReplaceTextTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p ReplaceTextTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (MOZ_UNLIKELY(
+ NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*mTextNode)))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ IgnoredErrorResult error;
+ nsAutoString undoneString;
+ mTextNode->SubstringData(mOffset, mStringToBeReplaced.Length(), undoneString,
+ error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("CharacterData::SubstringData() failed");
+ return error.StealNSResult();
+ }
+ if (MOZ_UNLIKELY(undoneString != mStringToBeReplaced)) {
+ NS_WARNING(
+ "ReplaceTextTransaction::RedoTransaction() did nothing due to "
+ "unexpected text");
+ return NS_OK;
+ }
+
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ OwningNonNull<Text> textNode = *mTextNode;
+
+ editorBase->DoReplaceText(textNode, mOffset, mStringToBeReplaced.Length(),
+ mStringToInsert, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("EditorBase::DoReplaceText() failed");
+ return error.StealNSResult();
+ }
+ // XXX What should we do if mutation event listener changed the node?
+ editorBase->RangeUpdaterRef().SelAdjReplaceText(textNode, mOffset,
+ mStringToBeReplaced.Length(),
+ mStringToInsert.Length());
+
+ if (!editorBase->AllowsTransactionsToChangeSelection()) {
+ return NS_OK;
+ }
+
+ // XXX Should we stop setting selection when mutation event listener
+ // modifies the text node?
+ editorBase->CollapseSelectionTo(SuggestPointToPutCaret<EditorRawDOMPoint>(),
+ error);
+ if (MOZ_UNLIKELY(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_ASSERTION(!error.Failed(),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/ReplaceTextTransaction.h b/editor/libeditor/ReplaceTextTransaction.h
new file mode 100644
index 0000000000..f7bfba5dbe
--- /dev/null
+++ b/editor/libeditor/ReplaceTextTransaction.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ReplaceTextTransaction_h
+#define ReplaceTextTransaction_h
+
+#include "EditorBase.h"
+#include "EditorDOMPoint.h"
+#include "EditorForwards.h"
+#include "EditTransactionBase.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/Text.h"
+#include "nsDebug.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+class ReplaceTextTransaction final : public EditTransactionBase {
+ private:
+ ReplaceTextTransaction(EditorBase& aEditorBase,
+ const nsAString& aStringToInsert, dom::Text& aTextNode,
+ uint32_t aStartOffset, uint32_t aLength)
+ : mEditorBase(&aEditorBase),
+ mTextNode(&aTextNode),
+ mStringToInsert(aStringToInsert),
+ mOffset(aStartOffset) {
+ if (aLength) {
+ IgnoredErrorResult ignoredError;
+ mTextNode->SubstringData(mOffset, aLength, mStringToBeReplaced,
+ ignoredError);
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "Failed to initialize ReplaceTextTransaction::mStringToBeReplaced, "
+ "but ignored");
+ }
+ }
+
+ virtual ~ReplaceTextTransaction() = default;
+
+ public:
+ static already_AddRefed<ReplaceTextTransaction> Create(
+ EditorBase& aEditorBase, const nsAString& aStringToInsert,
+ dom::Text& aTextNode, uint32_t aStartOffset, uint32_t aLength) {
+ MOZ_ASSERT(aLength > 0, "Use InsertTextTransaction instead");
+ MOZ_ASSERT(!aStringToInsert.IsEmpty(), "Use DeleteTextTransaction instead");
+ MOZ_ASSERT(aTextNode.Length() >= aStartOffset);
+ MOZ_ASSERT(aTextNode.Length() >= aStartOffset + aLength);
+
+ RefPtr<ReplaceTextTransaction> transaction = new ReplaceTextTransaction(
+ aEditorBase, aStringToInsert, aTextNode, aStartOffset, aLength);
+ return transaction.forget();
+ }
+
+ ReplaceTextTransaction() = delete;
+ ReplaceTextTransaction(const ReplaceTextTransaction&) = delete;
+ ReplaceTextTransaction& operator=(const ReplaceTextTransaction&) = delete;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ReplaceTextTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(ReplaceTextTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() final;
+
+ template <typename EditorDOMPointType>
+ EditorDOMPointType SuggestPointToPutCaret() const {
+ if (NS_WARN_IF(!mTextNode)) {
+ return EditorDOMPointType();
+ }
+ return EditorDOMPointType(mTextNode, mOffset + mStringToInsert.Length());
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const ReplaceTextTransaction& aTransaction);
+
+ private:
+ RefPtr<EditorBase> mEditorBase;
+ RefPtr<dom::Text> mTextNode;
+
+ nsString mStringToInsert;
+ nsString mStringToBeReplaced;
+
+ uint32_t mOffset;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef ReplaceTextTransaction_
diff --git a/editor/libeditor/SelectionState.cpp b/editor/libeditor/SelectionState.cpp
new file mode 100644
index 0000000000..f582e2434c
--- /dev/null
+++ b/editor/libeditor/SelectionState.cpp
@@ -0,0 +1,591 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SelectionState.h"
+
+#include "AutoRangeArray.h" // for AutoRangeArray
+#include "EditorUtils.h" // for EditorUtils, AutoRangeArray
+#include "ErrorList.h"
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
+#include "mozilla/IntegerRange.h" // for IntegerRange
+#include "mozilla/Likely.h" // For MOZ_LIKELY and MOZ_UNLIKELY
+#include "mozilla/RangeUtils.h" // for RangeUtils
+#include "mozilla/dom/RangeBinding.h"
+#include "mozilla/dom/Selection.h" // for Selection
+#include "nsAString.h" // for nsAString::Length
+#include "nsCycleCollectionParticipant.h"
+#include "nsDebug.h" // for NS_WARNING, etc.
+#include "nsError.h" // for NS_OK, etc.
+#include "nsIContent.h" // for nsIContent
+#include "nsISupportsImpl.h" // for nsRange::Release
+#include "nsRange.h" // for nsRange
+
+namespace mozilla {
+
+using namespace dom;
+
+/*****************************************************************************
+ * mozilla::RangeItem
+ *****************************************************************************/
+
+nsINode* RangeItem::GetRoot() const {
+ if (MOZ_UNLIKELY(!IsPositioned())) {
+ return nullptr;
+ }
+ nsINode* rootNode = RangeUtils::ComputeRootNode(mStartContainer);
+ if (mStartContainer == mEndContainer) {
+ return rootNode;
+ }
+ return MOZ_LIKELY(rootNode == RangeUtils::ComputeRootNode(mEndContainer))
+ ? rootNode
+ : nullptr;
+}
+
+/******************************************************************************
+ * mozilla::SelectionState
+ *
+ * Class for recording selection info. Stores selection as collection of
+ * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
+ * ranges since dom gravity will possibly change the ranges.
+ ******************************************************************************/
+
+template nsresult RangeUpdater::SelAdjCreateNode(const EditorDOMPoint& aPoint);
+template nsresult RangeUpdater::SelAdjCreateNode(
+ const EditorRawDOMPoint& aPoint);
+template nsresult RangeUpdater::SelAdjInsertNode(const EditorDOMPoint& aPoint);
+template nsresult RangeUpdater::SelAdjInsertNode(
+ const EditorRawDOMPoint& aPoint);
+
+SelectionState::SelectionState(const AutoRangeArray& aRanges)
+ : mDirection(aRanges.GetDirection()) {
+ mArray.SetCapacity(aRanges.Ranges().Length());
+ for (const OwningNonNull<nsRange>& range : aRanges.Ranges()) {
+ RefPtr<RangeItem> rangeItem = new RangeItem();
+ rangeItem->StoreRange(range);
+ mArray.AppendElement(std::move(rangeItem));
+ }
+}
+
+void SelectionState::SaveSelection(Selection& aSelection) {
+ // if we need more items in the array, new them
+ if (mArray.Length() < aSelection.RangeCount()) {
+ for (uint32_t i = mArray.Length(); i < aSelection.RangeCount(); i++) {
+ mArray.AppendElement();
+ mArray[i] = new RangeItem();
+ }
+ } else if (mArray.Length() > aSelection.RangeCount()) {
+ // else if we have too many, delete them
+ mArray.TruncateLength(aSelection.RangeCount());
+ }
+
+ // now store the selection ranges
+ const uint32_t rangeCount = aSelection.RangeCount();
+ for (const uint32_t i : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(aSelection.RangeCount() == rangeCount);
+ const nsRange* range = aSelection.GetRangeAt(i);
+ MOZ_ASSERT(range);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!range))) {
+ continue;
+ }
+ mArray[i]->StoreRange(*range);
+ }
+
+ mDirection = aSelection.GetDirection();
+}
+
+nsresult SelectionState::RestoreSelection(Selection& aSelection) {
+ // clear out selection
+ IgnoredErrorResult ignoredError;
+ aSelection.RemoveAllRanges(ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "Selection::RemoveAllRanges() failed, but ignored");
+
+ aSelection.SetDirection(mDirection);
+
+ ErrorResult error;
+ const CopyableAutoTArray<RefPtr<RangeItem>, 10> rangeItems(mArray);
+ for (const RefPtr<RangeItem>& rangeItem : rangeItems) {
+ RefPtr<nsRange> range = rangeItem->GetRange();
+ if (!range) {
+ NS_WARNING("RangeItem::GetRange() failed");
+ return NS_ERROR_FAILURE;
+ }
+ aSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, error);
+ if (error.Failed()) {
+ NS_WARNING(
+ "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed");
+ return error.StealNSResult();
+ }
+ }
+ return NS_OK;
+}
+
+void SelectionState::ApplyTo(AutoRangeArray& aRanges) {
+ aRanges.RemoveAllRanges();
+ aRanges.SetDirection(mDirection);
+ for (const RefPtr<RangeItem>& rangeItem : mArray) {
+ RefPtr<nsRange> range = rangeItem->GetRange();
+ if (MOZ_UNLIKELY(!range)) {
+ continue;
+ }
+ aRanges.Ranges().AppendElement(std::move(range));
+ }
+}
+
+bool SelectionState::Equals(const SelectionState& aOther) const {
+ if (mArray.Length() != aOther.mArray.Length()) {
+ return false;
+ }
+ if (mArray.IsEmpty()) {
+ return false; // XXX Why?
+ }
+ if (mDirection != aOther.mDirection) {
+ return false;
+ }
+
+ for (uint32_t i : IntegerRange(mArray.Length())) {
+ if (NS_WARN_IF(!mArray[i]) || NS_WARN_IF(!aOther.mArray[i]) ||
+ !mArray[i]->Equals(*aOther.mArray[i])) {
+ return false;
+ }
+ }
+ // if we got here, they are equal
+ return true;
+}
+
+/******************************************************************************
+ * mozilla::RangeUpdater
+ *
+ * Class for updating nsRanges in response to editor actions.
+ ******************************************************************************/
+
+RangeUpdater::RangeUpdater() : mLocked(false) {}
+
+void RangeUpdater::RegisterRangeItem(RangeItem& aRangeItem) {
+ if (mArray.Contains(&aRangeItem)) {
+ NS_ERROR("tried to register an already registered range");
+ return; // don't register it again. It would get doubly adjusted.
+ }
+ mArray.AppendElement(&aRangeItem);
+}
+
+void RangeUpdater::DropRangeItem(RangeItem& aRangeItem) {
+ NS_WARNING_ASSERTION(
+ mArray.Contains(&aRangeItem),
+ "aRangeItem is not in the range, but tried to removed from it");
+ mArray.RemoveElement(&aRangeItem);
+}
+
+void RangeUpdater::RegisterSelectionState(SelectionState& aSelectionState) {
+ for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) {
+ if (NS_WARN_IF(!rangeItem)) {
+ continue;
+ }
+ RegisterRangeItem(*rangeItem);
+ }
+}
+
+void RangeUpdater::DropSelectionState(SelectionState& aSelectionState) {
+ for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) {
+ if (NS_WARN_IF(!rangeItem)) {
+ continue;
+ }
+ DropRangeItem(*rangeItem);
+ }
+}
+
+// gravity methods:
+
+template <typename PT, typename CT>
+nsresult RangeUpdater::SelAdjCreateNode(
+ const EditorDOMPointBase<PT, CT>& aPoint) {
+ if (mLocked) {
+ // lock set by Will/DidReplaceParent, etc...
+ return NS_OK;
+ }
+ if (mArray.IsEmpty()) {
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!aPoint.IsSetAndValid())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ for (RefPtr<RangeItem>& rangeItem : mArray) {
+ if (NS_WARN_IF(!rangeItem)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (rangeItem->mStartContainer == aPoint.GetContainer() &&
+ rangeItem->mStartOffset > aPoint.Offset()) {
+ rangeItem->mStartOffset++;
+ }
+ if (rangeItem->mEndContainer == aPoint.GetContainer() &&
+ rangeItem->mEndOffset > aPoint.Offset()) {
+ rangeItem->mEndOffset++;
+ }
+ }
+ return NS_OK;
+}
+
+template <typename PT, typename CT>
+nsresult RangeUpdater::SelAdjInsertNode(
+ const EditorDOMPointBase<PT, CT>& aPoint) {
+ nsresult rv = SelAdjCreateNode(aPoint);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "RangeUpdater::SelAdjCreateNode() failed");
+ return rv;
+}
+
+void RangeUpdater::SelAdjDeleteNode(nsINode& aNodeToDelete) {
+ if (mLocked) {
+ // lock set by Will/DidReplaceParent, etc...
+ return;
+ }
+
+ if (mArray.IsEmpty()) {
+ return;
+ }
+
+ EditorRawDOMPoint atNodeToDelete(&aNodeToDelete);
+ NS_ASSERTION(atNodeToDelete.IsSetAndValid(),
+ "aNodeToDelete must be an orphan node or this is called "
+ "during mutation");
+ // check for range endpoints that are after aNodeToDelete and in the same
+ // parent
+ for (RefPtr<RangeItem>& rangeItem : mArray) {
+ MOZ_ASSERT(rangeItem);
+
+ if (rangeItem->mStartContainer == atNodeToDelete.GetContainer() &&
+ rangeItem->mStartOffset > atNodeToDelete.Offset()) {
+ rangeItem->mStartOffset--;
+ }
+ if (rangeItem->mEndContainer == atNodeToDelete.GetContainer() &&
+ rangeItem->mEndOffset > atNodeToDelete.Offset()) {
+ rangeItem->mEndOffset--;
+ }
+
+ // check for range endpoints that are in aNodeToDelete
+ if (rangeItem->mStartContainer == &aNodeToDelete) {
+ rangeItem->mStartContainer = atNodeToDelete.GetContainer();
+ rangeItem->mStartOffset = atNodeToDelete.Offset();
+ }
+ if (rangeItem->mEndContainer == &aNodeToDelete) {
+ rangeItem->mEndContainer = atNodeToDelete.GetContainer();
+ rangeItem->mEndOffset = atNodeToDelete.Offset();
+ }
+
+ // check for range endpoints that are in descendants of aNodeToDelete
+ bool updateEndBoundaryToo = false;
+ if (EditorUtils::IsDescendantOf(*rangeItem->mStartContainer,
+ aNodeToDelete)) {
+ updateEndBoundaryToo =
+ rangeItem->mStartContainer == rangeItem->mEndContainer;
+ rangeItem->mStartContainer = atNodeToDelete.GetContainer();
+ rangeItem->mStartOffset = atNodeToDelete.Offset();
+ }
+
+ // avoid having to call IsDescendantOf() for common case of range startnode
+ // == range endnode.
+ if (updateEndBoundaryToo ||
+ EditorUtils::IsDescendantOf(*rangeItem->mEndContainer, aNodeToDelete)) {
+ rangeItem->mEndContainer = atNodeToDelete.GetContainer();
+ rangeItem->mEndOffset = atNodeToDelete.Offset();
+ }
+ }
+}
+
+nsresult RangeUpdater::SelAdjSplitNode(nsIContent& aOriginalContent,
+ uint32_t aSplitOffset,
+ nsIContent& aNewContent) {
+ if (mLocked) {
+ // lock set by Will/DidReplaceParent, etc...
+ return NS_OK;
+ }
+
+ if (mArray.IsEmpty()) {
+ return NS_OK;
+ }
+
+ EditorRawDOMPoint atNewNode(&aNewContent);
+ if (NS_WARN_IF(!atNewNode.IsSetAndValid())) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+
+ auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer,
+ uint32_t& aOffset) -> void {
+ if (aContainer == atNewNode.GetContainer()) {
+ // When we create a right node, we insert it after the left node.
+ // In this case,
+ // - `{}<left/>` should become `{}<left/><right/>` (0 -> 0)
+ // - `<left/>{}` should become `<left/><right/>{}` (1 -> 2)
+ // - `{<left/>}` should become `{<left/><right/>}` (0 -> 0, 1 -> 2}
+ // Therefore, we need to increate the offset only when the offset equals
+ // or is larger than the offset at the right node.
+ if (aOffset >= atNewNode.Offset()) {
+ aOffset++;
+ }
+ }
+ // If point is in the range which are moved from aOriginalContent to
+ // aNewContent, we need to change its container to aNewContent and may need
+ // to adjust the offset. If point is in the range which are not moved from
+ // aOriginalContent, we may need to adjust the offset.
+ if (aContainer != &aOriginalContent) {
+ return;
+ }
+ if (aOffset >= aSplitOffset) {
+ aContainer = &aNewContent;
+ aOffset -= aSplitOffset;
+ }
+ };
+
+ for (RefPtr<RangeItem>& rangeItem : mArray) {
+ if (NS_WARN_IF(!rangeItem)) {
+ return NS_ERROR_FAILURE;
+ }
+ AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset);
+ AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset);
+ }
+ return NS_OK;
+}
+
+nsresult RangeUpdater::SelAdjJoinNodes(
+ const EditorRawDOMPoint& aStartOfRightContent,
+ const nsIContent& aRemovedContent,
+ const EditorDOMPoint& aOldPointAtRightContent) {
+ MOZ_ASSERT(aStartOfRightContent.IsSetAndValid());
+ MOZ_ASSERT(aOldPointAtRightContent.IsSet()); // Invalid point in most cases
+ MOZ_ASSERT(aOldPointAtRightContent.HasOffset());
+
+ if (mLocked) {
+ // lock set by Will/DidReplaceParent, etc...
+ return NS_OK;
+ }
+
+ if (mArray.IsEmpty()) {
+ return NS_OK;
+ }
+
+ auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer,
+ uint32_t& aOffset) -> void {
+ // FYI: Typically, containers of aOldPointAtRightContent and
+ // aStartOfRightContent are same. They are different when one of the
+ // node was moved to somewhere and they are joined by undoing splitting
+ // a node.
+ if (aContainer == &aRemovedContent) {
+ // If the point is in the removed content, move the point to the new
+ // point in the joined node. If left node content is moved into
+ // right node, the offset should be same. Otherwise, we need to advance
+ // the offset to length of the removed content.
+ aContainer = aStartOfRightContent.GetContainer();
+ aOffset += aStartOfRightContent.Offset();
+ }
+ // TODO: If aOldPointAtRightContent.GetContainer() was in aRemovedContent,
+ // we fail to adjust container and offset here because we need to
+ // make point to where aRemoveContent was. However, collecting all
+ // ancestors of the right content may be expensive. What's the best
+ // approach to fix this?
+ else if (aContainer == aOldPointAtRightContent.GetContainer()) {
+ // If the point is in common parent of joined content nodes and it
+ // pointed after the right content node, decrease the offset.
+ if (aOffset > aOldPointAtRightContent.Offset()) {
+ aOffset--;
+ }
+ // If it pointed the right content node, adjust it to point ex-first
+ // content of the right node.
+ else if (aOffset == aOldPointAtRightContent.Offset()) {
+ aContainer = aStartOfRightContent.GetContainer();
+ aOffset = aStartOfRightContent.Offset();
+ }
+ }
+ };
+
+ for (RefPtr<RangeItem>& rangeItem : mArray) {
+ if (NS_WARN_IF(!rangeItem)) {
+ return NS_ERROR_FAILURE;
+ }
+ AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset);
+ AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset);
+ }
+
+ return NS_OK;
+}
+
+void RangeUpdater::SelAdjReplaceText(const Text& aTextNode, uint32_t aOffset,
+ uint32_t aReplacedLength,
+ uint32_t aInsertedLength) {
+ if (mLocked) {
+ // lock set by Will/DidReplaceParent, etc...
+ return;
+ }
+
+ // First, adjust selection for insertion because when offset is in the
+ // replaced range, it's adjusted to aOffset and never modified by the
+ // insertion if we adjust selection for deletion first.
+ SelAdjInsertText(aTextNode, aOffset, aInsertedLength);
+
+ // Then, adjust selection for deletion.
+ SelAdjDeleteText(aTextNode, aOffset, aReplacedLength);
+}
+
+void RangeUpdater::SelAdjInsertText(const Text& aTextNode, uint32_t aOffset,
+ uint32_t aInsertedLength) {
+ if (mLocked) {
+ // lock set by Will/DidReplaceParent, etc...
+ return;
+ }
+
+ for (RefPtr<RangeItem>& rangeItem : mArray) {
+ MOZ_ASSERT(rangeItem);
+
+ if (rangeItem->mStartContainer == &aTextNode &&
+ rangeItem->mStartOffset > aOffset) {
+ rangeItem->mStartOffset += aInsertedLength;
+ }
+ if (rangeItem->mEndContainer == &aTextNode &&
+ rangeItem->mEndOffset > aOffset) {
+ rangeItem->mEndOffset += aInsertedLength;
+ }
+ }
+}
+
+void RangeUpdater::SelAdjDeleteText(const Text& aTextNode, uint32_t aOffset,
+ uint32_t aDeletedLength) {
+ if (mLocked) {
+ // lock set by Will/DidReplaceParent, etc...
+ return;
+ }
+
+ for (RefPtr<RangeItem>& rangeItem : mArray) {
+ MOZ_ASSERT(rangeItem);
+
+ if (rangeItem->mStartContainer == &aTextNode &&
+ rangeItem->mStartOffset > aOffset) {
+ if (rangeItem->mStartOffset >= aDeletedLength) {
+ rangeItem->mStartOffset -= aDeletedLength;
+ } else {
+ rangeItem->mStartOffset = 0;
+ }
+ }
+ if (rangeItem->mEndContainer == &aTextNode &&
+ rangeItem->mEndOffset > aOffset) {
+ if (rangeItem->mEndOffset >= aDeletedLength) {
+ rangeItem->mEndOffset -= aDeletedLength;
+ } else {
+ rangeItem->mEndOffset = 0;
+ }
+ }
+ }
+}
+
+void RangeUpdater::DidReplaceContainer(const Element& aRemovedElement,
+ Element& aInsertedElement) {
+ if (NS_WARN_IF(!mLocked)) {
+ return;
+ }
+ mLocked = false;
+
+ for (RefPtr<RangeItem>& rangeItem : mArray) {
+ if (NS_WARN_IF(!rangeItem)) {
+ return;
+ }
+
+ if (rangeItem->mStartContainer == &aRemovedElement) {
+ rangeItem->mStartContainer = &aInsertedElement;
+ }
+ if (rangeItem->mEndContainer == &aRemovedElement) {
+ rangeItem->mEndContainer = &aInsertedElement;
+ }
+ }
+}
+
+void RangeUpdater::DidRemoveContainer(const Element& aRemovedElement,
+ nsINode& aRemovedElementContainerNode,
+ uint32_t aOldOffsetOfRemovedElement,
+ uint32_t aOldChildCountOfRemovedElement) {
+ if (NS_WARN_IF(!mLocked)) {
+ return;
+ }
+ mLocked = false;
+
+ for (RefPtr<RangeItem>& rangeItem : mArray) {
+ if (NS_WARN_IF(!rangeItem)) {
+ return;
+ }
+
+ if (rangeItem->mStartContainer == &aRemovedElement) {
+ rangeItem->mStartContainer = &aRemovedElementContainerNode;
+ rangeItem->mStartOffset += aOldOffsetOfRemovedElement;
+ } else if (rangeItem->mStartContainer == &aRemovedElementContainerNode &&
+ rangeItem->mStartOffset > aOldOffsetOfRemovedElement) {
+ rangeItem->mStartOffset += aOldChildCountOfRemovedElement - 1;
+ }
+
+ if (rangeItem->mEndContainer == &aRemovedElement) {
+ rangeItem->mEndContainer = &aRemovedElementContainerNode;
+ rangeItem->mEndOffset += aOldOffsetOfRemovedElement;
+ } else if (rangeItem->mEndContainer == &aRemovedElementContainerNode &&
+ rangeItem->mEndOffset > aOldOffsetOfRemovedElement) {
+ rangeItem->mEndOffset += aOldChildCountOfRemovedElement - 1;
+ }
+ }
+}
+
+void RangeUpdater::DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset,
+ const nsINode& aNewParent, uint32_t aNewOffset) {
+ if (mLocked) {
+ // Do nothing if moving nodes is occurred while changing the container.
+ return;
+ }
+ auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aNode, uint32_t& aOffset) {
+ if (aNode == &aOldParent) {
+ // If previously pointed the moved content, it should keep pointing it.
+ if (aOffset == aOldOffset) {
+ aNode = const_cast<nsINode*>(&aNewParent);
+ aOffset = aNewOffset;
+ } else if (aOffset > aOldOffset) {
+ aOffset--;
+ }
+ return;
+ }
+ if (aNode == &aNewParent) {
+ if (aOffset > aNewOffset) {
+ aOffset++;
+ }
+ }
+ };
+ for (RefPtr<RangeItem>& rangeItem : mArray) {
+ if (NS_WARN_IF(!rangeItem)) {
+ return;
+ }
+
+ AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset);
+ AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset);
+ }
+}
+
+/******************************************************************************
+ * mozilla::RangeItem
+ *
+ * Helper struct for SelectionState. This stores range endpoints.
+ ******************************************************************************/
+
+NS_IMPL_CYCLE_COLLECTION(RangeItem, mStartContainer, mEndContainer)
+
+void RangeItem::StoreRange(const nsRange& aRange) {
+ mStartContainer = aRange.GetStartContainer();
+ mStartOffset = aRange.StartOffset();
+ mEndContainer = aRange.GetEndContainer();
+ mEndOffset = aRange.EndOffset();
+}
+
+already_AddRefed<nsRange> RangeItem::GetRange() const {
+ RefPtr<nsRange> range = nsRange::Create(
+ mStartContainer, mStartOffset, mEndContainer, mEndOffset, IgnoreErrors());
+ NS_WARNING_ASSERTION(range, "nsRange::Create() failed");
+ return range.forget();
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/SelectionState.h b/editor/libeditor/SelectionState.h
new file mode 100644
index 0000000000..704246d7c2
--- /dev/null
+++ b/editor/libeditor/SelectionState.h
@@ -0,0 +1,622 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_SelectionState_h
+#define mozilla_SelectionState_h
+
+#include "mozilla/EditorDOMPoint.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/dom/Document.h"
+#include "nsCOMPtr.h"
+#include "nsDirection.h"
+#include "nsINode.h"
+#include "nsRange.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+class nsCycleCollectionTraversalCallback;
+class nsRange;
+namespace mozilla {
+namespace dom {
+class Element;
+class Selection;
+class Text;
+} // namespace dom
+
+/**
+ * A helper struct for saving/setting ranges.
+ */
+struct RangeItem final {
+ RangeItem() : mStartOffset(0), mEndOffset(0) {}
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~RangeItem() = default;
+
+ public:
+ void StoreRange(const nsRange& aRange);
+ void StoreRange(const EditorRawDOMPoint& aStartPoint,
+ const EditorRawDOMPoint& aEndPoint) {
+ MOZ_ASSERT(aStartPoint.IsSet());
+ MOZ_ASSERT(aEndPoint.IsSet());
+ mStartContainer = aStartPoint.GetContainer();
+ mStartOffset = aStartPoint.Offset();
+ mEndContainer = aEndPoint.GetContainer();
+ mEndOffset = aEndPoint.Offset();
+ }
+ void Clear() {
+ mStartContainer = mEndContainer = nullptr;
+ mStartOffset = mEndOffset = 0;
+ }
+ already_AddRefed<nsRange> GetRange() const;
+
+ // Same as the API of dom::AbstractRange
+ [[nodiscard]] nsINode* GetRoot() const;
+ [[nodiscard]] bool Collapsed() const {
+ return mStartContainer == mEndContainer && mStartOffset == mEndOffset;
+ }
+ [[nodiscard]] bool IsPositioned() const {
+ return mStartContainer && mEndContainer;
+ }
+ [[nodiscard]] bool Equals(const RangeItem& aOther) const {
+ return mStartContainer == aOther.mStartContainer &&
+ mEndContainer == aOther.mEndContainer &&
+ mStartOffset == aOther.mStartOffset &&
+ mEndOffset == aOther.mEndOffset;
+ }
+ template <typename EditorDOMPointType = EditorDOMPoint>
+ EditorDOMPointType StartPoint() const {
+ return EditorDOMPointType(mStartContainer, mStartOffset);
+ }
+ template <typename EditorDOMPointType = EditorDOMPoint>
+ EditorDOMPointType EndPoint() const {
+ return EditorDOMPointType(mEndContainer, mEndOffset);
+ }
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem)
+
+ nsCOMPtr<nsINode> mStartContainer;
+ nsCOMPtr<nsINode> mEndContainer;
+ uint32_t mStartOffset;
+ uint32_t mEndOffset;
+};
+
+/**
+ * mozilla::SelectionState
+ *
+ * Class for recording selection info. Stores selection as collection of
+ * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
+ * ranges since dom gravity will possibly change the ranges.
+ */
+
+class SelectionState final {
+ public:
+ SelectionState() = default;
+ explicit SelectionState(const AutoRangeArray& aRanges);
+
+ /**
+ * Same as the API as dom::Selection
+ */
+ [[nodiscard]] bool IsCollapsed() const {
+ if (mArray.Length() != 1) {
+ return false;
+ }
+ return mArray[0]->Collapsed();
+ }
+
+ void RemoveAllRanges() {
+ mArray.Clear();
+ mDirection = eDirNext;
+ }
+
+ [[nodiscard]] uint32_t RangeCount() const { return mArray.Length(); }
+
+ /**
+ * Saving all ranges of aSelection.
+ */
+ void SaveSelection(dom::Selection& aSelection);
+
+ /**
+ * Setting aSelection to have all ranges stored by this instance.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
+ RestoreSelection(dom::Selection& aSelection);
+
+ /**
+ * Setting aRanges to have all ranges stored by this instance.
+ */
+ void ApplyTo(AutoRangeArray& aRanges);
+
+ /**
+ * HasOnlyCollapsedRange() returns true only when there is a positioned range
+ * which is collapsed. I.e., the selection represents a caret point.
+ */
+ [[nodiscard]] bool HasOnlyCollapsedRange() const {
+ if (mArray.Length() != 1) {
+ return false;
+ }
+ if (!mArray[0]->IsPositioned() || !mArray[0]->Collapsed()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Equals() returns true only when there are same number of ranges and
+ * all their containers and offsets are exactly same. This won't check
+ * the validity of each range with the current DOM tree.
+ */
+ [[nodiscard]] bool Equals(const SelectionState& aOther) const;
+
+ /**
+ * Returns common root node of all ranges' start and end containers.
+ * Some of them have different root nodes, this returns nullptr.
+ */
+ [[nodiscard]] nsINode* GetCommonRootNode() const {
+ nsINode* rootNode = nullptr;
+ for (const RefPtr<RangeItem>& rangeItem : mArray) {
+ nsINode* newRootNode = rangeItem->GetRoot();
+ if (!newRootNode || (rootNode && rootNode != newRootNode)) {
+ return nullptr;
+ }
+ rootNode = newRootNode;
+ }
+ return rootNode;
+ }
+
+ private:
+ CopyableAutoTArray<RefPtr<RangeItem>, 1> mArray;
+ nsDirection mDirection = eDirNext;
+
+ friend class RangeUpdater;
+ friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
+ SelectionState&, const char*,
+ uint32_t);
+ friend void ImplCycleCollectionUnlink(SelectionState&);
+};
+
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback, SelectionState& aField,
+ const char* aName, uint32_t aFlags = 0) {
+ ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags);
+}
+
+inline void ImplCycleCollectionUnlink(SelectionState& aField) {
+ ImplCycleCollectionUnlink(aField.mArray);
+}
+
+class MOZ_STACK_CLASS RangeUpdater final {
+ public:
+ RangeUpdater();
+
+ void RegisterRangeItem(RangeItem& aRangeItem);
+ void DropRangeItem(RangeItem& aRangeItem);
+ void RegisterSelectionState(SelectionState& aSelectionState);
+ void DropSelectionState(SelectionState& aSelectionState);
+
+ // editor selection gravity routines. Note that we can't always depend on
+ // DOM Range gravity to do what we want to the "real" selection. For
+ // instance, if you move a node, that corresponds to deleting it and
+ // reinserting it. DOM Range gravity will promote the selection out of the
+ // node on deletion, which is not what you want if you know you are
+ // reinserting it.
+ template <typename PT, typename CT>
+ nsresult SelAdjCreateNode(const EditorDOMPointBase<PT, CT>& aPoint);
+ template <typename PT, typename CT>
+ nsresult SelAdjInsertNode(const EditorDOMPointBase<PT, CT>& aPoint);
+ void SelAdjDeleteNode(nsINode& aNode);
+
+ /**
+ * SelAdjSplitNode() is called immediately after spliting aOriginalNode
+ * and inserted aNewContent into the DOM tree.
+ *
+ * @param aOriginalContent The node which was split.
+ * @param aSplitOffset The old offset in aOriginalContent at splitting
+ * it.
+ * @param aNewContent The new content node which was inserted into
+ * the DOM tree.
+ */
+ nsresult SelAdjSplitNode(nsIContent& aOriginalContent, uint32_t aSplitOffset,
+ nsIContent& aNewContent);
+
+ /**
+ * SelAdjJoinNodes() is called immediately after joining aRemovedContent and
+ * the container of aStartOfRightContent.
+ *
+ * @param aStartOfRightContent The container is joined content node which
+ * now has all children or text data which were
+ * in aRemovedContent. And this points where
+ * the joined position.
+ * @param aRemovedContent The removed content.
+ * @param aOldPointAtRightContent The point where the right content node was
+ * before joining them. The offset must have
+ * been initialized before the joining.
+ */
+ nsresult SelAdjJoinNodes(const EditorRawDOMPoint& aStartOfRightContent,
+ const nsIContent& aRemovedContent,
+ const EditorDOMPoint& aOldPointAtRightContent);
+ void SelAdjInsertText(const dom::Text& aTextNode, uint32_t aOffset,
+ uint32_t aInsertedLength);
+ void SelAdjDeleteText(const dom::Text& aTextNode, uint32_t aOffset,
+ uint32_t aDeletedLength);
+ void SelAdjReplaceText(const dom::Text& aTextNode, uint32_t aOffset,
+ uint32_t aReplacedLength, uint32_t aInsertedLength);
+ // the following gravity routines need will/did sandwiches, because the other
+ // gravity routines will be called inside of these sandwiches, but should be
+ // ignored.
+ void WillReplaceContainer() {
+ // XXX Isn't this possible with mutation event listener?
+ NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
+ mLocked = true;
+ }
+ void DidReplaceContainer(const dom::Element& aRemovedElement,
+ dom::Element& aInsertedElement);
+ void WillRemoveContainer() {
+ // XXX Isn't this possible with mutation event listener?
+ NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
+ mLocked = true;
+ }
+ void DidRemoveContainer(const dom::Element& aRemovedElement,
+ nsINode& aRemovedElementContainerNode,
+ uint32_t aOldOffsetOfRemovedElement,
+ uint32_t aOldChildCountOfRemovedElement);
+ void WillInsertContainer() {
+ // XXX Isn't this possible with mutation event listener?
+ NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
+ mLocked = true;
+ }
+ void DidInsertContainer() {
+ NS_WARNING_ASSERTION(mLocked, "Not locked");
+ mLocked = false;
+ }
+ void DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset,
+ const nsINode& aNewParent, uint32_t aNewOffset);
+
+ private:
+ // TODO: A lot of loop in these methods check whether each item `nullptr` or
+ // not. We should make it not nullable later.
+ nsTArray<RefPtr<RangeItem>> mArray;
+ bool mLocked;
+};
+
+/**
+ * Helper class for using SelectionState. Stack based class for doing
+ * preservation of dom points across editor actions.
+ */
+
+class MOZ_STACK_CLASS AutoTrackDOMPoint final {
+ public:
+ AutoTrackDOMPoint() = delete;
+ AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, nsCOMPtr<nsINode>* aNode,
+ uint32_t* aOffset)
+ : mRangeUpdater(aRangeUpdater),
+ mNode(aNode),
+ mOffset(aOffset),
+ mRangeItem(do_AddRef(new RangeItem())),
+ mWasConnected(aNode && (*aNode)->IsInComposedDoc()) {
+ mRangeItem->mStartContainer = *mNode;
+ mRangeItem->mEndContainer = *mNode;
+ mRangeItem->mStartOffset = *mOffset;
+ mRangeItem->mEndOffset = *mOffset;
+ mDocument = (*mNode)->OwnerDoc();
+ mRangeUpdater.RegisterRangeItem(mRangeItem);
+ }
+
+ AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, EditorDOMPoint* aPoint)
+ : mRangeUpdater(aRangeUpdater),
+ mNode(nullptr),
+ mOffset(nullptr),
+ mPoint(Some(aPoint->IsSet() ? aPoint : nullptr)),
+ mRangeItem(do_AddRef(new RangeItem())),
+ mWasConnected(aPoint && aPoint->IsInComposedDoc()) {
+ if (!aPoint->IsSet()) {
+ mIsTracking = false;
+ return; // Nothing should be tracked.
+ }
+ mRangeItem->mStartContainer = aPoint->GetContainer();
+ mRangeItem->mEndContainer = aPoint->GetContainer();
+ mRangeItem->mStartOffset = aPoint->Offset();
+ mRangeItem->mEndOffset = aPoint->Offset();
+ mDocument = aPoint->GetContainer()->OwnerDoc();
+ mRangeUpdater.RegisterRangeItem(mRangeItem);
+ }
+
+ ~AutoTrackDOMPoint() { FlushAndStopTracking(); }
+
+ void FlushAndStopTracking() {
+ if (!mIsTracking) {
+ return;
+ }
+ mIsTracking = false;
+ if (mPoint.isSome()) {
+ mRangeUpdater.DropRangeItem(mRangeItem);
+ // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
+ // and the number of times may be too many. (E.g., 1533913.html hits
+ // over 700 times!) We should just put warning instead.
+ if (NS_WARN_IF(!mRangeItem->mStartContainer)) {
+ mPoint.ref()->Clear();
+ return;
+ }
+ // If the node was removed from the original document, clear the instance
+ // since the user should not keep handling the adopted or orphan node
+ // anymore.
+ if (NS_WARN_IF(mWasConnected &&
+ !mRangeItem->mStartContainer->IsInComposedDoc()) ||
+ NS_WARN_IF(mRangeItem->mStartContainer->OwnerDoc() != mDocument)) {
+ mPoint.ref()->Clear();
+ return;
+ }
+ if (NS_WARN_IF(mRangeItem->mStartContainer->Length() <
+ mRangeItem->mStartOffset)) {
+ mPoint.ref()->SetToEndOf(mRangeItem->mStartContainer);
+ return;
+ }
+ mPoint.ref()->Set(mRangeItem->mStartContainer, mRangeItem->mStartOffset);
+ return;
+ }
+ mRangeUpdater.DropRangeItem(mRangeItem);
+ *mNode = mRangeItem->mStartContainer;
+ *mOffset = mRangeItem->mStartOffset;
+ if (!(*mNode)) {
+ return;
+ }
+ // If the node was removed from the original document, clear the instances
+ // since the user should not keep handling the adopted or orphan node
+ // anymore.
+ if (NS_WARN_IF(mWasConnected && !(*mNode)->IsInComposedDoc()) ||
+ NS_WARN_IF((*mNode)->OwnerDoc() != mDocument)) {
+ *mNode = nullptr;
+ *mOffset = 0;
+ }
+ }
+
+ void StopTracking() { mIsTracking = false; }
+
+ private:
+ RangeUpdater& mRangeUpdater;
+ // Allow tracking nsINode until nsNode is gone
+ nsCOMPtr<nsINode>* mNode;
+ uint32_t* mOffset;
+ Maybe<EditorDOMPoint*> mPoint;
+ OwningNonNull<RangeItem> mRangeItem;
+ RefPtr<dom::Document> mDocument;
+ bool mIsTracking = true;
+ bool mWasConnected;
+};
+
+class MOZ_STACK_CLASS AutoTrackDOMRange final {
+ public:
+ AutoTrackDOMRange() = delete;
+ AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMPoint* aStartPoint,
+ EditorDOMPoint* aEndPoint)
+ : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
+ mStartPointTracker.emplace(aRangeUpdater, aStartPoint);
+ mEndPointTracker.emplace(aRangeUpdater, aEndPoint);
+ }
+ AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMRange* aRange)
+ : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
+ mStartPointTracker.emplace(
+ aRangeUpdater, const_cast<EditorDOMPoint*>(&aRange->StartRef()));
+ mEndPointTracker.emplace(aRangeUpdater,
+ const_cast<EditorDOMPoint*>(&aRange->EndRef()));
+ }
+ AutoTrackDOMRange(RangeUpdater& aRangeUpdater, RefPtr<nsRange>* aRange)
+ : mStartPoint((*aRange)->StartRef()),
+ mEndPoint((*aRange)->EndRef()),
+ mRangeRefPtr(aRange),
+ mRangeOwningNonNull(nullptr) {
+ mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
+ mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
+ }
+ AutoTrackDOMRange(RangeUpdater& aRangeUpdater, OwningNonNull<nsRange>* aRange)
+ : mStartPoint((*aRange)->StartRef()),
+ mEndPoint((*aRange)->EndRef()),
+ mRangeRefPtr(nullptr),
+ mRangeOwningNonNull(aRange) {
+ mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
+ mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
+ }
+ ~AutoTrackDOMRange() { FlushAndStopTracking(); }
+
+ void FlushAndStopTracking() {
+ if (!mStartPointTracker && !mEndPointTracker) {
+ return;
+ }
+ mStartPointTracker.reset();
+ mEndPointTracker.reset();
+ if (!mRangeRefPtr && !mRangeOwningNonNull) {
+ // This must be created with EditorDOMRange or EditorDOMPoints. In the
+ // cases, destroying mStartPointTracker and mEndPointTracker has done
+ // everything which we need to do.
+ return;
+ }
+ // Otherwise, update the DOM ranges by ourselves.
+ if (mRangeRefPtr) {
+ if (!mStartPoint.IsSet() || !mEndPoint.IsSet()) {
+ (*mRangeRefPtr)->Reset();
+ return;
+ }
+ (*mRangeRefPtr)
+ ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
+ mEndPoint.ToRawRangeBoundary());
+ return;
+ }
+ if (mRangeOwningNonNull) {
+ if (!mStartPoint.IsSet() || !mEndPoint.IsSet()) {
+ (*mRangeOwningNonNull)->Reset();
+ return;
+ }
+ (*mRangeOwningNonNull)
+ ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
+ mEndPoint.ToRawRangeBoundary());
+ return;
+ }
+ }
+
+ void StopTracking() {
+ if (mStartPointTracker) {
+ mStartPointTracker->StopTracking();
+ }
+ if (mEndPointTracker) {
+ mEndPointTracker->StopTracking();
+ }
+ }
+ void StopTrackingStartBoundary() {
+ MOZ_ASSERT(!mRangeRefPtr,
+ "StopTrackingStartBoundary() is not available when tracking "
+ "RefPtr<nsRange>");
+ MOZ_ASSERT(!mRangeOwningNonNull,
+ "StopTrackingStartBoundary() is not available when tracking "
+ "OwningNonNull<nsRange>");
+ if (!mStartPointTracker) {
+ return;
+ }
+ mStartPointTracker->StopTracking();
+ }
+ void StopTrackingEndBoundary() {
+ MOZ_ASSERT(!mRangeRefPtr,
+ "StopTrackingEndBoundary() is not available when tracking "
+ "RefPtr<nsRange>");
+ MOZ_ASSERT(!mRangeOwningNonNull,
+ "StopTrackingEndBoundary() is not available when tracking "
+ "OwningNonNull<nsRange>");
+ if (!mEndPointTracker) {
+ return;
+ }
+ mEndPointTracker->StopTracking();
+ }
+
+ private:
+ Maybe<AutoTrackDOMPoint> mStartPointTracker;
+ Maybe<AutoTrackDOMPoint> mEndPointTracker;
+ EditorDOMPoint mStartPoint;
+ EditorDOMPoint mEndPoint;
+ RefPtr<nsRange>* mRangeRefPtr;
+ OwningNonNull<nsRange>* mRangeOwningNonNull;
+};
+
+/**
+ * Another helper class for SelectionState. Stack based class for doing
+ * Will/DidReplaceContainer()
+ */
+
+class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final {
+ public:
+ AutoReplaceContainerSelNotify() = delete;
+ // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
+ // for the members.
+ MOZ_CAN_RUN_SCRIPT
+ AutoReplaceContainerSelNotify(RangeUpdater& aRangeUpdater,
+ dom::Element& aOriginalElement,
+ dom::Element& aNewElement)
+ : mRangeUpdater(aRangeUpdater),
+ mOriginalElement(aOriginalElement),
+ mNewElement(aNewElement) {
+ mRangeUpdater.WillReplaceContainer();
+ }
+
+ ~AutoReplaceContainerSelNotify() {
+ mRangeUpdater.DidReplaceContainer(mOriginalElement, mNewElement);
+ }
+
+ private:
+ RangeUpdater& mRangeUpdater;
+ dom::Element& mOriginalElement;
+ dom::Element& mNewElement;
+};
+
+/**
+ * Another helper class for SelectionState. Stack based class for doing
+ * Will/DidRemoveContainer()
+ */
+
+class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final {
+ public:
+ AutoRemoveContainerSelNotify() = delete;
+ AutoRemoveContainerSelNotify(RangeUpdater& aRangeUpdater,
+ const EditorRawDOMPoint& aAtRemovingElement)
+ : mRangeUpdater(aRangeUpdater),
+ mRemovingElement(*aAtRemovingElement.GetChild()->AsElement()),
+ mParentNode(*aAtRemovingElement.GetContainer()),
+ mOffsetInParent(aAtRemovingElement.Offset()),
+ mChildCountOfRemovingElement(mRemovingElement->GetChildCount()) {
+ MOZ_ASSERT(aAtRemovingElement.IsSet());
+ mRangeUpdater.WillRemoveContainer();
+ }
+
+ ~AutoRemoveContainerSelNotify() {
+ mRangeUpdater.DidRemoveContainer(mRemovingElement, mParentNode,
+ mOffsetInParent,
+ mChildCountOfRemovingElement);
+ }
+
+ private:
+ RangeUpdater& mRangeUpdater;
+ OwningNonNull<dom::Element> mRemovingElement;
+ OwningNonNull<nsINode> mParentNode;
+ uint32_t mOffsetInParent;
+ uint32_t mChildCountOfRemovingElement;
+};
+
+/**
+ * Another helper class for SelectionState. Stack based class for doing
+ * Will/DidInsertContainer()
+ * XXX The lock state isn't useful if the edit action is triggered from
+ * a mutation event listener so that looks like that we can remove
+ * this class.
+ */
+
+class MOZ_STACK_CLASS AutoInsertContainerSelNotify final {
+ private:
+ RangeUpdater& mRangeUpdater;
+
+ public:
+ AutoInsertContainerSelNotify() = delete;
+ explicit AutoInsertContainerSelNotify(RangeUpdater& aRangeUpdater)
+ : mRangeUpdater(aRangeUpdater) {
+ mRangeUpdater.WillInsertContainer();
+ }
+
+ ~AutoInsertContainerSelNotify() { mRangeUpdater.DidInsertContainer(); }
+};
+
+/**
+ * Another helper class for SelectionState. Stack based class for doing
+ * DidMoveNode()
+ */
+
+class MOZ_STACK_CLASS AutoMoveNodeSelNotify final {
+ public:
+ AutoMoveNodeSelNotify() = delete;
+ AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater,
+ const EditorRawDOMPoint& aOldPoint,
+ const EditorRawDOMPoint& aNewPoint)
+ : mRangeUpdater(aRangeUpdater),
+ mOldParent(*aOldPoint.GetContainer()),
+ mNewParent(*aNewPoint.GetContainer()),
+ mOldOffset(aOldPoint.Offset()),
+ mNewOffset(aNewPoint.Offset()) {
+ MOZ_ASSERT(aOldPoint.IsSet());
+ MOZ_ASSERT(aNewPoint.IsSet());
+ }
+
+ ~AutoMoveNodeSelNotify() {
+ mRangeUpdater.DidMoveNode(mOldParent, mOldOffset, mNewParent, mNewOffset);
+ }
+
+ private:
+ RangeUpdater& mRangeUpdater;
+ nsINode& mOldParent;
+ nsINode& mNewParent;
+ const uint32_t mOldOffset;
+ const uint32_t mNewOffset;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_SelectionState_h
diff --git a/editor/libeditor/SplitNodeTransaction.cpp b/editor/libeditor/SplitNodeTransaction.cpp
new file mode 100644
index 0000000000..01c61bd4d4
--- /dev/null
+++ b/editor/libeditor/SplitNodeTransaction.cpp
@@ -0,0 +1,219 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SplitNodeTransaction.h"
+
+#include "EditorDOMPoint.h" // for EditorRawDOMPoint
+#include "HTMLEditHelpers.h" // for SplitNodeResult
+#include "HTMLEditor.h" // for HTMLEditor
+#include "HTMLEditorInlines.h"
+#include "HTMLEditUtils.h"
+#include "SelectionState.h" // for AutoTrackDOMPoint and RangeUpdater
+
+#include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/ToString.h"
+#include "nsAString.h"
+#include "nsDebug.h" // for NS_ASSERTION, etc.
+#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc.
+#include "nsIContent.h" // for nsIContent
+
+namespace mozilla {
+
+using namespace dom;
+
+template already_AddRefed<SplitNodeTransaction> SplitNodeTransaction::Create(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aStartOfRightContent);
+template already_AddRefed<SplitNodeTransaction> SplitNodeTransaction::Create(
+ HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aStartOfRightContent);
+
+// static
+template <typename PT, typename CT>
+already_AddRefed<SplitNodeTransaction> SplitNodeTransaction::Create(
+ HTMLEditor& aHTMLEditor,
+ const EditorDOMPointBase<PT, CT>& aStartOfRightContent) {
+ RefPtr<SplitNodeTransaction> transaction =
+ new SplitNodeTransaction(aHTMLEditor, aStartOfRightContent);
+ return transaction.forget();
+}
+
+template <typename PT, typename CT>
+SplitNodeTransaction::SplitNodeTransaction(
+ HTMLEditor& aHTMLEditor,
+ const EditorDOMPointBase<PT, CT>& aStartOfRightContent)
+ : mHTMLEditor(&aHTMLEditor),
+ mSplitContent(aStartOfRightContent.template GetContainerAs<nsIContent>()),
+ mSplitOffset(aStartOfRightContent.Offset()) {
+ // printf("SplitNodeTransaction size: %zu\n", sizeof(SplitNodeTransaction));
+ static_assert(sizeof(SplitNodeTransaction) <= 64,
+ "Transaction classes may be created a lot and may be alive "
+ "long so that keep the foot print smaller as far as possible");
+ MOZ_DIAGNOSTIC_ASSERT(aStartOfRightContent.IsInContentNode());
+ MOZ_DIAGNOSTIC_ASSERT(HTMLEditUtils::IsSplittableNode(
+ *aStartOfRightContent.template ContainerAs<nsIContent>()));
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const SplitNodeTransaction& aTransaction) {
+ aStream << "{ mParentNode=" << aTransaction.mParentNode.get();
+ if (aTransaction.mParentNode) {
+ aStream << " (" << *aTransaction.mParentNode << ")";
+ }
+ aStream << ", mNewContent=" << aTransaction.mNewContent.get();
+ if (aTransaction.mNewContent) {
+ aStream << " (" << *aTransaction.mNewContent << ")";
+ }
+ aStream << ", mSplitContent=" << aTransaction.mSplitContent.get();
+ if (aTransaction.mSplitContent) {
+ aStream << " (" << *aTransaction.mSplitContent << ")";
+ }
+ aStream << ", mSplitOffset=" << aTransaction.mSplitOffset
+ << ", mHTMLEditor=" << aTransaction.mHTMLEditor.get() << " }";
+ return aStream;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(SplitNodeTransaction, EditTransactionBase,
+ mHTMLEditor, mParentNode, mSplitContent,
+ mNewContent)
+
+NS_IMPL_ADDREF_INHERITED(SplitNodeTransaction, EditTransactionBase)
+NS_IMPL_RELEASE_INHERITED(SplitNodeTransaction, EditTransactionBase)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SplitNodeTransaction)
+NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
+
+NS_IMETHODIMP SplitNodeTransaction::DoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p SplitNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (MOZ_UNLIKELY(NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(!mSplitContent))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ MOZ_ASSERT(mSplitOffset <= mSplitContent->Length());
+
+ // Create a new node
+ IgnoredErrorResult error;
+ // Don't use .downcast directly because AsContent has an assertion we want
+ nsCOMPtr<nsINode> newNode = mSplitContent->CloneNode(false, error);
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("nsINode::CloneNode() failed");
+ return error.StealNSResult();
+ }
+ if (MOZ_UNLIKELY(NS_WARN_IF(!newNode))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mNewContent = newNode->AsContent();
+ mParentNode = mSplitContent->GetParentNode();
+ if (!mParentNode) {
+ NS_WARNING("The splitting content was an orphan node");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor;
+ const OwningNonNull<nsIContent> splittingContent = *mSplitContent;
+ // MOZ_KnownLive(*mNewContent): it's grabbed by newNode
+ Result<SplitNodeResult, nsresult> splitNodeResult = DoTransactionInternal(
+ htmlEditor, splittingContent, MOZ_KnownLive(*mNewContent), mSplitOffset);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("SplitNodeTransaction::DoTransactionInternal() failed");
+ return EditorBase::ToGenericNSResult(splitNodeResult.unwrapErr());
+ }
+ // The user should handle selection rather here.
+ splitNodeResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+Result<SplitNodeResult, nsresult> SplitNodeTransaction::DoTransactionInternal(
+ HTMLEditor& aHTMLEditor, nsIContent& aSplittingContent,
+ nsIContent& aNewContent, uint32_t aSplitOffset) {
+ if (Element* const splittingElement = Element::FromNode(aSplittingContent)) {
+ // MOZ_KnownLive(*splittingElement): aSplittingContent should be grabbed by
+ // the callers.
+ nsresult rv =
+ aHTMLEditor.MarkElementDirty(MOZ_KnownLive(*splittingElement));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::MarkElementDirty() failed, but ignored");
+ }
+
+ Result<SplitNodeResult, nsresult> splitNodeResult = aHTMLEditor.DoSplitNode(
+ EditorDOMPoint(&aSplittingContent,
+ std::min(aSplitOffset, aSplittingContent.Length())),
+ aNewContent);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::DoSplitNode() failed");
+ return splitNodeResult;
+ }
+ // When adding caret suggestion to SplitNodeResult, here didn't change
+ // selection so that just ignore it.
+ splitNodeResult.inspect().IgnoreCaretPointSuggestion();
+ return splitNodeResult;
+}
+
+NS_IMETHODIMP SplitNodeTransaction::UndoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p SplitNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (MOZ_UNLIKELY(NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(!mNewContent) ||
+ NS_WARN_IF(!mParentNode) || NS_WARN_IF(!mSplitContent) ||
+ NS_WARN_IF(mNewContent->IsBeingRemoved()))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // This assumes Do inserted the new node in front of the prior existing node
+ const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor;
+ const OwningNonNull<nsIContent> keepingContent = *mSplitContent;
+ const OwningNonNull<nsIContent> removingContent = *mNewContent;
+ EditorDOMPoint joinedPoint;
+ // Unfortunately, we cannot track joining point if moving right node content
+ // into left node since it cannot track changes from web apps and HTMLEditor
+ // never removes the content of the left node. So it should be true that
+ // we don't need to track the point in this case.
+ nsresult rv = htmlEditor->DoJoinNodes(keepingContent, removingContent);
+ if (NS_SUCCEEDED(rv)) {
+ // Adjust split offset for redo here
+ if (joinedPoint.IsSet()) {
+ mSplitOffset = joinedPoint.Offset();
+ }
+ } else {
+ NS_WARNING("HTMLEditor::DoJoinNodes() failed");
+ }
+ return rv;
+}
+
+/* Redo cannot simply resplit the right node, because subsequent transactions
+ * on the redo stack may depend on the left node existing in its previous
+ * state.
+ */
+NS_IMETHODIMP SplitNodeTransaction::RedoTransaction() {
+ MOZ_LOG(GetLogModule(), LogLevel::Info,
+ ("%p SplitNodeTransaction::%s this=%s", this, __FUNCTION__,
+ ToString(*this).c_str()));
+
+ if (MOZ_UNLIKELY(NS_WARN_IF(!mNewContent) || NS_WARN_IF(!mParentNode) ||
+ NS_WARN_IF(!mSplitContent) || NS_WARN_IF(!mHTMLEditor))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor;
+ const OwningNonNull<nsIContent> newContent = *mNewContent;
+ const OwningNonNull<nsIContent> splittingContent = *mSplitContent;
+ Result<SplitNodeResult, nsresult> splitNodeResult = DoTransactionInternal(
+ htmlEditor, splittingContent, newContent, mSplitOffset);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("SplitNodeTransaction::DoTransactionInternal() failed");
+ return EditorBase::ToGenericNSResult(splitNodeResult.unwrapErr());
+ }
+ // When adding caret suggestion to SplitNodeResult, here didn't change
+ // selection so that just ignore it.
+ splitNodeResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/SplitNodeTransaction.h b/editor/libeditor/SplitNodeTransaction.h
new file mode 100644
index 0000000000..978957446a
--- /dev/null
+++ b/editor/libeditor/SplitNodeTransaction.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SplitNodeTransaction_h
+#define SplitNodeTransaction_h
+
+#include "EditorForwards.h"
+#include "EditTransactionBase.h" // for EditorTransactionBase
+
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsIContent.h"
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS_INHERITED
+#include "nscore.h" // for NS_IMETHOD
+
+namespace mozilla {
+
+/**
+ * A transaction that splits a node into two identical nodes, with the children
+ * divided between the new nodes.
+ */
+class SplitNodeTransaction final : public EditTransactionBase {
+ private:
+ template <typename PT, typename CT>
+ SplitNodeTransaction(HTMLEditor& aHTMLEditor,
+ const EditorDOMPointBase<PT, CT>& aStartOfRightContent);
+
+ public:
+ /**
+ * Creates a transaction to create a new node identical to an existing node,
+ * and split the contents between the same point in both nodes.
+ *
+ * @param aHTMLEditor The provider of core editing operations.
+ * @param aStartOfRightContent The point to split. Its container will be
+ * split, and its preceding or following
+ * content will be moved to the new node. And
+ * the point will be start of the right node or
+ * end of the left node.
+ */
+ template <typename PT, typename CT>
+ static already_AddRefed<SplitNodeTransaction> Create(
+ HTMLEditor& aHTMLEditor,
+ const EditorDOMPointBase<PT, CT>& aStartOfRightContent);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SplitNodeTransaction,
+ EditTransactionBase)
+
+ NS_DECL_EDITTRANSACTIONBASE
+ NS_DECL_EDITTRANSACTIONBASE_GETASMETHODS_OVERRIDE(SplitNodeTransaction)
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
+
+ nsIContent* GetSplitContent() const { return mSplitContent; }
+ nsIContent* GetNewContent() const { return mNewContent; }
+ nsINode* GetParentNode() const { return mParentNode; }
+
+ // The split offset. At undoing, this is recomputed with tracking the
+ // first child of mSplitContent.
+ uint32_t SplitOffset() const { return mSplitOffset; }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const SplitNodeTransaction& aTransaction);
+
+ protected:
+ virtual ~SplitNodeTransaction() = default;
+
+ MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult> DoTransactionInternal(
+ HTMLEditor& aHTMLEditor, nsIContent& aSplittingContent,
+ nsIContent& aNewContent, uint32_t aSplitOffset);
+
+ RefPtr<HTMLEditor> mHTMLEditor;
+
+ // The node which should be parent of both mNewContent and mSplitContent.
+ nsCOMPtr<nsINode> mParentNode;
+
+ // The node we create when splitting mSplitContent.
+ nsCOMPtr<nsIContent> mNewContent;
+
+ // The content node which we split.
+ nsCOMPtr<nsIContent> mSplitContent;
+
+ // The offset where we split in mSplitContent. This is required for doing and
+ // redoing. Therefore, this is updated when undoing.
+ uint32_t mSplitOffset;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef SplitNodeTransaction_h
diff --git a/editor/libeditor/TextEditSubActionHandler.cpp b/editor/libeditor/TextEditSubActionHandler.cpp
new file mode 100644
index 0000000000..526407d24a
--- /dev/null
+++ b/editor/libeditor/TextEditSubActionHandler.cpp
@@ -0,0 +1,815 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ErrorList.h"
+#include "TextEditor.h"
+
+#include "AutoRangeArray.h"
+#include "EditAction.h"
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "HTMLEditor.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/NodeFilterBinding.h"
+#include "mozilla/dom/NodeIterator.h"
+#include "mozilla/dom/Selection.h"
+
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsCRT.h"
+#include "nsCRTGlue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIHTMLCollection.h"
+#include "nsINode.h"
+#include "nsISupports.h"
+#include "nsLiteralString.h"
+#include "nsNameSpaceManager.h"
+#include "nsPrintfCString.h"
+#include "nsTextNode.h"
+#include "nsUnicharUtils.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+#define CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY \
+ if (IsReadonly()) { \
+ return EditActionResult::CanceledResult(); \
+ }
+
+void TextEditor::OnStartToHandleTopLevelEditSubAction(
+ EditSubAction aTopLevelEditSubAction,
+ nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRv.Failed());
+
+ EditorBase::OnStartToHandleTopLevelEditSubAction(
+ aTopLevelEditSubAction, aDirectionOfTopLevelEditSubAction, aRv);
+
+ MOZ_ASSERT(GetTopLevelEditSubAction() == aTopLevelEditSubAction);
+ MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() ==
+ aDirectionOfTopLevelEditSubAction);
+
+ if (NS_WARN_IF(Destroyed())) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return;
+ }
+
+ if (aTopLevelEditSubAction == EditSubAction::eSetText) {
+ // SetText replaces all text, so spell checker handles starting from the
+ // start of new value.
+ SetSpellCheckRestartPoint(EditorDOMPoint(mRootElement, 0));
+ return;
+ }
+
+ if (aTopLevelEditSubAction == EditSubAction::eInsertText ||
+ aTopLevelEditSubAction == EditSubAction::eInsertTextComingFromIME) {
+ // For spell checker, previous selected node should be text node if
+ // possible. If anchor is root of editor, it may become invalid offset
+ // after inserting text.
+ const EditorRawDOMPoint point =
+ FindBetterInsertionPoint(EditorRawDOMPoint(SelectionRef().AnchorRef()));
+ if (point.IsSet()) {
+ SetSpellCheckRestartPoint(point);
+ return;
+ }
+ NS_WARNING("TextEditor::FindBetterInsertionPoint() failed, but ignored");
+ }
+ if (SelectionRef().AnchorRef().IsSet()) {
+ SetSpellCheckRestartPoint(EditorRawDOMPoint(SelectionRef().AnchorRef()));
+ }
+}
+
+nsresult TextEditor::OnEndHandlingTopLevelEditSubAction() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ nsresult rv;
+ while (true) {
+ if (NS_WARN_IF(Destroyed())) {
+ rv = NS_ERROR_EDITOR_DESTROYED;
+ break;
+ }
+
+ // XXX Probably, we should spellcheck again after edit action (not top-level
+ // sub-action) is handled because the ranges can be referred only by
+ // users.
+ if (NS_FAILED(rv = HandleInlineSpellCheckAfterEdit())) {
+ NS_WARNING("TextEditor::HandleInlineSpellCheckAfterEdit() failed");
+ break;
+ }
+
+ if (!IsSingleLineEditor() &&
+ NS_FAILED(rv = EnsurePaddingBRElementInMultilineEditor())) {
+ NS_WARNING(
+ "EditorBase::EnsurePaddingBRElementInMultilineEditor() failed");
+ break;
+ }
+
+ rv = EnsureCaretNotAtEndOfTextNode();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ break;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "TextEditor::EnsureCaretNotAtEndOfTextNode() failed, but ignored");
+ rv = NS_OK;
+ break;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ EditorBase::OnEndHandlingTopLevelEditSubAction();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::OnEndHandlingTopLevelEditSubAction() failed, but ignored");
+ MOZ_ASSERT(!GetTopLevelEditSubAction());
+ MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == eNone);
+ return rv;
+}
+
+nsresult TextEditor::InsertLineBreakAsSubAction() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertLineBreak, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ Result<EditActionResult, nsresult> result =
+ InsertLineFeedCharacterAtSelection();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "TextEditor::InsertLineFeedCharacterAtSelection() failed, but ignored");
+ return result.unwrapErr();
+ }
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult>
+TextEditor::InsertLineFeedCharacterAtSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsSingleLineEditor());
+
+ UndefineCaretBidiLevel();
+
+ CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY
+
+ if (mMaxTextLength >= 0) {
+ nsAutoString insertionString(u"\n"_ns);
+ Result<EditActionResult, nsresult> result =
+ MaybeTruncateInsertionStringForMaxLength(insertionString);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "TextEditor::MaybeTruncateInsertionStringForMaxLength() failed");
+ return result;
+ }
+ if (result.inspect().Handled()) {
+ // Don't return as handled since we stopped inserting the line break.
+ return EditActionResult::CanceledResult();
+ }
+ }
+
+ // if the selection isn't collapsed, delete it.
+ if (!SelectionRef().IsCollapsed()) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eNoStrip) failed");
+ return Err(rv);
+ }
+ }
+
+ const auto pointToInsert = GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsert.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ MOZ_ASSERT(pointToInsert.IsSetAndValid());
+ MOZ_ASSERT(!pointToInsert.IsContainerHTMLElement(nsGkAtoms::br));
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ // Insert a linefeed character.
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, u"\n"_ns, pointToInsert);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("TextEditor::InsertTextWithTransaction(\"\\n\") failed");
+ return insertTextResult.propagateErr();
+ }
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ EditorDOMPoint pointToPutCaret = insertTextResult.inspect().Handled()
+ ? insertTextResult.inspect()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>()
+ : pointToInsert;
+ if (NS_WARN_IF(!pointToPutCaret.IsSetAndValid())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // XXX I don't think we still need this. This must have been required when
+ // `<textarea>` was implemented with text nodes and `<br>` elements.
+ // We want the caret to stick to the content on the "right". We want the
+ // caret to stick to whatever is past the break. This is because the break is
+ // on the same line we were on, but the next content will be on the following
+ // line.
+ pointToPutCaret.SetInterlinePosition(InterlinePosition::StartOfNextLine);
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult TextEditor::EnsureCaretNotAtEndOfTextNode() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // If there is no selection ranges, we should set to the end of the editor.
+ // This is usually performed in InitEditorContentAndSelection(), however,
+ // if the editor is reframed, this may be called by
+ // OnEndHandlingTopLevelEditSubAction().
+ if (SelectionRef().RangeCount()) {
+ return NS_OK;
+ }
+
+ nsresult rv = CollapseSelectionToEndOfTextNode();
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "TextEditor::CollapseSelectionToEndOfTextNode() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "TextEditor::CollapseSelectionToEndOfTextNode() failed, but ignored");
+
+ return NS_OK;
+}
+
+void TextEditor::HandleNewLinesInStringForSingleLineEditor(
+ nsString& aString) const {
+ static const char16_t kLF = static_cast<char16_t>('\n');
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aString.FindChar(static_cast<uint16_t>('\r')) == kNotFound);
+
+ // First of all, check if aString contains '\n' since if the string
+ // does not include it, we don't need to do nothing here.
+ int32_t firstLF = aString.FindChar(kLF, 0);
+ if (firstLF == kNotFound) {
+ return;
+ }
+
+ switch (mNewlineHandling) {
+ case nsIEditor::eNewlinesReplaceWithSpaces:
+ // Default of Firefox:
+ // Strip trailing newlines first so we don't wind up with trailing spaces
+ aString.Trim(LFSTR, false, true);
+ aString.ReplaceChar(kLF, ' ');
+ break;
+ case nsIEditor::eNewlinesStrip:
+ aString.StripChar(kLF);
+ break;
+ case nsIEditor::eNewlinesPasteToFirst:
+ default: {
+ // we get first *non-empty* line.
+ int32_t offset = 0;
+ while (firstLF == offset) {
+ offset++;
+ firstLF = aString.FindChar(kLF, offset);
+ }
+ if (firstLF > 0) {
+ aString.Truncate(firstLF);
+ }
+ if (offset > 0) {
+ aString.Cut(0, offset);
+ }
+ break;
+ }
+ case nsIEditor::eNewlinesReplaceWithCommas:
+ // Default of Thunderbird:
+ aString.Trim(LFSTR, true, true);
+ aString.ReplaceChar(kLF, ',');
+ break;
+ case nsIEditor::eNewlinesStripSurroundingWhitespace: {
+ nsAutoString result;
+ uint32_t offset = 0;
+ while (offset < aString.Length()) {
+ int32_t nextLF = !offset ? firstLF : aString.FindChar(kLF, offset);
+ if (nextLF < 0) {
+ result.Append(nsDependentSubstring(aString, offset));
+ break;
+ }
+ uint32_t wsBegin = nextLF;
+ // look backwards for the first non-white-space char
+ while (wsBegin > offset && NS_IS_SPACE(aString[wsBegin - 1])) {
+ --wsBegin;
+ }
+ result.Append(nsDependentSubstring(aString, offset, wsBegin - offset));
+ offset = nextLF + 1;
+ while (offset < aString.Length() && NS_IS_SPACE(aString[offset])) {
+ ++offset;
+ }
+ }
+ aString = result;
+ break;
+ }
+ case nsIEditor::eNewlinesPasteIntact:
+ // even if we're pasting newlines, don't paste leading/trailing ones
+ aString.Trim(LFSTR, true, true);
+ break;
+ }
+}
+
+Result<EditActionResult, nsresult> TextEditor::HandleInsertText(
+ EditSubAction aEditSubAction, const nsAString& aInsertionString,
+ SelectionHandling aSelectionHandling) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText ||
+ aEditSubAction == EditSubAction::eInsertTextComingFromIME);
+ MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore,
+ aEditSubAction == EditSubAction::eInsertTextComingFromIME);
+
+ UndefineCaretBidiLevel();
+
+ nsAutoString insertionString(aInsertionString);
+ if (!aInsertionString.IsEmpty() && mMaxTextLength >= 0) {
+ Result<EditActionResult, nsresult> result =
+ MaybeTruncateInsertionStringForMaxLength(insertionString);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING(
+ "TextEditor::MaybeTruncateInsertionStringForMaxLength() failed");
+ EditActionResult unwrappedResult = result.unwrap();
+ unwrappedResult.MarkAsHandled();
+ return unwrappedResult;
+ }
+ // If we're exceeding the maxlength when composing IME, we need to clean up
+ // the composing text, so we shouldn't return early.
+ if (result.inspect().Handled() && insertionString.IsEmpty() &&
+ aEditSubAction != EditSubAction::eInsertTextComingFromIME) {
+ return EditActionResult::CanceledResult();
+ }
+ }
+
+ uint32_t start = 0;
+ if (IsPasswordEditor()) {
+ if (GetComposition() && !GetComposition()->String().IsEmpty()) {
+ start = GetComposition()->XPOffsetInTextNode();
+ } else {
+ uint32_t end = 0;
+ nsContentUtils::GetSelectionInTextControl(&SelectionRef(), GetRoot(),
+ start, end);
+ }
+ }
+
+ // if the selection isn't collapsed, delete it.
+ if (!SelectionRef().IsCollapsed() &&
+ aSelectionHandling == SelectionHandling::Delete) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eNoStrip) failed");
+ return Err(rv);
+ }
+ }
+
+ if (aInsertionString.IsEmpty() &&
+ aEditSubAction != EditSubAction::eInsertTextComingFromIME) {
+ // HACK: this is a fix for bug 19395
+ // I can't outlaw all empty insertions
+ // because IME transaction depend on them
+ // There is more work to do to make the
+ // world safe for IME.
+ return EditActionResult::CanceledResult();
+ }
+
+ // XXX Why don't we cancel here? Shouldn't we do this first?
+ CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY
+
+ MaybeDoAutoPasswordMasking();
+
+ // People have lots of different ideas about what text fields
+ // should do with multiline pastes. See bugs 21032, 23485, 23485, 50935.
+ // The six possible options are:
+ // 0. paste newlines intact
+ // 1. paste up to the first newline (default)
+ // 2. replace newlines with spaces
+ // 3. strip newlines
+ // 4. replace with commas
+ // 5. strip newlines and surrounding white-space
+ // So find out what we're expected to do:
+ if (IsSingleLineEditor()) {
+ // XXX Some callers of TextEditor::InsertTextAsAction() already make the
+ // string use only \n as a linebreaker. However, they are not hot
+ // path and nsContentUtils::PlatformToDOMLineBreaks() does nothing
+ // if the string doesn't include \r. So, let's convert linebreakers
+ // here. Note that there are too many callers of
+ // TextEditor::InsertTextAsAction(). So, it's difficult to keep
+ // maintaining all of them won't reach here without \r nor \r\n.
+ // XXX Should we handle do this before truncating the string for
+ // `maxlength`?
+ nsContentUtils::PlatformToDOMLineBreaks(insertionString);
+ HandleNewLinesInStringForSingleLineEditor(insertionString);
+ }
+
+ const auto atStartOfSelection = GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!atStartOfSelection.IsSetAndValid())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ MOZ_ASSERT(!atStartOfSelection.IsContainerHTMLElement(nsGkAtoms::br));
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
+ EditorDOMPoint compositionStartPoint =
+ GetFirstIMESelectionStartPoint<EditorDOMPoint>();
+ if (!compositionStartPoint.IsSet()) {
+ compositionStartPoint = FindBetterInsertionPoint(atStartOfSelection);
+ NS_WARNING_ASSERTION(
+ compositionStartPoint.IsSet(),
+ "TextEditor::FindBetterInsertionPoint() failed, but ignored");
+ }
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, insertionString,
+ compositionStartPoint);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("EditorBase::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ nsresult rv = insertTextResult.unwrap().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ } else {
+ MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText);
+
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, insertionString,
+ atStartOfSelection);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("EditorBase::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ if (insertTextResult.inspect().Handled()) {
+ // Make the caret attach to the inserted text, unless this text ends with
+ // a LF, in which case make the caret attach to the next line.
+ const bool endsWithLF =
+ !insertionString.IsEmpty() && insertionString.Last() == nsCRT::LF;
+ EditorDOMPoint pointToPutCaret = insertTextResult.inspect()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>();
+ pointToPutCaret.SetInterlinePosition(
+ endsWithLF ? InterlinePosition::StartOfNextLine
+ : InterlinePosition::EndOfLine);
+ MOZ_ASSERT(pointToPutCaret.IsInTextNode(),
+ "After inserting text into a text node, insertTextResult "
+ "should return a point in a text node");
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+
+ // Unmask inputted character(s) if necessary.
+ if (IsPasswordEditor() && IsMaskingPassword() && CanEchoPasswordNow()) {
+ nsresult rv = SetUnmaskRangeAndNotify(start, insertionString.Length(),
+ LookAndFeel::GetPasswordMaskDelay());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::SetUnmaskRangeAndNotify() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ return EditActionResult::HandledResult();
+}
+
+Result<EditActionResult, nsresult> TextEditor::SetTextWithoutTransaction(
+ const nsAString& aValue) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsIMEComposing());
+ MOZ_ASSERT(!IsUndoRedoEnabled());
+ MOZ_ASSERT(GetEditAction() != EditAction::eReplaceText);
+ MOZ_ASSERT(mMaxTextLength < 0);
+ MOZ_ASSERT(aValue.FindChar(static_cast<char16_t>('\r')) == kNotFound);
+
+ UndefineCaretBidiLevel();
+
+ // XXX If we're setting value, shouldn't we keep setting the new value here?
+ CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY
+
+ MaybeDoAutoPasswordMasking();
+
+ RefPtr<Element> anonymousDivElement = GetRoot();
+ RefPtr<Text> textNode =
+ Text::FromNodeOrNull(anonymousDivElement->GetFirstChild());
+ MOZ_ASSERT(textNode);
+
+ // We can use this fast path only when:
+ // - we need to insert a text node.
+ // - we need to replace content of existing text node.
+ // Additionally, for avoiding odd result, we should check whether we're in
+ // usual condition.
+ if (!IsSingleLineEditor()) {
+ // If we're a multiline text editor, i.e., <textarea>, there is a padding
+ // <br> element for empty last line followed by scrollbar/resizer elements.
+ // Otherwise, a text node is followed by them.
+ if (!textNode->GetNextSibling() ||
+ !EditorUtils::IsPaddingBRElementForEmptyLastLine(
+ *textNode->GetNextSibling())) {
+ return EditActionResult::IgnoredResult();
+ }
+ }
+
+ // XXX Password fields accept line breaks as normal characters with this code.
+ // Is this intentional?
+ nsAutoString sanitizedValue(aValue);
+ if (IsSingleLineEditor() && !IsPasswordEditor()) {
+ HandleNewLinesInStringForSingleLineEditor(sanitizedValue);
+ }
+
+ nsresult rv = SetTextNodeWithoutTransaction(sanitizedValue, *textNode);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::SetTextNodeWithoutTransaction() failed");
+ return Err(rv);
+ }
+
+ return EditActionResult::HandledResult();
+}
+
+Result<EditActionResult, nsresult> TextEditor::HandleDeleteSelection(
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aStripWrappers == nsIEditor::eNoStrip);
+
+ UndefineCaretBidiLevel();
+
+ CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY
+
+ if (IsEmpty()) {
+ return EditActionResult::CanceledResult();
+ }
+ Result<EditActionResult, nsresult> result =
+ HandleDeleteSelectionInternal(aDirectionAndAmount, nsIEditor::eNoStrip);
+ // HandleDeleteSelectionInternal() creates SelectionBatcher. Therefore,
+ // quitting from it might cause having destroyed the editor.
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ result.isOk(),
+ "TextEditor::HandleDeleteSelectionInternal(eNoStrip) failed");
+ return result;
+}
+
+Result<EditActionResult, nsresult> TextEditor::HandleDeleteSelectionInternal(
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aStripWrappers == nsIEditor::eNoStrip);
+
+ // If the current selection is empty (e.g the user presses backspace with
+ // a collapsed selection), then we want to avoid sending the selectstart
+ // event to the user, so we hide selection changes. However, we still
+ // want to send a single selectionchange event to the document, so we
+ // batch the selectionchange events, such that a single event fires after
+ // the AutoHideSelectionChanges destructor has been run.
+ SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__);
+ AutoHideSelectionChanges hideSelection(SelectionRef());
+ nsAutoScriptBlocker scriptBlocker;
+
+ if (IsPasswordEditor() && IsMaskingPassword()) {
+ MaskAllCharacters();
+ } else {
+ const auto selectionStartPoint =
+ GetFirstSelectionStartPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!selectionStartPoint.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (!SelectionRef().IsCollapsed()) {
+ nsresult rv = DeleteSelectionWithTransaction(aDirectionAndAmount,
+ nsIEditor::eNoStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionWithTransaction(eNoStrip) failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ // Test for distance between caret and text that will be deleted
+ AutoCaretBidiLevelManager bidiLevelManager(*this, aDirectionAndAmount,
+ selectionStartPoint);
+ if (MOZ_UNLIKELY(bidiLevelManager.Failed())) {
+ NS_WARNING("EditorBase::AutoCaretBidiLevelManager() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ bidiLevelManager.MaybeUpdateCaretBidiLevel(*this);
+ if (bidiLevelManager.Canceled()) {
+ return EditActionResult::CanceledResult();
+ }
+ }
+
+ AutoRangeArray rangesToDelete(SelectionRef());
+ Result<nsIEditor::EDirection, nsresult> result =
+ rangesToDelete.ExtendAnchorFocusRangeFor(*this, aDirectionAndAmount);
+ if (result.isErr()) {
+ NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
+ return result.propagateErr();
+ }
+ if (const Text* theTextNode = GetTextNode()) {
+ rangesToDelete.EnsureRangesInTextNode(*theTextNode);
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError = DeleteRangesWithTransaction(
+ result.unwrap(), nsIEditor::eNoStrip, rangesToDelete);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("EditorBase::DeleteRangesWithTransaction(eNoStrip) failed");
+ return caretPointOrError.propagateErr();
+ }
+
+ nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+
+ return EditActionResult::HandledResult();
+}
+
+Result<EditActionResult, nsresult>
+TextEditor::ComputeValueFromTextNodeAndBRElement(nsAString& aValue) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsHTMLEditor());
+
+ Element* anonymousDivElement = GetRoot();
+ if (MOZ_UNLIKELY(!anonymousDivElement)) {
+ // Don't warn this case, this is possible, e.g., 997805.html
+ aValue.Truncate();
+ return EditActionResult::HandledResult();
+ }
+
+ Text* textNode = Text::FromNodeOrNull(anonymousDivElement->GetFirstChild());
+ MOZ_ASSERT(textNode);
+
+ if (!textNode->Length()) {
+ aValue.Truncate();
+ return EditActionResult::HandledResult();
+ }
+
+ nsIContent* firstChildExceptText = textNode->GetNextSibling();
+ // If the DOM tree is unexpected, fall back to the expensive path.
+ bool isInput = IsSingleLineEditor();
+ bool isTextarea = !isInput;
+ if (NS_WARN_IF(isInput && firstChildExceptText) ||
+ NS_WARN_IF(isTextarea && !firstChildExceptText) ||
+ NS_WARN_IF(isTextarea &&
+ !EditorUtils::IsPaddingBRElementForEmptyLastLine(
+ *firstChildExceptText) &&
+ !firstChildExceptText->IsXULElement(nsGkAtoms::scrollbar))) {
+ return EditActionResult::IgnoredResult();
+ }
+
+ // Otherwise, the text data is the value.
+ textNode->GetData(aValue);
+ return EditActionResult::HandledResult();
+}
+
+Result<EditActionResult, nsresult>
+TextEditor::MaybeTruncateInsertionStringForMaxLength(
+ nsAString& aInsertionString) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(mMaxTextLength >= 0);
+
+ if (IsIMEComposing()) {
+ return EditActionResult::IgnoredResult();
+ }
+
+ // Ignore user pastes
+ switch (GetEditAction()) {
+ case EditAction::ePaste:
+ case EditAction::ePasteAsQuotation:
+ case EditAction::eDrop:
+ case EditAction::eReplaceText:
+ // EditActionPrinciple() is non-null iff the edit was requested by
+ // javascript.
+ if (!GetEditActionPrincipal()) {
+ // By now we are certain that this is a user paste, before we ignore it,
+ // lets check if the user explictly enabled truncating user pastes.
+ if (!StaticPrefs::editor_truncate_user_pastes()) {
+ return EditActionResult::IgnoredResult();
+ }
+ }
+ [[fallthrough]];
+ default:
+ break;
+ }
+
+ uint32_t currentLength = UINT32_MAX;
+ nsresult rv = GetTextLength(&currentLength);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::GetTextLength() failed");
+ return Err(rv);
+ }
+
+ uint32_t selectionStart, selectionEnd;
+ nsContentUtils::GetSelectionInTextControl(&SelectionRef(), GetRoot(),
+ selectionStart, selectionEnd);
+
+ TextComposition* composition = GetComposition();
+ const uint32_t kOldCompositionStringLength =
+ composition ? composition->String().Length() : 0;
+
+ const uint32_t kSelectionLength = selectionEnd - selectionStart;
+ // XXX This computation must be wrong. If we'll support non-collapsed
+ // selection even during composition for Korean IME, kSelectionLength
+ // is part of kOldCompositionStringLength.
+ const uint32_t kNewLength =
+ currentLength - kSelectionLength - kOldCompositionStringLength;
+ if (kNewLength >= AssertedCast<uint32_t>(mMaxTextLength)) {
+ aInsertionString.Truncate(); // Too long, we cannot accept new character.
+ return EditActionResult::HandledResult();
+ }
+
+ if (aInsertionString.Length() + kNewLength <=
+ AssertedCast<uint32_t>(mMaxTextLength)) {
+ return EditActionResult::IgnoredResult(); // Enough short string.
+ }
+
+ int32_t newInsertionStringLength = mMaxTextLength - kNewLength;
+ MOZ_ASSERT(newInsertionStringLength > 0);
+ char16_t maybeHighSurrogate =
+ aInsertionString.CharAt(newInsertionStringLength - 1);
+ char16_t maybeLowSurrogate =
+ aInsertionString.CharAt(newInsertionStringLength);
+ // Don't split the surrogate pair.
+ if (NS_IS_SURROGATE_PAIR(maybeHighSurrogate, maybeLowSurrogate)) {
+ newInsertionStringLength--;
+ }
+ // XXX What should we do if we're removing IVS but its preceding
+ // character won't be removed?
+ aInsertionString.Truncate(newInsertionStringLength);
+ return EditActionResult::HandledResult();
+}
+
+bool TextEditor::CanEchoPasswordNow() const {
+ if (!LookAndFeel::GetEchoPassword() || EchoingPasswordPrevented()) {
+ return false;
+ }
+
+ return GetEditAction() != EditAction::eDrop &&
+ GetEditAction() != EditAction::ePaste;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp
new file mode 100644
index 0000000000..2403dd1f55
--- /dev/null
+++ b/editor/libeditor/TextEditor.cpp
@@ -0,0 +1,1224 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextEditor.h"
+
+#include <algorithm>
+
+#include "EditAction.h"
+#include "EditAggregateTransaction.h"
+#include "EditorDOMPoint.h"
+#include "HTMLEditor.h"
+#include "HTMLEditUtils.h"
+#include "InternetCiter.h"
+#include "PlaceholderTransaction.h"
+#include "gfxFontUtils.h"
+
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TextServicesDocument.h"
+#include "mozilla/Try.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h"
+
+#include "nsAString.h"
+#include "nsCRT.h"
+#include "nsCaret.h"
+#include "nsCharTraits.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentCID.h"
+#include "nsContentList.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsFocusManager.h"
+#include "nsGkAtoms.h"
+#include "nsIClipboard.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsIPrincipal.h"
+#include "nsISelectionController.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITransferable.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsNameSpaceManager.h"
+#include "nsLiteralString.h"
+#include "nsPresContext.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTextFragment.h"
+#include "nsTextNode.h"
+#include "nsUnicharUtils.h"
+#include "nsXPCOM.h"
+
+class nsIOutputStream;
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
+
+template EditorDOMPoint TextEditor::FindBetterInsertionPoint(
+ const EditorDOMPoint& aPoint) const;
+template EditorRawDOMPoint TextEditor::FindBetterInsertionPoint(
+ const EditorRawDOMPoint& aPoint) const;
+
+TextEditor::TextEditor() : EditorBase(EditorBase::EditorType::Text) {
+ // printf("Size of TextEditor: %zu\n", sizeof(TextEditor));
+ static_assert(
+ sizeof(TextEditor) <= 512,
+ "TextEditor instance should be allocatable in the quantum class bins");
+}
+
+TextEditor::~TextEditor() {
+ // Remove event listeners. Note that if we had an HTML editor,
+ // it installed its own instead of these
+ RemoveEventListeners();
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(TextEditor)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TextEditor, EditorBase)
+ if (tmp->mPasswordMaskData) {
+ tmp->mPasswordMaskData->CancelTimer(PasswordMaskData::ReleaseTimer::No);
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPasswordMaskData->mTimer)
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TextEditor, EditorBase)
+ if (tmp->mPasswordMaskData) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPasswordMaskData->mTimer)
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(TextEditor, EditorBase)
+NS_IMPL_RELEASE_INHERITED(TextEditor, EditorBase)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextEditor)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END_INHERITING(EditorBase)
+
+NS_IMETHODIMP TextEditor::EndOfDocument() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv = CollapseSelectionToEndOfTextNode();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::CollapseSelectionToEndOfTextNode() failed");
+ // This is low level API for embedders and chrome script so that we can return
+ // raw error code here.
+ return rv;
+}
+
+nsresult TextEditor::CollapseSelectionToEndOfTextNode() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Element* anonymousDivElement = GetRoot();
+ if (NS_WARN_IF(!anonymousDivElement)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ RefPtr<Text> textNode =
+ Text::FromNodeOrNull(anonymousDivElement->GetFirstChild());
+ MOZ_ASSERT(textNode);
+ nsresult rv = CollapseSelectionToEndOf(*textNode);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToEndOf() failed");
+ return rv;
+}
+
+nsresult TextEditor::Init(Document& aDocument, Element& aAnonymousDivElement,
+ nsISelectionController& aSelectionController,
+ uint32_t aFlags,
+ UniquePtr<PasswordMaskData>&& aPasswordMaskData) {
+ MOZ_ASSERT(!mInitSucceeded,
+ "TextEditor::Init() called again without calling PreDestroy()?");
+ MOZ_ASSERT(!(aFlags & nsIEditor::eEditorPasswordMask) == !aPasswordMaskData);
+ mPasswordMaskData = std::move(aPasswordMaskData);
+
+ // Init the base editor
+ nsresult rv = InitInternal(aDocument, &aAnonymousDivElement,
+ aSelectionController, aFlags);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InitInternal() failed");
+ return rv;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // We set mInitSucceeded here rather than at the end of the function,
+ // since InitEditorContentAndSelection() can perform some transactions
+ // and can warn if mInitSucceeded is still false.
+ MOZ_ASSERT(!mInitSucceeded, "TextEditor::Init() shouldn't be nested");
+ mInitSucceeded = true;
+
+ rv = InitEditorContentAndSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::InitEditorContentAndSelection() failed");
+ // XXX Shouldn't we expose `NS_ERROR_EDITOR_DESTROYED` even though this
+ // is a public method?
+ mInitSucceeded = false;
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Throw away the old transaction manager if this is not the first time that
+ // we're initializing the editor.
+ ClearUndoRedo();
+ EnableUndoRedo();
+ return NS_OK;
+}
+
+nsresult TextEditor::InitEditorContentAndSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ MOZ_TRY(EnsureEmptyTextFirstChild());
+
+ // If the selection hasn't been set up yet, set it up collapsed to the end of
+ // our editable content.
+ if (!SelectionRef().RangeCount()) {
+ nsresult rv = CollapseSelectionToEndOfTextNode();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionToEndOfTextNode() failed");
+ return rv;
+ }
+ }
+
+ if (!IsSingleLineEditor()) {
+ nsresult rv = EnsurePaddingBRElementInMultilineEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::EnsurePaddingBRElementInMultilineEditor() failed");
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TextEditor::PostCreate() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = PostCreateInternal();
+
+ // Restore unmasked range if there is.
+ if (IsPasswordEditor() && !IsAllMasked()) {
+ DebugOnly<nsresult> rvIgnored =
+ SetUnmaskRangeAndNotify(UnmaskedStart(), UnmaskedLength());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRangeAndNotify() failed to "
+ "restore unmasked range, but ignored");
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::PostCreateInternal() failed");
+ return rv;
+}
+
+UniquePtr<PasswordMaskData> TextEditor::PreDestroy() {
+ if (mDidPreDestroy) {
+ return nullptr;
+ }
+
+ UniquePtr<PasswordMaskData> passwordMaskData = std::move(mPasswordMaskData);
+ if (passwordMaskData) {
+ // Disable auto-masking timer since nobody can catch the notification
+ // from the timer and canceling the unmasking.
+ passwordMaskData->CancelTimer(PasswordMaskData::ReleaseTimer::Yes);
+ // Similary, keeping preventing echoing password temporarily across
+ // TextEditor instances is hard. So, we should forget it.
+ passwordMaskData->mEchoingPasswordPrevented = false;
+ }
+
+ PreDestroyInternal();
+
+ return passwordMaskData;
+}
+
+nsresult TextEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) {
+ // NOTE: When you change this method, you should also change:
+ // * editor/libeditor/tests/test_texteditor_keyevent_handling.html
+ // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
+ //
+ // And also when you add new key handling, you need to change the subclass's
+ // HandleKeyPressEvent()'s switch statement.
+
+ if (NS_WARN_IF(!aKeyboardEvent)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (IsReadonly()) {
+ HandleKeyPressEventInReadOnlyMode(*aKeyboardEvent);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress,
+ "HandleKeyPressEvent gets non-keypress event");
+
+ switch (aKeyboardEvent->mKeyCode) {
+ case NS_VK_META:
+ case NS_VK_WIN:
+ case NS_VK_SHIFT:
+ case NS_VK_CONTROL:
+ case NS_VK_ALT:
+ // FYI: This shouldn't occur since modifier key shouldn't cause eKeyPress
+ // event.
+ aKeyboardEvent->PreventDefault();
+ return NS_OK;
+
+ case NS_VK_BACK:
+ case NS_VK_DELETE:
+ case NS_VK_TAB: {
+ nsresult rv = EditorBase::HandleKeyPressEvent(aKeyboardEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::HandleKeyPressEvent() failed");
+ return rv;
+ }
+ case NS_VK_RETURN: {
+ if (!aKeyboardEvent->IsInputtingLineBreak()) {
+ return NS_OK;
+ }
+ if (!IsSingleLineEditor()) {
+ aKeyboardEvent->PreventDefault();
+ }
+ // We need to dispatch "beforeinput" event at least even if we're a
+ // single line text editor.
+ nsresult rv = InsertLineBreakAsAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::InsertLineBreakAsAction() failed");
+ return rv;
+ }
+ }
+
+ if (!aKeyboardEvent->IsInputtingText()) {
+ // we don't PreventDefault() here or keybindings like control-x won't work
+ return NS_OK;
+ }
+ aKeyboardEvent->PreventDefault();
+ // If we dispatch 2 keypress events for a surrogate pair and we set only
+ // first `.key` value to the surrogate pair, the preceding one has it and the
+ // other has empty string. In this case, we should handle only the first one
+ // with the key value.
+ if (!StaticPrefs::dom_event_keypress_dispatch_once_per_surrogate_pair() &&
+ !StaticPrefs::dom_event_keypress_key_allow_lone_surrogate() &&
+ aKeyboardEvent->mKeyValue.IsEmpty() &&
+ IS_SURROGATE(aKeyboardEvent->mCharCode)) {
+ return NS_OK;
+ }
+ // Our widget shouldn't set `\r` to `mKeyValue`, but it may be synthesized
+ // keyboard event and its value may be `\r`. In such case, we should treat
+ // it as `\n` for the backward compatibility because we stopped converting
+ // `\r` and `\r\n` to `\n` at getting `HTMLInputElement.value` and
+ // `HTMLTextAreaElement.value` for the performance (i.e., we don't need to
+ // take care in `HTMLEditor`).
+ nsAutoString str(aKeyboardEvent->mKeyValue);
+ if (str.IsEmpty()) {
+ MOZ_ASSERT(aKeyboardEvent->mCharCode <= 0xFFFF,
+ "Non-BMP character needs special handling");
+ str.Assign(aKeyboardEvent->mCharCode == nsCRT::CR
+ ? static_cast<char16_t>(nsCRT::LF)
+ : static_cast<char16_t>(aKeyboardEvent->mCharCode));
+ } else {
+ MOZ_ASSERT(str.Find(u"\r\n"_ns) == kNotFound,
+ "This assumes that typed text does not include CRLF");
+ str.ReplaceChar('\r', '\n');
+ }
+ nsresult rv = OnInputText(str);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed");
+ return rv;
+}
+
+NS_IMETHODIMP TextEditor::InsertLineBreak() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLineBreak);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (NS_WARN_IF(IsSingleLineEditor())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertLineBreakAsSubAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::InsertLineBreakAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::InsertLineBreakAsAction(nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLineBreak,
+ aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (IsSingleLineEditor()) {
+ return NS_OK;
+ }
+
+ // XXX This may be called by execCommand() with "insertParagraph".
+ // In such case, naming the transaction "TypingTxnName" is odd.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+ rv = InsertLineBreakAsSubAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertLineBreakAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::SetTextAsAction(
+ const nsAString& aString,
+ AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound);
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetText,
+ aPrincipal);
+ if (aAllowBeforeInputEventCancelable == AllowBeforeInputEventCancelable::No) {
+ editActionData.MakeBeforeInputEventNonCancelable();
+ }
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = SetTextAsSubAction(aString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetTextAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::SetTextAsSubAction(const nsAString& aString) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(mPlaceholderBatch);
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSetText, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ if (!IsIMEComposing() && !IsUndoRedoEnabled() &&
+ GetEditAction() != EditAction::eReplaceText && mMaxTextLength < 0) {
+ Result<EditActionResult, nsresult> result =
+ SetTextWithoutTransaction(aString);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("TextEditor::SetTextWithoutTransaction() failed");
+ return result.unwrapErr();
+ }
+ if (!result.inspect().Ignored()) {
+ return NS_OK;
+ }
+ }
+
+ {
+ // Note that do not notify selectionchange caused by selecting all text
+ // because it's preparation of our delete implementation so web apps
+ // shouldn't receive such selectionchange before the first mutation.
+ AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__);
+
+ // XXX We should make ReplaceSelectionAsSubAction() take range. Then,
+ // we can saving the expensive cost of modifying `Selection` here.
+ if (NS_SUCCEEDED(SelectEntireDocument())) {
+ DebugOnly<nsresult> rvIgnored = ReplaceSelectionAsSubAction(aString);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::ReplaceSelectionAsSubAction() failed, but ignored");
+ }
+ }
+
+ // Destroying AutoUpdateViewBatch may cause destroying us.
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+already_AddRefed<Element> TextEditor::GetInputEventTargetElement() const {
+ RefPtr<Element> target = Element::FromEventTargetOrNull(mEventTarget);
+ return target.forget();
+}
+
+bool TextEditor::IsEmpty() const {
+ // Even if there is no padding <br> element for empty editor, we should be
+ // detected as empty editor if all the children are text nodes and these
+ // have no content.
+ Element* anonymousDivElement = GetRoot();
+ if (!anonymousDivElement) {
+ return true; // Don't warn it, this is possible, e.g., 997805.html
+ }
+
+ MOZ_ASSERT(anonymousDivElement->GetFirstChild() &&
+ anonymousDivElement->GetFirstChild()->IsText());
+
+ // Only when there is non-empty text node, we are not empty.
+ return !anonymousDivElement->GetFirstChild()->Length();
+}
+
+NS_IMETHODIMP TextEditor::GetTextLength(uint32_t* aCount) {
+ MOZ_ASSERT(aCount);
+
+ // initialize out params
+ *aCount = 0;
+
+ // special-case for empty document, to account for the padding <br> element
+ // for empty editor.
+ // XXX This should be overridden by `HTMLEditor` and we should return the
+ // first text node's length from `TextEditor` instead. The following
+ // code is too expensive.
+ if (IsEmpty()) {
+ return NS_OK;
+ }
+
+ Element* rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t totalLength = 0;
+ PostContentIterator postOrderIter;
+ DebugOnly<nsresult> rvIgnored = postOrderIter.Init(rootElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "PostContentIterator::Init() failed, but ignored");
+ EditorType editorType = GetEditorType();
+ for (; !postOrderIter.IsDone(); postOrderIter.Next()) {
+ nsINode* currentNode = postOrderIter.GetCurrentNode();
+ if (currentNode && currentNode->IsText() &&
+ EditorUtils::IsEditableContent(*currentNode->AsText(), editorType)) {
+ totalLength += currentNode->Length();
+ }
+ }
+
+ *aCount = totalLength;
+ return NS_OK;
+}
+
+bool TextEditor::IsCopyToClipboardAllowedInternal() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ if (!EditorBase::IsCopyToClipboardAllowedInternal()) {
+ return false;
+ }
+
+ if (!IsSingleLineEditor() || !IsPasswordEditor() ||
+ NS_WARN_IF(!mPasswordMaskData)) {
+ return true;
+ }
+
+ // If we're a password editor, we should allow selected text to be copied
+ // to the clipboard only when selection range is in unmasked range.
+ if (IsAllMasked() || IsMaskingPassword() || !UnmaskedLength()) {
+ return false;
+ }
+
+ // If there are 2 or more ranges, we don't allow to copy/cut for now since
+ // we need to check whether all ranges are in unmasked range or not.
+ // Anyway, such operation in password field does not make sense.
+ if (SelectionRef().RangeCount() > 1) {
+ return false;
+ }
+
+ uint32_t selectionStart = 0, selectionEnd = 0;
+ nsContentUtils::GetSelectionInTextControl(&SelectionRef(), mRootElement,
+ selectionStart, selectionEnd);
+ return UnmaskedStart() <= selectionStart && UnmaskedEnd() >= selectionEnd;
+}
+
+nsresult TextEditor::HandlePasteAsQuotation(
+ AutoEditActionDataSetter& aEditActionData, int32_t aClipboardType) {
+ MOZ_ASSERT(aClipboardType == nsIClipboard::kGlobalClipboard ||
+ aClipboardType == nsIClipboard::kSelectionClipboard);
+ if (NS_WARN_IF(!GetDocument())) {
+ return NS_OK;
+ }
+
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard =
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return rv;
+ }
+
+ // XXX Why don't we dispatch ePaste event here?
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ Result<nsCOMPtr<nsITransferable>, nsresult> maybeTransferable =
+ EditorUtils::CreateTransferableForPlainText(*GetDocument());
+ if (maybeTransferable.isErr()) {
+ NS_WARNING("EditorUtils::CreateTransferableForPlainText() failed");
+ return maybeTransferable.unwrapErr();
+ }
+ nsCOMPtr<nsITransferable> trans(maybeTransferable.unwrap());
+ if (!trans) {
+ NS_WARNING(
+ "EditorUtils::CreateTransferableForPlainText() returned nullptr, but "
+ "ignored");
+ return NS_OK;
+ }
+
+ auto* windowContext = GetDocument()->GetWindowContext();
+ if (!windowContext) {
+ NS_WARNING("Editor didn't have document window context");
+ return NS_ERROR_FAILURE;
+ }
+ // Get the Data from the clipboard
+ rv = clipboard->GetData(trans, aClipboardType, windowContext);
+
+ // Now we ask the transferable for the data
+ // it still owns the data, we just have a pointer to it.
+ // If it can't support a "text" output of the data the call will fail
+ nsCOMPtr<nsISupports> genericDataObj;
+ nsAutoCString flavor;
+ rv = trans->GetAnyTransferData(flavor, getter_AddRefs(genericDataObj));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsITransferable::GetAnyTransferData() failed");
+ return rv;
+ }
+
+ if (!flavor.EqualsLiteral(kTextMime) &&
+ !flavor.EqualsLiteral(kMozTextInternal)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISupportsString> text = do_QueryInterface(genericDataObj);
+ if (!text) {
+ return NS_OK;
+ }
+
+ nsString stuffToPaste;
+ DebugOnly<nsresult> rvIgnored = text->GetData(stuffToPaste);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ if (stuffToPaste.IsEmpty()) {
+ return NS_OK;
+ }
+
+ aEditActionData.SetData(stuffToPaste);
+ if (!stuffToPaste.IsEmpty()) {
+ nsContentUtils::PlatformToDOMLineBreaks(stuffToPaste);
+ }
+ rv = aEditActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return rv;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertWithQuotationsAsSubAction(stuffToPaste);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::InsertWithQuotationsAsSubAction() failed");
+ return rv;
+}
+
+nsresult TextEditor::InsertWithQuotationsAsSubAction(
+ const nsAString& aQuotedText) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (IsReadonly()) {
+ return NS_OK;
+ }
+
+ // Let the citer quote it for us:
+ nsString quotedStuff;
+ InternetCiter::GetCiteString(aQuotedText, quotedStuff);
+
+ // It's best to put a blank line after the quoted text so that mails
+ // written without thinking won't be so ugly.
+ if (!aQuotedText.IsEmpty() && (aQuotedText.Last() != char16_t('\n'))) {
+ quotedStuff.Append(char16_t('\n'));
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // XXX Do we need to support paste-as-quotation in password editor (and
+ // also in single line editor)?
+ MaybeDoAutoPasswordMasking();
+
+ nsresult rv = InsertTextAsSubAction(quotedStuff, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+}
+
+nsresult TextEditor::SelectEntireDocument() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ RefPtr<Element> anonymousDivElement = GetRoot();
+ if (NS_WARN_IF(!anonymousDivElement)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ RefPtr<Text> text =
+ Text::FromNodeOrNull(anonymousDivElement->GetFirstChild());
+ MOZ_ASSERT(text);
+
+ MOZ_TRY(SelectionRef().SetStartAndEndInLimiter(
+ *text, 0, *text, text->TextDataLength(), eDirNext,
+ nsISelectionListener::SELECTALL_REASON));
+
+ return NS_OK;
+}
+
+EventTarget* TextEditor::GetDOMEventTarget() const { return mEventTarget; }
+
+void TextEditor::ReinitializeSelection(Element& aElement) {
+ if (NS_WARN_IF(Destroyed())) {
+ return;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ // We don't need to flush pending notifications here and we don't need to
+ // handle spellcheck at first focus. Therefore, we don't need to call
+ // `TextEditor::OnFocus` here.
+ EditorBase::OnFocus(aElement);
+
+ // If previous focused editor turn on spellcheck and this editor doesn't
+ // turn on it, spellcheck state is mismatched. So we need to re-sync it.
+ SyncRealTimeSpell();
+}
+
+nsresult TextEditor::OnFocus(const nsINode& aOriginalEventTargetNode) {
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return NS_ERROR_FAILURE;
+ }
+ // Let's update the layout information right now because there are some
+ // pending notifications and flushing them may cause destroying the editor.
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ if (MOZ_UNLIKELY(!CanKeepHandlingFocusEvent(aOriginalEventTargetNode))) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Spell check a textarea the first time that it is focused.
+ nsresult rv = FlushPendingSpellCheck();
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::FlushPendingSpellCheck() failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::FlushPendingSpellCheck() failed, but ignored");
+ if (MOZ_UNLIKELY(!CanKeepHandlingFocusEvent(aOriginalEventTargetNode))) {
+ return NS_OK;
+ }
+
+ return EditorBase::OnFocus(aOriginalEventTargetNode);
+}
+
+nsresult TextEditor::OnBlur(const EventTarget* aEventTarget) {
+ // check if something else is focused. If another element is focused, then
+ // we should not change the selection.
+ nsFocusManager* focusManager = nsFocusManager::GetFocusManager();
+ if (MOZ_UNLIKELY(!focusManager)) {
+ return NS_OK;
+ }
+
+ // If another element already has focus, we should not maintain the selection
+ // because we may not have the rights doing it.
+ if (focusManager->GetFocusedElement()) {
+ return NS_OK;
+ }
+
+ nsresult rv = FinalizeSelection();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::FinalizeSelection() failed");
+ return rv;
+}
+
+nsresult TextEditor::SetAttributeOrEquivalent(Element* aElement,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ bool aSuppressTransaction) {
+ if (NS_WARN_IF(!aElement) || NS_WARN_IF(!aAttribute)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetAttribute);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = SetAttributeWithTransaction(*aElement, *aAttribute, aValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::RemoveAttributeOrEquivalent(Element* aElement,
+ nsAtom* aAttribute,
+ bool aSuppressTransaction) {
+ if (NS_WARN_IF(!aElement) || NS_WARN_IF(!aAttribute)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveAttribute);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = RemoveAttributeWithTransaction(*aElement, *aAttribute);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+template <typename EditorDOMPointType>
+EditorDOMPointType TextEditor::FindBetterInsertionPoint(
+ const EditorDOMPointType& aPoint) const {
+ if (MOZ_UNLIKELY(NS_WARN_IF(!aPoint.IsInContentNode()))) {
+ return aPoint;
+ }
+
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ Element* const anonymousDivElement = GetRoot();
+ if (aPoint.GetContainer() == anonymousDivElement) {
+ // In some cases, aPoint points start of the anonymous <div>. To avoid
+ // injecting unneeded text nodes, we first look to see if we have one
+ // available. In that case, we'll just adjust node and offset accordingly.
+ if (aPoint.IsStartOfContainer()) {
+ if (aPoint.GetContainer()->HasChildren() &&
+ aPoint.GetContainer()->GetFirstChild()->IsText()) {
+ return EditorDOMPointType(aPoint.GetContainer()->GetFirstChild(), 0u);
+ }
+ }
+ // In some other cases, aPoint points the terminating padding <br> element
+ // for empty last line in the anonymous <div>. In that case, we'll adjust
+ // aInOutNode and aInOutOffset to the preceding text node, if any.
+ else {
+ nsIContent* child = aPoint.GetContainer()->GetLastChild();
+ while (child) {
+ if (child->IsText()) {
+ return EditorDOMPointType::AtEndOf(*child);
+ }
+ child = child->GetPreviousSibling();
+ }
+ }
+ }
+
+ // Sometimes, aPoint points the padding <br> element. In that case, we'll
+ // adjust the insertion point to the previous text node, if one exists, or to
+ // the parent anonymous DIV.
+ if (EditorUtils::IsPaddingBRElementForEmptyLastLine(
+ *aPoint.template ContainerAs<nsIContent>()) &&
+ aPoint.IsStartOfContainer()) {
+ nsIContent* previousSibling = aPoint.GetContainer()->GetPreviousSibling();
+ if (previousSibling && previousSibling->IsText()) {
+ return EditorDOMPointType::AtEndOf(*previousSibling);
+ }
+
+ nsINode* parentOfContainer = aPoint.GetContainerParent();
+ if (parentOfContainer && parentOfContainer == anonymousDivElement) {
+ return EditorDOMPointType(parentOfContainer,
+ aPoint.template ContainerAs<nsIContent>(), 0u);
+ }
+ }
+
+ return aPoint;
+}
+
+// static
+void TextEditor::MaskString(nsString& aString, const Text& aTextNode,
+ uint32_t aStartOffsetInString,
+ uint32_t aStartOffsetInText) {
+ MOZ_ASSERT(aTextNode.HasFlag(NS_MAYBE_MASKED));
+ MOZ_ASSERT(aStartOffsetInString == 0 || aStartOffsetInText == 0);
+
+ uint32_t unmaskStart = UINT32_MAX, unmaskLength = 0;
+ TextEditor* textEditor =
+ nsContentUtils::GetTextEditorFromAnonymousNodeWithoutCreation(&aTextNode);
+ if (textEditor && textEditor->UnmaskedLength() > 0) {
+ unmaskStart = textEditor->UnmaskedStart();
+ unmaskLength = textEditor->UnmaskedLength();
+ // If text is copied from after unmasked range, we can treat this case
+ // as mask all.
+ if (aStartOffsetInText >= unmaskStart + unmaskLength) {
+ unmaskLength = 0;
+ unmaskStart = UINT32_MAX;
+ } else {
+ // If text is copied from middle of unmasked range, reduce the length
+ // and adjust start offset.
+ if (aStartOffsetInText > unmaskStart) {
+ unmaskLength = unmaskStart + unmaskLength - aStartOffsetInText;
+ unmaskStart = 0;
+ }
+ // If text is copied from before start of unmasked range, just adjust
+ // the start offset.
+ else {
+ unmaskStart -= aStartOffsetInText;
+ }
+ // Make the range is in the string.
+ unmaskStart += aStartOffsetInString;
+ }
+ }
+
+ const char16_t kPasswordMask = TextEditor::PasswordMask();
+ for (uint32_t i = aStartOffsetInString; i < aString.Length(); ++i) {
+ bool isSurrogatePair = NS_IS_HIGH_SURROGATE(aString.CharAt(i)) &&
+ i < aString.Length() - 1 &&
+ NS_IS_LOW_SURROGATE(aString.CharAt(i + 1));
+ if (i < unmaskStart || i >= unmaskStart + unmaskLength) {
+ if (isSurrogatePair) {
+ aString.SetCharAt(kPasswordMask, i);
+ aString.SetCharAt(kPasswordMask, i + 1);
+ } else {
+ aString.SetCharAt(kPasswordMask, i);
+ }
+ }
+
+ // Skip the following low surrogate.
+ if (isSurrogatePair) {
+ ++i;
+ }
+ }
+}
+
+nsresult TextEditor::SetUnmaskRangeInternal(uint32_t aStart, uint32_t aLength,
+ uint32_t aTimeout, bool aNotify,
+ bool aForceStartMasking) {
+ if (mPasswordMaskData) {
+ mPasswordMaskData->mIsMaskingPassword = aForceStartMasking || aTimeout != 0;
+
+ // We cannot manage multiple unmasked ranges so that shrink the previous
+ // range first.
+ if (!IsAllMasked()) {
+ mPasswordMaskData->mUnmaskedLength = 0;
+ mPasswordMaskData->CancelTimer(PasswordMaskData::ReleaseTimer::No);
+ }
+ }
+
+ // If we're not a password editor, return error since this call does not
+ // make sense.
+ if (!IsPasswordEditor() || NS_WARN_IF(!mPasswordMaskData)) {
+ mPasswordMaskData->CancelTimer(PasswordMaskData::ReleaseTimer::Yes);
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ Element* rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ Text* text = Text::FromNodeOrNull(rootElement->GetFirstChild());
+ if (!text || !text->Length()) {
+ // There is no anonymous text node in the editor.
+ return aStart > 0 && aStart != UINT32_MAX ? NS_ERROR_INVALID_ARG : NS_OK;
+ }
+
+ if (aStart < UINT32_MAX) {
+ uint32_t valueLength = text->Length();
+ if (aStart >= valueLength) {
+ return NS_ERROR_INVALID_ARG; // There is no character can be masked.
+ }
+ // If aStart is middle of a surrogate pair, expand it to include the
+ // preceding high surrogate because the caller may want to show a
+ // character before the character at `aStart + 1`.
+ const nsTextFragment* textFragment = text->GetText();
+ if (textFragment->IsLowSurrogateFollowingHighSurrogateAt(aStart)) {
+ mPasswordMaskData->mUnmaskedStart = aStart - 1;
+ // If caller collapses the range, keep it. Otherwise, expand the length.
+ if (aLength > 0) {
+ ++aLength;
+ }
+ } else {
+ mPasswordMaskData->mUnmaskedStart = aStart;
+ }
+ mPasswordMaskData->mUnmaskedLength =
+ std::min(valueLength - UnmaskedStart(), aLength);
+ // If unmasked end is middle of a surrogate pair, expand it to include
+ // the following low surrogate because the caller may want to show a
+ // character after the character at `aStart + aLength`.
+ if (UnmaskedEnd() < valueLength &&
+ textFragment->IsLowSurrogateFollowingHighSurrogateAt(UnmaskedEnd())) {
+ mPasswordMaskData->mUnmaskedLength++;
+ }
+ // If it's first time to mask the unmasking characters with timer, create
+ // the timer now. Then, we'll keep using it for saving the creation cost.
+ if (!HasAutoMaskingTimer() && aLength && aTimeout && UnmaskedLength()) {
+ mPasswordMaskData->mTimer = NS_NewTimer();
+ }
+ } else {
+ if (NS_WARN_IF(aLength != 0)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ mPasswordMaskData->MaskAll();
+ }
+
+ // Notify nsTextFrame of this update if the caller wants this to do it.
+ // Only in this case, script may run.
+ if (aNotify) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ // Notify nsTextFrame of masking range change.
+ if (RefPtr<PresShell> presShell = document->GetObservingPresShell()) {
+ nsAutoScriptBlocker blockRunningScript;
+ uint32_t valueLength = text->Length();
+ CharacterDataChangeInfo changeInfo = {false, 0, valueLength, valueLength,
+ nullptr};
+ presShell->CharacterDataChanged(text, changeInfo);
+ }
+
+ // Scroll caret into the view since masking or unmasking character may
+ // move caret to outside of the view.
+ nsresult rv = ScrollSelectionFocusIntoView();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::ScrollSelectionFocusIntoView() failed");
+ return rv;
+ }
+ }
+
+ if (!IsAllMasked() && aTimeout != 0) {
+ // Initialize the timer to mask the range automatically.
+ MOZ_ASSERT(HasAutoMaskingTimer());
+ DebugOnly<nsresult> rvIgnored = mPasswordMaskData->mTimer->InitWithCallback(
+ this, aTimeout, nsITimer::TYPE_ONE_SHOT);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITimer::InitWithCallback() failed, but ignored");
+ }
+
+ return NS_OK;
+}
+
+// static
+char16_t TextEditor::PasswordMask() {
+ char16_t ret = LookAndFeel::GetPasswordCharacter();
+ if (!ret) {
+ ret = '*';
+ }
+ return ret;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP TextEditor::Notify(nsITimer* aTimer) {
+ // Check whether our text editor's password flag was changed before this
+ // "hide password character" timer actually fires.
+ if (!IsPasswordEditor() || NS_WARN_IF(!mPasswordMaskData)) {
+ return NS_OK;
+ }
+
+ if (IsAllMasked()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eHidePassword);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Mask all characters.
+ nsresult rv = MaskAllCharactersAndNotify();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::MaskAllCharactersAndNotify() failed");
+
+ if (StaticPrefs::editor_password_testing_mask_delay()) {
+ if (RefPtr<Element> target = GetInputEventTargetElement()) {
+ RefPtr<Document> document = target->OwnerDoc();
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchTrustedEvent(
+ document, target, u"MozLastInputMasked"_ns, CanBubble::eYes,
+ Cancelable::eNo);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsContentUtils::DispatchTrustedEvent("
+ "MozLastInputMasked) failed, but ignored");
+ }
+ }
+
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP TextEditor::GetName(nsACString& aName) {
+ aName.AssignLiteral("TextEditor");
+ return NS_OK;
+}
+
+void TextEditor::WillDeleteText(uint32_t aCurrentLength,
+ uint32_t aRemoveStartOffset,
+ uint32_t aRemoveLength) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!IsPasswordEditor() || NS_WARN_IF(!mPasswordMaskData) || IsAllMasked()) {
+ return;
+ }
+
+ // Adjust unmasked range before deletion since DOM mutation may cause
+ // layout referring the range in old text.
+
+ // If we need to mask automatically, mask all now.
+ if (IsMaskingPassword()) {
+ DebugOnly<nsresult> rvIgnored = MaskAllCharacters();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::MaskAllCharacters() failed, but ignored");
+ return;
+ }
+
+ if (aRemoveStartOffset < UnmaskedStart()) {
+ // If removing range is before the unmasked range, move it.
+ if (aRemoveStartOffset + aRemoveLength <= UnmaskedStart()) {
+ DebugOnly<nsresult> rvIgnored =
+ SetUnmaskRange(UnmaskedStart() - aRemoveLength, UnmaskedLength());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ // If removing range starts before unmasked range, and ends in unmasked
+ // range, move and shrink the range.
+ if (aRemoveStartOffset + aRemoveLength < UnmaskedEnd()) {
+ uint32_t unmaskedLengthInRemovingRange =
+ aRemoveStartOffset + aRemoveLength - UnmaskedStart();
+ DebugOnly<nsresult> rvIgnored = SetUnmaskRange(
+ aRemoveStartOffset, UnmaskedLength() - unmaskedLengthInRemovingRange);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ // If removing range includes all unmasked range, collapse it to the
+ // remove offset.
+ DebugOnly<nsresult> rvIgnored = SetUnmaskRange(aRemoveStartOffset, 0);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ if (aRemoveStartOffset < UnmaskedEnd()) {
+ // If removing range is in unmasked range, shrink the range.
+ if (aRemoveStartOffset + aRemoveLength <= UnmaskedEnd()) {
+ DebugOnly<nsresult> rvIgnored =
+ SetUnmaskRange(UnmaskedStart(), UnmaskedLength() - aRemoveLength);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ // If removing range starts from unmasked range, and ends after it,
+ // shrink it.
+ DebugOnly<nsresult> rvIgnored =
+ SetUnmaskRange(UnmaskedStart(), aRemoveStartOffset - UnmaskedStart());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ // If removing range is after the unmasked range, keep it.
+}
+
+nsresult TextEditor::DidInsertText(uint32_t aNewLength,
+ uint32_t aInsertedOffset,
+ uint32_t aInsertedLength) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!IsPasswordEditor() || NS_WARN_IF(!mPasswordMaskData) || IsAllMasked()) {
+ return NS_OK;
+ }
+
+ if (IsMaskingPassword()) {
+ // If we need to mask password, mask all right now.
+ nsresult rv = MaskAllCharactersAndNotify();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::MaskAllCharacters() failed");
+ return rv;
+ }
+
+ if (aInsertedOffset < UnmaskedStart()) {
+ // If insertion point is before unmasked range, expand the unmasked range
+ // to include the new text.
+ nsresult rv = SetUnmaskRangeAndNotify(
+ aInsertedOffset, UnmaskedEnd() + aInsertedLength - aInsertedOffset);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetUnmaskRangeAndNotify() failed");
+ return rv;
+ }
+
+ if (aInsertedOffset <= UnmaskedEnd()) {
+ // If insertion point is in unmasked range, unmask new text.
+ nsresult rv = SetUnmaskRangeAndNotify(UnmaskedStart(),
+ UnmaskedLength() + aInsertedLength);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetUnmaskRangeAndNotify() failed");
+ return rv;
+ }
+
+ // If insertion point is after unmasked range, extend the unmask range to
+ // include the new text.
+ nsresult rv = SetUnmaskRangeAndNotify(
+ UnmaskedStart(), aInsertedOffset + aInsertedLength - UnmaskedStart());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetUnmaskRangeAndNotify() failed");
+ return rv;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/TextEditor.h b/editor/libeditor/TextEditor.h
new file mode 100644
index 0000000000..d6836020f1
--- /dev/null
+++ b/editor/libeditor/TextEditor.h
@@ -0,0 +1,633 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_TextEditor_h
+#define mozilla_TextEditor_h
+
+#include "mozilla/EditorBase.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/TextControlState.h"
+#include "mozilla/UniquePtr.h"
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsINamed.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+#include "nscore.h"
+
+class nsIContent;
+class nsIDocumentEncoder;
+class nsIOutputStream;
+class nsIPrincipal;
+class nsISelectionController;
+class nsITransferable;
+
+namespace mozilla {
+namespace dom {
+class Selection;
+} // namespace dom
+
+/**
+ * The text editor implementation.
+ * Use to edit text document represented as a DOM tree.
+ */
+class TextEditor final : public EditorBase,
+ public nsITimerCallback,
+ public nsINamed {
+ public:
+ /****************************************************************************
+ * NOTE: DO NOT MAKE YOUR NEW METHODS PUBLIC IF they are called by other
+ * classes under libeditor except EditorEventListener and
+ * HTMLEditorEventListener because each public method which may fire
+ * eEditorInput event will need to instantiate new stack class for
+ * managing input type value of eEditorInput and cache some objects
+ * for smarter handling. In other words, when you add new root
+ * method to edit the DOM tree, you can make your new method public.
+ ****************************************************************************/
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TextEditor, EditorBase)
+
+ TextEditor();
+
+ /**
+ * Note that TextEditor::Init() shouldn't cause running script synchronously.
+ * So, `MOZ_CAN_RUN_SCRIPT_BOUNDARY` is safe here.
+ *
+ * @param aDocument The document which aAnonymousDivElement belongs to.
+ * @param aAnonymousDivElement
+ * The root editable element for this editor.
+ * @param aSelectionController
+ * The selection controller for independent selections
+ * in the `<input>` or `<textarea>` element.
+ * @param aFlags Some of nsIEditor::eEditor*Mask flags.
+ * @param aPasswordMaskData
+ * Set to an instance only when aFlags includes
+ * `nsIEditor::eEditorPasswordMask`. Otherwise, must be
+ * `nullptr`.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
+ Init(Document& aDocument, Element& aAnonymousDivElement,
+ nsISelectionController& aSelectionController, uint32_t aFlags,
+ UniquePtr<PasswordMaskData>&& aPasswordMaskData);
+
+ /**
+ * PostCreate() should be called after Init, and is the time that the editor
+ * tells its documentStateObservers that the document has been created.
+ * Note that TextEditor::PostCreate() shouldn't cause running script
+ * synchronously. So, `MOZ_CAN_RUN_SCRIPT_BOUNDARY` is safe here.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult PostCreate();
+
+ /**
+ * This method re-initializes the selection and caret state that are for
+ * current editor state. When editor session is destroyed, it always reset
+ * selection state even if this has no focus. So if destroying editor,
+ * we have to call this method for focused editor to set selection state.
+ */
+ MOZ_CAN_RUN_SCRIPT void ReinitializeSelection(Element& aElement);
+
+ /**
+ * PreDestroy() is called before the editor goes away, and gives the editor a
+ * chance to tell its documentStateObservers that the document is going away.
+ * Note that TextEditor::PreDestroy() shouldn't cause running script
+ * synchronously. So, `MOZ_CAN_RUN_SCRIPT_BOUNDARY` is safe here.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT_BOUNDARY UniquePtr<PasswordMaskData>
+ PreDestroy();
+
+ static TextEditor* GetFrom(nsIEditor* aEditor) {
+ return aEditor ? aEditor->GetAsTextEditor() : nullptr;
+ }
+ static const TextEditor* GetFrom(const nsIEditor* aEditor) {
+ return aEditor ? aEditor->GetAsTextEditor() : nullptr;
+ }
+
+ /**
+ * Helper method for `AppendString()` and `AppendSubString()`. This should
+ * be called only when `aText` is in a password field. This method masks
+ * A part of or all of `aText` (`aStartOffsetInText` and later) should've
+ * been copied (appended) to `aString`. `aStartOffsetInString` is where
+ * the password was appended into `aString`.
+ */
+ static void MaskString(nsString& aString, const dom::Text& aTextNode,
+ uint32_t aStartOffsetInString,
+ uint32_t aStartOffsetInText);
+
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ // Overrides of nsIEditor
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD EndOfDocument() final;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD InsertLineBreak() final;
+ NS_IMETHOD GetTextLength(uint32_t* aCount) final;
+
+ // Shouldn't be used internally, but we need these using declarations for
+ // avoiding warnings of clang.
+ using EditorBase::CanCopy;
+ using EditorBase::CanCut;
+ using EditorBase::CanPaste;
+
+ // Overrides of EditorBase
+ bool IsEmpty() const final;
+
+ bool CanPaste(int32_t aClipboardType) const final;
+
+ bool CanPasteTransferable(nsITransferable* aTransferable) final;
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) final;
+
+ dom::EventTarget* GetDOMEventTarget() const final;
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ OnFocus(const nsINode& aOriginalEventTargetNode) final;
+
+ nsresult OnBlur(const dom::EventTarget* aEventTarget) final;
+
+ /**
+ * The maximum number of characters allowed.
+ * default: -1 (unlimited).
+ */
+ int32_t MaxTextLength() const { return mMaxTextLength; }
+ void SetMaxTextLength(int32_t aLength) { mMaxTextLength = aLength; }
+
+ /**
+ * This updates the wrap width used for initializing a document encoder within
+ * a call of EditorBase::GetAndInitDocEncoder().
+ */
+ void SetWrapColumn(int32_t aWrapColumn) { mWrapColumn = aWrapColumn; }
+
+ /**
+ * Replace existed string with a string.
+ * This is fast path to replace all string when using single line control.
+ *
+ * @param aString The string to be set
+ * @param aAllowBeforeInputEventCancelable
+ * Whether `beforeinput` event which will be
+ * dispatched for this can be cancelable or not.
+ * @param aPrincipal Set subject principal if it may be called by
+ * JS. If set to nullptr, will be treated as
+ * called by system.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetTextAsAction(
+ const nsAString& aString,
+ AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ InsertLineBreakAsAction(nsIPrincipal* aPrincipal = nullptr) final;
+
+ /**
+ * ComputeTextValue() computes plaintext value of this editor. This may be
+ * too expensive if it's in hot path.
+ *
+ * @param aDocumentEncoderFlags Flags of nsIDocumentEncoder.
+ * @param aCharset Encoding of the document.
+ */
+ nsresult ComputeTextValue(uint32_t aDocumentEncoderFlags,
+ nsAString& aOutputString) const {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv = ComputeValueInternal(u"text/plain"_ns, aDocumentEncoderFlags,
+ aOutputString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ return NS_OK;
+ }
+
+ /**
+ * The following methods are available only when the instance is a password
+ * editor. They return whether there is unmasked range or not and range
+ * start and length.
+ */
+ MOZ_ALWAYS_INLINE bool IsAllMasked() const {
+ MOZ_ASSERT(IsPasswordEditor());
+ return !mPasswordMaskData || mPasswordMaskData->IsAllMasked();
+ }
+ MOZ_ALWAYS_INLINE uint32_t UnmaskedStart() const {
+ MOZ_ASSERT(IsPasswordEditor());
+ return mPasswordMaskData ? mPasswordMaskData->mUnmaskedStart : UINT32_MAX;
+ }
+ MOZ_ALWAYS_INLINE uint32_t UnmaskedLength() const {
+ MOZ_ASSERT(IsPasswordEditor());
+ return mPasswordMaskData ? mPasswordMaskData->mUnmaskedLength : 0;
+ }
+ MOZ_ALWAYS_INLINE uint32_t UnmaskedEnd() const {
+ MOZ_ASSERT(IsPasswordEditor());
+ return mPasswordMaskData ? mPasswordMaskData->UnmaskedEnd() : UINT32_MAX;
+ }
+
+ /**
+ * IsMaskingPassword() returns false when the last caller of `Unmask()`
+ * didn't want to mask again automatically. When this returns true, user
+ * input causes masking the password even before timed-out.
+ */
+ bool IsMaskingPassword() const {
+ MOZ_ASSERT(IsPasswordEditor());
+ return mPasswordMaskData && mPasswordMaskData->mIsMaskingPassword;
+ }
+
+ /**
+ * PasswordMask() returns a character which masks each character in password
+ * fields.
+ */
+ static char16_t PasswordMask();
+
+ /**
+ * If you want to prevent to echo password temporarily, use the following
+ * methods.
+ */
+ bool EchoingPasswordPrevented() const {
+ return mPasswordMaskData && mPasswordMaskData->mEchoingPasswordPrevented;
+ }
+ void PreventToEchoPassword() {
+ if (mPasswordMaskData) {
+ mPasswordMaskData->mEchoingPasswordPrevented = true;
+ }
+ }
+ void AllowToEchoPassword() {
+ if (mPasswordMaskData) {
+ mPasswordMaskData->mEchoingPasswordPrevented = false;
+ }
+ }
+
+ dom::Text* GetTextNode() {
+ MOZ_DIAGNOSTIC_ASSERT(GetRoot());
+ MOZ_DIAGNOSTIC_ASSERT(GetRoot()->GetFirstChild());
+ MOZ_DIAGNOSTIC_ASSERT(GetRoot()->GetFirstChild()->IsText());
+ if (MOZ_UNLIKELY(!GetRoot() || !GetRoot()->GetFirstChild())) {
+ return nullptr;
+ }
+ return GetRoot()->GetFirstChild()->GetAsText();
+ }
+ const dom::Text* GetTextNode() const {
+ return const_cast<TextEditor*>(this)->GetTextNode();
+ }
+
+ protected: // May be called by friends.
+ /****************************************************************************
+ * Some friend classes are allowed to call the following protected methods.
+ * However, those methods won't prepare caches of some objects which are
+ * necessary for them. So, if you call them from friend classes, you need
+ * to make sure that AutoEditActionDataSetter is created.
+ ****************************************************************************/
+
+ // Overrides of EditorBase
+ MOZ_CAN_RUN_SCRIPT nsresult RemoveAttributeOrEquivalent(
+ Element* aElement, nsAtom* aAttribute, bool aSuppressTransaction) final;
+ MOZ_CAN_RUN_SCRIPT nsresult SetAttributeOrEquivalent(
+ Element* aElement, nsAtom* aAttribute, const nsAString& aValue,
+ bool aSuppressTransaction) final;
+ using EditorBase::RemoveAttributeOrEquivalent;
+ using EditorBase::SetAttributeOrEquivalent;
+
+ /**
+ * FindBetterInsertionPoint() tries to look for better insertion point which
+ * is typically the nearest text node and offset in it.
+ *
+ * @param aPoint Insertion point which the callers found.
+ * @return Better insertion point if there is. If not returns
+ * same point as aPoint.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType FindBetterInsertionPoint(
+ const EditorDOMPointType& aPoint) const;
+
+ /**
+ * InsertLineBreakAsSubAction() inserts a line break.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertLineBreakAsSubAction();
+
+ /**
+ * Replace existed string with aString. Caller must guarantee that there
+ * is a placeholder transaction which will have the transaction.
+ *
+ * @ param aString The string to be set.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ SetTextAsSubAction(const nsAString& aString);
+
+ /**
+ * MaybeDoAutoPasswordMasking() may mask password if we're doing auto-masking.
+ */
+ void MaybeDoAutoPasswordMasking() {
+ if (IsPasswordEditor() && IsMaskingPassword()) {
+ MaskAllCharacters();
+ }
+ }
+
+ /**
+ * SetUnmaskRange() is available only when the instance is a password
+ * editor. This just updates unmask range. I.e., caller needs to
+ * guarantee to update the layout.
+ *
+ * @param aStart First index to show the character.
+ * If aLength is 0, this value is ignored.
+ * @param aLength Optional, Length to show characters.
+ * If UINT32_MAX, it means unmasking all characters after
+ * aStart.
+ * If 0, it means that masking all characters.
+ * @param aTimeout Optional, specify milliseconds to hide the unmasked
+ * characters after this call.
+ * If 0, it means this won't mask the characters
+ * automatically.
+ * If aLength is 0, this value is ignored.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult SetUnmaskRange(
+ uint32_t aStart, uint32_t aLength = UINT32_MAX, uint32_t aTimeout = 0) {
+ return SetUnmaskRangeInternal(aStart, aLength, aTimeout, false, false);
+ }
+
+ /**
+ * SetUnmaskRangeAndNotify() is available only when the instance is a
+ * password editor. This updates unmask range and notifying the text frame
+ * to update the visible characters.
+ *
+ * @param aStart First index to show the character.
+ * If UINT32_MAX, it means masking all.
+ * @param aLength Optional, Length to show characters.
+ * If UINT32_MAX, it means unmasking all characters after
+ * aStart.
+ * @param aTimeout Optional, specify milliseconds to hide the unmasked
+ * characters after this call.
+ * If 0, it means this won't mask the characters
+ * automatically.
+ * If aLength is 0, this value is ignored.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetUnmaskRangeAndNotify(
+ uint32_t aStart, uint32_t aLength = UINT32_MAX, uint32_t aTimeout = 0) {
+ return SetUnmaskRangeInternal(aStart, aLength, aTimeout, true, false);
+ }
+
+ /**
+ * MaskAllCharacters() is an alias of SetUnmaskRange() to mask all characters.
+ * In other words, this removes existing unmask range.
+ * After this is called, TextEditor starts masking password automatically.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult MaskAllCharacters() {
+ if (!mPasswordMaskData) {
+ return NS_OK; // Already we don't have masked range data.
+ }
+ return SetUnmaskRangeInternal(UINT32_MAX, 0, 0, false, true);
+ }
+
+ /**
+ * MaskAllCharactersAndNotify() is an alias of SetUnmaskRangeAndNotify() to
+ * mask all characters and notifies the text frame. In other words, this
+ * removes existing unmask range.
+ * After this is called, TextEditor starts masking password automatically.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult MaskAllCharactersAndNotify() {
+ return SetUnmaskRangeInternal(UINT32_MAX, 0, 0, true, true);
+ }
+
+ /**
+ * WillDeleteText() is called before `DeleteTextTransaction` or something
+ * removes text in a text node. Note that this won't be called if the
+ * instance is `HTMLEditor` since supporting it makes the code complicated
+ * due to mutation events.
+ *
+ * @param aCurrentLength Current text length of the node.
+ * @param aRemoveStartOffset Start offset of the range to be removed.
+ * @param aRemoveLength Length of the range to be removed.
+ */
+ void WillDeleteText(uint32_t aCurrentLength, uint32_t aRemoveStartOffset,
+ uint32_t aRemoveLength);
+
+ /**
+ * DidInsertText() is called after `InsertTextTransaction` or something
+ * inserts text into a text node. Note that this won't be called if the
+ * instance is `HTMLEditor` since supporting it makes the code complicated
+ * due to mutatione events.
+ *
+ * @param aNewLength New text length after the insertion.
+ * @param aInsertedOffset Start offset of the inserted text.
+ * @param aInsertedLength Length of the inserted text.
+ * @return NS_OK or NS_ERROR_EDITOR_DESTROYED.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult DidInsertText(
+ uint32_t aNewLength, uint32_t aInsertedOffset, uint32_t aInsertedLength);
+
+ protected: // edit sub-action handler
+ /**
+ * MaybeTruncateInsertionStringForMaxLength() truncates aInsertionString to
+ * `maxlength` if it was not pasted in by the user.
+ *
+ * @param aInsertionString [in/out] New insertion string. This is
+ * truncated to `maxlength` if it was not pasted in
+ * by the user.
+ * @return If aInsertionString is truncated, it returns "as
+ * handled", else "as ignored."
+ */
+ Result<EditActionResult, nsresult> MaybeTruncateInsertionStringForMaxLength(
+ nsAString& aInsertionString);
+
+ /**
+ * InsertLineFeedCharacterAtSelection() inserts a linefeed character at
+ * selection.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ InsertLineFeedCharacterAtSelection();
+
+ /**
+ * Handles the newline characters according to the default system prefs
+ * (editor.singleLine.pasteNewlines).
+ * Each value means:
+ * nsIEditor::eNewlinesReplaceWithSpaces (2, Firefox default):
+ * replace newlines with spaces.
+ * nsIEditor::eNewlinesStrip (3):
+ * remove newlines from the string.
+ * nsIEditor::eNewlinesReplaceWithCommas (4, Thunderbird default):
+ * replace newlines with commas.
+ * nsIEditor::eNewlinesStripSurroundingWhitespace (5):
+ * collapse newlines and surrounding white-space characters and
+ * remove them from the string.
+ * nsIEditor::eNewlinesPasteIntact (0):
+ * only remove the leading and trailing newlines.
+ * nsIEditor::eNewlinesPasteToFirst (1) or any other value:
+ * remove the first newline and all characters following it.
+ *
+ * @param aString the string to be modified in place.
+ */
+ void HandleNewLinesInStringForSingleLineEditor(nsString& aString) const;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleInsertText(EditSubAction aEditSubAction,
+ const nsAString& aInsertionString,
+ SelectionHandling aSelectionHandling) final;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertDroppedDataTransferAsAction(
+ AutoEditActionDataSetter& aEditActionData,
+ dom::DataTransfer& aDataTransfer, const EditorDOMPoint& aDroppedAt,
+ nsIPrincipal* aSourcePrincipal) final;
+
+ /**
+ * HandleDeleteSelectionInternal() is a helper method of
+ * HandleDeleteSelection(). Must be called only when the instance is
+ * TextEditor.
+ * NOTE: This method creates SelectionBatcher. Therefore, each caller
+ * needs to check if the editor is still available even if this returns
+ * NS_OK.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleDeleteSelectionInternal(nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers);
+
+ /**
+ * This method handles "delete selection" commands.
+ *
+ * @param aDirectionAndAmount Direction of the deletion.
+ * @param aStripWrappers Must be nsIEditor::eNoStrip.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ HandleDeleteSelection(nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) final;
+
+ /**
+ * ComputeValueFromTextNodeAndBRElement() tries to compute "value" of
+ * this editor content only with text nodes and `<br>` elements.
+ * If this succeeds to compute the value, it's returned with aValue and
+ * the result is marked as "handled". Otherwise, the caller needs to
+ * compute it with another way.
+ */
+ Result<EditActionResult, nsresult> ComputeValueFromTextNodeAndBRElement(
+ nsAString& aValue) const;
+
+ /**
+ * SetTextWithoutTransaction() is optimized method to set `<input>.value`
+ * and `<textarea>.value` to aValue without transaction. This must be
+ * called only when it's not `HTMLEditor` and undo/redo is disabled.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
+ SetTextWithoutTransaction(const nsAString& aValue);
+
+ /**
+ * EnsureCaretNotAtEndOfTextNode() collapses selection at the padding `<br>`
+ * element (i.e., container becomes the anonymous `<div>` element) if
+ * `Selection` is at end of the text node.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult EnsureCaretNotAtEndOfTextNode();
+
+ protected: // Called by helper classes.
+ MOZ_CAN_RUN_SCRIPT void OnStartToHandleTopLevelEditSubAction(
+ EditSubAction aTopLevelEditSubAction,
+ nsIEditor::EDirection aDirectionOfTopLevelEditSubAction,
+ ErrorResult& aRv) final;
+ MOZ_CAN_RUN_SCRIPT nsresult OnEndHandlingTopLevelEditSubAction() final;
+
+ /**
+ * HandleInlineSpellCheckAfterEdit() does spell-check after handling top level
+ * edit subaction.
+ */
+ nsresult HandleInlineSpellCheckAfterEdit() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ if (!GetSpellCheckRestartPoint().IsSet()) {
+ return NS_OK; // Maybe being initialized.
+ }
+ nsresult rv = HandleInlineSpellCheck(GetSpellCheckRestartPoint());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to spellcheck");
+ ClearSpellCheckRestartPoint();
+ return rv;
+ }
+
+ protected: // Shouldn't be used by friend classes
+ virtual ~TextEditor();
+
+ /**
+ * CanEchoPasswordNow() returns true if currently we can echo password.
+ * If it's direct user input such as pasting or dropping text, this
+ * returns false even if we may echo password.
+ */
+ bool CanEchoPasswordNow() const;
+
+ /**
+ * InitEditorContentAndSelection() may insert a padding `<br>` element for
+ * if it's required in the anonymous `<div>` element or `<body>` element and
+ * collapse selection at the end if there is no selection ranges.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InitEditorContentAndSelection();
+
+ /**
+ * Collapse `Selection` to end of the text node in the anonymous <div>
+ * element.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult CollapseSelectionToEndOfTextNode();
+
+ /**
+ * Make the given selection span the entire document.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SelectEntireDocument() final;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandlePaste(
+ AutoEditActionDataSetter& aEditActionData, int32_t aClipboardType) final;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandlePasteAsQuotation(
+ AutoEditActionDataSetter& aEditActionData, int32_t aClipboardType) final;
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ HandlePasteTransferable(AutoEditActionDataSetter& aEditActionData,
+ nsITransferable& aTransferable) final;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ InsertWithQuotationsAsSubAction(const nsAString& aQuotedText) final;
+
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ InsertTextFromTransferable(nsITransferable* transferable);
+
+ bool IsCopyToClipboardAllowedInternal() const final;
+
+ already_AddRefed<Element> GetInputEventTargetElement() const final;
+
+ /**
+ * See SetUnmaskRange() and SetUnmaskRangeAndNotify() for the detail.
+ *
+ * @param aForceStartMasking If true, forcibly starts masking. This should
+ * be used only when `nsIEditor::Mask()` is called.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetUnmaskRangeInternal(uint32_t aStart,
+ uint32_t aLength,
+ uint32_t aTimeout,
+ bool aNotify,
+ bool aForceStartMasking);
+
+ MOZ_ALWAYS_INLINE bool HasAutoMaskingTimer() const {
+ return mPasswordMaskData && mPasswordMaskData->mTimer;
+ }
+
+ protected:
+ UniquePtr<PasswordMaskData> mPasswordMaskData;
+
+ int32_t mMaxTextLength = -1;
+
+ friend class AutoRangeArray; // FindBetterInsertionPoint
+ friend class DeleteNodeTransaction;
+ friend class EditorBase;
+ friend class InsertNodeTransaction;
+};
+
+} // namespace mozilla
+
+mozilla::TextEditor* nsIEditor::AsTextEditor() {
+ MOZ_DIAGNOSTIC_ASSERT(IsTextEditor());
+ return static_cast<mozilla::TextEditor*>(this);
+}
+
+const mozilla::TextEditor* nsIEditor::AsTextEditor() const {
+ MOZ_DIAGNOSTIC_ASSERT(IsTextEditor());
+ return static_cast<const mozilla::TextEditor*>(this);
+}
+
+mozilla::TextEditor* nsIEditor::GetAsTextEditor() {
+ return AsEditorBase()->IsTextEditor() ? AsTextEditor() : nullptr;
+}
+
+const mozilla::TextEditor* nsIEditor::GetAsTextEditor() const {
+ return AsEditorBase()->IsTextEditor() ? AsTextEditor() : nullptr;
+}
+
+#endif // #ifndef mozilla_TextEditor_h
diff --git a/editor/libeditor/TextEditorDataTransfer.cpp b/editor/libeditor/TextEditorDataTransfer.cpp
new file mode 100644
index 0000000000..ae7752843e
--- /dev/null
+++ b/editor/libeditor/TextEditorDataTransfer.cpp
@@ -0,0 +1,279 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextEditor.h"
+
+#include "EditorUtils.h"
+#include "HTMLEditor.h"
+#include "SelectionState.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/Selection.h"
+
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIClipboard.h"
+#include "nsIContent.h"
+#include "nsIDragService.h"
+#include "nsIDragSession.h"
+#include "nsIPrincipal.h"
+#include "nsIFormControl.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITransferable.h"
+#include "nsIVariant.h"
+#include "nsLiteralString.h"
+#include "nsRange.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsXPCOM.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+nsresult TextEditor::InsertTextFromTransferable(
+ nsITransferable* aTransferable) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTextEditor());
+
+ nsAutoCString bestFlavor;
+ nsCOMPtr<nsISupports> genericDataObj;
+ nsresult rv = aTransferable->GetAnyTransferData(
+ bestFlavor, getter_AddRefs(genericDataObj));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "nsITransferable::GetAnyDataTransferData() failed, but ignored");
+ if (NS_SUCCEEDED(rv) && (bestFlavor.EqualsLiteral(kTextMime) ||
+ bestFlavor.EqualsLiteral(kMozTextInternal))) {
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ nsAutoString stuffToPaste;
+ if (nsCOMPtr<nsISupportsString> text = do_QueryInterface(genericDataObj)) {
+ text->GetData(stuffToPaste);
+ }
+ MOZ_ASSERT(GetEditAction() == EditAction::ePaste);
+ // Use native line breaks for compatibility with Chrome.
+ // XXX Although, somebody has already converted native line breaks to
+ // XP line breaks.
+ UpdateEditActionData(stuffToPaste);
+
+ nsresult rv = MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(
+ rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "EditorBase::MaybeDispatchBeforeInputEvent() failed");
+ return rv;
+ }
+
+ if (!stuffToPaste.IsEmpty()) {
+ // Sanitize possible carriage returns in the string to be inserted
+ nsContentUtils::PlatformToDOMLineBreaks(stuffToPaste);
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ nsresult rv =
+ InsertTextAsSubAction(stuffToPaste, SelectionHandling::Delete);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+ }
+ }
+ }
+
+ // Try to scroll the selection into view if the paste/drop succeeded
+ rv = ScrollSelectionFocusIntoView();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::ScrollSelectionFocusIntoView() failed");
+ return rv;
+}
+
+nsresult TextEditor::InsertDroppedDataTransferAsAction(
+ AutoEditActionDataSetter& aEditActionData, DataTransfer& aDataTransfer,
+ const EditorDOMPoint& aDroppedAt, nsIPrincipal* aSourcePrincipal) {
+ MOZ_ASSERT(aEditActionData.GetEditAction() == EditAction::eDrop);
+ MOZ_ASSERT(GetEditAction() == EditAction::eDrop);
+ MOZ_ASSERT(aDroppedAt.IsSet());
+ MOZ_ASSERT(aDataTransfer.MozItemCount() > 0);
+
+ uint32_t numItems = aDataTransfer.MozItemCount();
+ AutoTArray<nsString, 5> textArray;
+ textArray.SetCapacity(numItems);
+ uint32_t textLength = 0;
+ for (uint32_t i = 0; i < numItems; ++i) {
+ nsCOMPtr<nsIVariant> data;
+ aDataTransfer.GetDataAtNoSecurityCheck(u"text/plain"_ns, i,
+ getter_AddRefs(data));
+ if (!data) {
+ continue;
+ }
+ // Use nsString to avoid copying its storage to textArray.
+ nsString insertText;
+ data->GetAsAString(insertText);
+ if (insertText.IsEmpty()) {
+ continue;
+ }
+ textArray.AppendElement(insertText);
+ textLength += insertText.Length();
+ }
+ // Use nsString to avoid copying its storage to aEditActionData.
+ nsString data;
+ data.SetCapacity(textLength);
+ // Join the text array from end to start because we insert each items
+ // in the aDataTransfer at same point from start to end. Although I
+ // don't know whether this is intentional behavior.
+ for (nsString& text : Reversed(textArray)) {
+ data.Append(text);
+ }
+ // Use native line breaks for compatibility with Chrome.
+ // XXX Although, somebody has already converted native line breaks to
+ // XP line breaks.
+ aEditActionData.SetData(data);
+
+ nsresult rv = aEditActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return rv;
+ }
+
+ // Then, insert the text. Note that we shouldn't need to walk the array
+ // anymore because nobody should listen to mutation events of anonymous
+ // text node in <input>/<textarea>.
+ nsContentUtils::PlatformToDOMLineBreaks(data);
+ rv = InsertTextAt(data, aDroppedAt, DeleteSelectedContent::No);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAt(DeleteSelectedContent::No) "
+ "failed, but ignored");
+ return rv;
+}
+
+nsresult TextEditor::HandlePaste(AutoEditActionDataSetter& aEditActionData,
+ int32_t aClipboardType) {
+ if (NS_WARN_IF(!GetDocument())) {
+ return NS_OK;
+ }
+
+ // The data will be initialized in InsertTextFromTransferable() if we're not
+ // an HTMLEditor. Therefore, we cannot dispatch "beforeinput" here.
+
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard =
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return rv;
+ }
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ Result<nsCOMPtr<nsITransferable>, nsresult> maybeTransferable =
+ EditorUtils::CreateTransferableForPlainText(*GetDocument());
+ if (maybeTransferable.isErr()) {
+ NS_WARNING("EditorUtils::CreateTransferableForPlainText() failed");
+ return maybeTransferable.unwrapErr();
+ }
+ nsCOMPtr<nsITransferable> transferable(maybeTransferable.unwrap());
+ if (NS_WARN_IF(!transferable)) {
+ NS_WARNING(
+ "EditorUtils::CreateTransferableForPlainText() returned nullptr, but "
+ "ignored");
+ return NS_OK; // XXX Why?
+ }
+ // Get the Data from the clipboard.
+ auto* windowContext = GetDocument()->GetWindowContext();
+ if (!windowContext) {
+ NS_WARNING("Editor didn't have document window context");
+ return NS_ERROR_FAILURE;
+ }
+ rv = clipboard->GetData(transferable, aClipboardType, windowContext);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsIClipboard::GetData() failed, but ignored");
+ return NS_OK; // XXX Why?
+ }
+ // XXX Why don't we check this first?
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+ rv = InsertTextFromTransferable(transferable);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::InsertTextFromTransferable() failed");
+ return rv;
+}
+
+nsresult TextEditor::HandlePasteTransferable(
+ AutoEditActionDataSetter& aEditActionData, nsITransferable& aTransferable) {
+ if (!IsModifiable()) {
+ return NS_OK;
+ }
+
+ // FYI: The data of beforeinput will be initialized in
+ // InsertTextFromTransferable(). Therefore, here does not touch
+ // aEditActionData.
+ nsresult rv = InsertTextFromTransferable(&aTransferable);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::InsertTextFromTransferable() failed");
+ return rv;
+}
+
+bool TextEditor::CanPaste(int32_t aClipboardType) const {
+ if (AreClipboardCommandsUnconditionallyEnabled()) {
+ return true;
+ }
+
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ return false;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard(
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return false;
+ }
+
+ // the flavors that we can deal with
+ AutoTArray<nsCString, 1> textEditorFlavors = {nsDependentCString(kTextMime)};
+
+ bool haveFlavors;
+ rv = clipboard->HasDataMatchingFlavors(textEditorFlavors, aClipboardType,
+ &haveFlavors);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIClipboard::HasDataMatchingFlavors() failed");
+ return NS_SUCCEEDED(rv) && haveFlavors;
+}
+
+bool TextEditor::CanPasteTransferable(nsITransferable* aTransferable) {
+ // can't paste if readonly
+ if (!IsModifiable()) {
+ return false;
+ }
+
+ // If |aTransferable| is null, assume that a paste will succeed.
+ if (!aTransferable) {
+ return true;
+ }
+
+ nsCOMPtr<nsISupports> data;
+ nsresult rv = aTransferable->GetTransferData(kTextMime, getter_AddRefs(data));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsITransferable::GetTransferData(kTextMime) failed");
+ return NS_SUCCEEDED(rv) && data;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/WSRunObject.cpp b/editor/libeditor/WSRunObject.cpp
new file mode 100644
index 0000000000..7149578be1
--- /dev/null
+++ b/editor/libeditor/WSRunObject.cpp
@@ -0,0 +1,4471 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WSRunObject.h"
+
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "ErrorList.h"
+#include "HTMLEditHelpers.h" // for MoveNodeResult, SplitNodeResult
+#include "HTMLEditor.h"
+#include "HTMLEditorNestedClasses.h" // for AutoMoveOneLineHandler
+#include "HTMLEditUtils.h"
+#include "SelectionState.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Casting.h"
+#include "mozilla/SelectionState.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/RangeUtils.h"
+#include "mozilla/StaticPrefs_dom.h" // for StaticPrefs::dom_*
+#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
+#include "mozilla/InternalMutationEvent.h"
+#include "mozilla/dom/AncestorIterator.h"
+
+#include "nsAString.h"
+#include "nsCRT.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIContent.h"
+#include "nsIContentInlines.h"
+#include "nsISupportsImpl.h"
+#include "nsRange.h"
+#include "nsString.h"
+#include "nsTextFragment.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
+using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
+
+template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPoint& aPoint) const;
+template WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ const EditorRawDOMPoint& aPoint) const;
+template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPoint& aPoint) const;
+template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
+ const EditorRawDOMPoint& aPoint) const;
+template EditorDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+template EditorRawDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+template EditorDOMPoint WSRunScanner::GetFirstVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+template EditorRawDOMPoint WSRunScanner::GetFirstVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+
+template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aScanStartPoint);
+template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ HTMLEditor& aHTMLEditor, const EditorDOMPointInText& aScanStartPoint);
+
+template WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorDOMPoint& aPoint, const Element* aEditingHost,
+ BlockInlineCheck aBlockInlineCheck);
+template WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorRawDOMPoint& aPoint, const Element* aEditingHost,
+ BlockInlineCheck aBlockInlineCheck);
+template WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorDOMPointInText& aPoint, const Element* aEditingHost,
+ BlockInlineCheck aBlockInlineCheck);
+
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint,
+ const EditorDOMPoint& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint,
+ const EditorRawDOMPoint& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint,
+ const EditorDOMPointInText& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint,
+ const EditorRawDOMPointInText& aPoint);
+
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint,
+ const EditorDOMPoint& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint,
+ const EditorRawDOMPoint& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint,
+ const EditorDOMPointInText& aPoint);
+NS_INSTANTIATE_CONST_METHOD_RETURNING_ANY_EDITOR_DOM_POINT(
+ WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint,
+ const EditorRawDOMPointInText& aPoint);
+
+Result<EditorDOMPoint, nsresult>
+WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit,
+ const Element& aSplittingBlockElement) {
+ if (NS_WARN_IF(!aPointToSplit.IsInContentNode()) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(aSplittingBlockElement)) ||
+ NS_WARN_IF(!EditorUtils::IsEditableContent(
+ *aPointToSplit.ContainerAs<nsIContent>(), EditorType::HTML))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // The container of aPointToSplit may be not splittable, e.g., selection
+ // may be collapsed **in** a `<br>` element or a comment node. So, look
+ // for splittable point with climbing the tree up.
+ EditorDOMPoint pointToSplit(aPointToSplit);
+ for (nsIContent* content : aPointToSplit.ContainerAs<nsIContent>()
+ ->InclusiveAncestorsOfType<nsIContent>()) {
+ if (content == &aSplittingBlockElement) {
+ break;
+ }
+ if (HTMLEditUtils::IsSplittableNode(*content)) {
+ break;
+ }
+ pointToSplit.Set(content);
+ }
+
+ {
+ AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &pointToSplit);
+
+ nsresult rv = WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit(aHTMLEditor,
+ pointToSplit);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit() failed");
+ return Err(rv);
+ }
+ }
+
+ if (NS_WARN_IF(!pointToSplit.IsInContentNode()) ||
+ NS_WARN_IF(
+ !pointToSplit.ContainerAs<nsIContent>()->IsInclusiveDescendantOf(
+ &aSplittingBlockElement)) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(aSplittingBlockElement)) ||
+ NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(
+ *pointToSplit.ContainerAs<nsIContent>()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ return pointToSplit;
+}
+
+// static
+Result<EditActionResult, nsresult> WhiteSpaceVisibilityKeeper::
+ MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement(
+ HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
+ Element& aRightBlockElement, const EditorDOMPoint& aAtRightBlockChild,
+ const Maybe<nsAtom*>& aListElementTagName,
+ const HTMLBRElement* aPrecedingInvisibleBRElement,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(
+ EditorUtils::IsDescendantOf(aLeftBlockElement, aRightBlockElement));
+ MOZ_ASSERT(&aRightBlockElement == aAtRightBlockChild.GetContainer());
+
+ // NOTE: This method may extend deletion range:
+ // - to delete invisible white-spaces at end of aLeftBlockElement
+ // - to delete invisible white-spaces at start of
+ // afterRightBlockChild.GetChild()
+ // - to delete invisible white-spaces before afterRightBlockChild.GetChild()
+ // - to delete invisible `<br>` element at end of aLeftBlockElement
+
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ aHTMLEditor, EditorDOMPoint::AtEndOf(aLeftBlockElement));
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+
+ // Check whether aLeftBlockElement is a descendant of aRightBlockElement.
+ if (aHTMLEditor.MayHaveMutationEventListeners()) {
+ EditorDOMPoint leftBlockContainingPointInRightBlockElement;
+ if (aHTMLEditor.MayHaveMutationEventListeners() &&
+ MOZ_UNLIKELY(!EditorUtils::IsDescendantOf(
+ aLeftBlockElement, aRightBlockElement,
+ &leftBlockContainingPointInRightBlockElement))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at end of left block element caused "
+ "moving the left block element outside the right block element");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(leftBlockContainingPointInRightBlockElement !=
+ aAtRightBlockChild)) {
+ NS_WARNING(
+ "Deleting invisible whitespace at end of left block element caused "
+ "changing the left block element in the right block element");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(aRightBlockElement,
+ EditorType::HTML))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at end of left block element caused "
+ "making the right block element non-editable");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(aLeftBlockElement,
+ EditorType::HTML))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at end of left block element caused "
+ "making the left block element non-editable");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ OwningNonNull<Element> rightBlockElement = aRightBlockElement;
+ EditorDOMPoint afterRightBlockChild = aAtRightBlockChild.NextPoint();
+ {
+ // We can't just track rightBlockElement because it's an Element.
+ AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(),
+ &afterRightBlockChild);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ aHTMLEditor, afterRightBlockChild);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+
+ // XXX AutoTrackDOMPoint instance, tracker, hasn't been destroyed here.
+ // Do we really need to do update rightBlockElement here??
+ // XXX And afterRightBlockChild.GetContainerAs<Element>() always returns
+ // an element pointer so that probably here should not use
+ // accessors of EditorDOMPoint, should use DOM API directly instead.
+ if (afterRightBlockChild.GetContainerAs<Element>()) {
+ rightBlockElement = *afterRightBlockChild.ContainerAs<Element>();
+ } else if (NS_WARN_IF(
+ !afterRightBlockChild.GetContainerParentAs<Element>())) {
+ return Err(NS_ERROR_UNEXPECTED);
+ } else {
+ rightBlockElement = *afterRightBlockChild.GetContainerParentAs<Element>();
+ }
+ }
+
+ // Do br adjustment.
+ RefPtr<HTMLBRElement> invisibleBRElementAtEndOfLeftBlockElement =
+ WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
+ aHTMLEditor.ComputeEditingHost(),
+ EditorDOMPoint::AtEndOf(aLeftBlockElement),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ NS_ASSERTION(
+ aPrecedingInvisibleBRElement == invisibleBRElementAtEndOfLeftBlockElement,
+ "The preceding invisible BR element computation was different");
+ auto ret = EditActionResult::IgnoredResult();
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+ // NOTE: Keep syncing with CanMergeLeftAndRightBlockElements() of
+ // AutoInclusiveAncestorBlockElementsJoiner.
+ if (NS_WARN_IF(aListElementTagName.isSome())) {
+ // Since 2002, here was the following comment:
+ // > The idea here is to take all children in rightListElement that are
+ // > past offset, and pull them into leftlistElement.
+ // However, this has never been performed because we are here only when
+ // neither left list nor right list is a descendant of the other but
+ // in such case, getting a list item in the right list node almost
+ // always failed since a variable for offset of
+ // rightListElement->GetChildAt() was not initialized. So, it might be
+ // a bug, but we should keep this traditional behavior for now. If you
+ // find when we get here, please remove this comment if we don't need to
+ // do it. Otherwise, please move children of the right list node to the
+ // end of the left list node.
+
+ // XXX Although, we do nothing here, but for keeping traditional
+ // behavior, we should mark as handled.
+ ret.MarkAsHandled();
+ } else {
+ // XXX Why do we ignore the result of AutoMoveOneLineHandler::Run()?
+ NS_ASSERTION(rightBlockElement == afterRightBlockChild.GetContainer(),
+ "The relation is not guaranteed but assumed");
+#ifdef DEBUG
+ Result<bool, nsresult> firstLineHasContent =
+ HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
+ EditorDOMPoint(rightBlockElement, afterRightBlockChild.Offset()),
+ aEditingHost);
+#endif // #ifdef DEBUG
+ HTMLEditor::AutoMoveOneLineHandler lineMoverToEndOfLeftBlock(
+ aLeftBlockElement);
+ nsresult rv = lineMoverToEndOfLeftBlock.Prepare(
+ aHTMLEditor,
+ EditorDOMPoint(rightBlockElement, afterRightBlockChild.Offset()),
+ aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoMoveOneLineHandler::Prepare() failed");
+ return Err(rv);
+ }
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ lineMoverToEndOfLeftBlock.Run(aHTMLEditor, aEditingHost);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("AutoMoveOneLineHandler::Run() failed");
+ return moveNodeResult.propagateErr();
+ }
+
+#ifdef DEBUG
+ MOZ_ASSERT(!firstLineHasContent.isErr());
+ if (firstLineHasContent.inspect()) {
+ NS_ASSERTION(moveNodeResult.inspect().Handled(),
+ "Failed to consider whether moving or not something");
+ } else {
+ NS_ASSERTION(moveNodeResult.inspect().Ignored(),
+ "Failed to consider whether moving or not something");
+ }
+#endif // #ifdef DEBUG
+
+ // We don't need to update selection here because of dontChangeMySelection
+ // above.
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ ret |= moveNodeResult.unwrap();
+ // Now, all children of rightBlockElement were moved to leftBlockElement.
+ // So, afterRightBlockChild is now invalid.
+ afterRightBlockChild.Clear();
+ }
+
+ if (!invisibleBRElementAtEndOfLeftBlockElement ||
+ !invisibleBRElementAtEndOfLeftBlockElement->IsInComposedDoc()) {
+ return ret;
+ }
+
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ *invisibleBRElementAtEndOfLeftBlockElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed, but ignored");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+// static
+Result<EditActionResult, nsresult> WhiteSpaceVisibilityKeeper::
+ MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement(
+ HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
+ Element& aRightBlockElement, const EditorDOMPoint& aAtLeftBlockChild,
+ nsIContent& aLeftContentInBlock,
+ const Maybe<nsAtom*>& aListElementTagName,
+ const HTMLBRElement* aPrecedingInvisibleBRElement,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(
+ EditorUtils::IsDescendantOf(aRightBlockElement, aLeftBlockElement));
+ MOZ_ASSERT(
+ &aLeftBlockElement == &aLeftContentInBlock ||
+ EditorUtils::IsDescendantOf(aLeftContentInBlock, aLeftBlockElement));
+ MOZ_ASSERT(&aLeftBlockElement == aAtLeftBlockChild.GetContainer());
+
+ // NOTE: This method may extend deletion range:
+ // - to delete invisible white-spaces at start of aRightBlockElement
+ // - to delete invisible white-spaces before aRightBlockElement
+ // - to delete invisible white-spaces at start of aAtLeftBlockChild.GetChild()
+ // - to delete invisible white-spaces before aAtLeftBlockChild.GetChild()
+ // - to delete invisible `<br>` element before aAtLeftBlockChild.GetChild()
+
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0));
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+
+ // Check whether aRightBlockElement is a descendant of aLeftBlockElement.
+ if (aHTMLEditor.MayHaveMutationEventListeners()) {
+ EditorDOMPoint rightBlockContainingPointInLeftBlockElement;
+ if (aHTMLEditor.MayHaveMutationEventListeners() &&
+ MOZ_UNLIKELY(!EditorUtils::IsDescendantOf(
+ aRightBlockElement, aLeftBlockElement,
+ &rightBlockContainingPointInLeftBlockElement))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at start of right block element "
+ "caused moving the right block element outside the left block "
+ "element");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(rightBlockContainingPointInLeftBlockElement !=
+ aAtLeftBlockChild)) {
+ NS_WARNING(
+ "Deleting invisible whitespace at start of right block element "
+ "caused changing the right block element position in the left block "
+ "element");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(aLeftBlockElement,
+ EditorType::HTML))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at start of right block element "
+ "caused making the left block element non-editable");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(aRightBlockElement,
+ EditorType::HTML))) {
+ NS_WARNING(
+ "Deleting invisible whitespace at start of right block element "
+ "caused making the right block element non-editable");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ OwningNonNull<Element> originalLeftBlockElement = aLeftBlockElement;
+ OwningNonNull<Element> leftBlockElement = aLeftBlockElement;
+ EditorDOMPoint atLeftBlockChild(aAtLeftBlockChild);
+ {
+ // We can't just track leftBlockElement because it's an Element, so track
+ // something else.
+ AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &atLeftBlockChild);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ aHTMLEditor, EditorDOMPoint(atLeftBlockChild.GetContainer(),
+ atLeftBlockChild.Offset()));
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+ if (MOZ_UNLIKELY(!atLeftBlockChild.IsSetAndValid())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() caused "
+ "unexpected DOM tree");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // XXX atLeftBlockChild.GetContainerAs<Element>() should always return
+ // an element pointer so that probably here should not use
+ // accessors of EditorDOMPoint, should use DOM API directly instead.
+ if (Element* nearestAncestor =
+ atLeftBlockChild.GetContainerOrContainerParentElement()) {
+ leftBlockElement = *nearestAncestor;
+ } else {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ // Do br adjustment.
+ RefPtr<HTMLBRElement> invisibleBRElementBeforeLeftBlockElement =
+ WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
+ aHTMLEditor.ComputeEditingHost(), atLeftBlockChild,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ NS_ASSERTION(
+ aPrecedingInvisibleBRElement == invisibleBRElementBeforeLeftBlockElement,
+ "The preceding invisible BR element computation was different");
+ auto ret = EditActionResult::IgnoredResult();
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+ // NOTE: Keep syncing with CanMergeLeftAndRightBlockElements() of
+ // AutoInclusiveAncestorBlockElementsJoiner.
+ if (aListElementTagName.isSome()) {
+ // XXX Why do we ignore the error from MoveChildrenWithTransaction()?
+ MOZ_ASSERT(originalLeftBlockElement == atLeftBlockChild.GetContainer(),
+ "This is not guaranteed, but assumed");
+#ifdef DEBUG
+ Result<bool, nsresult> rightBlockHasContent =
+ aHTMLEditor.CanMoveChildren(aRightBlockElement, aLeftBlockElement);
+#endif // #ifdef DEBUG
+ // TODO: Stop using HTMLEditor::PreserveWhiteSpaceStyle::No due to no tests.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveChildrenWithTransaction(
+ aRightBlockElement,
+ EditorDOMPoint(atLeftBlockChild.GetContainer(),
+ atLeftBlockChild.Offset()),
+ HTMLEditor::PreserveWhiteSpaceStyle::No,
+ HTMLEditor::RemoveIfCommentNode::Yes);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ if (NS_WARN_IF(moveNodeResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(moveNodeResult.unwrapErr());
+ }
+ NS_WARNING(
+ "HTMLEditor::MoveChildrenWithTransaction() failed, but ignored");
+ } else {
+#ifdef DEBUG
+ MOZ_ASSERT(!rightBlockHasContent.isErr());
+ if (rightBlockHasContent.inspect()) {
+ NS_ASSERTION(moveNodeResult.inspect().Handled(),
+ "Failed to consider whether moving or not children");
+ } else {
+ NS_ASSERTION(moveNodeResult.inspect().Ignored(),
+ "Failed to consider whether moving or not children");
+ }
+#endif // #ifdef DEBUG
+ // We don't need to update selection here because of dontChangeMySelection
+ // above.
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ ret |= moveNodeResult.unwrap();
+ }
+ // atLeftBlockChild was moved to rightListElement. So, it's invalid now.
+ atLeftBlockChild.Clear();
+ } else {
+ // Left block is a parent of right block, and the parent of the previous
+ // visible content. Right block is a child and contains the contents we
+ // want to move.
+
+ EditorDOMPoint pointToMoveFirstLineContent;
+ if (&aLeftContentInBlock == leftBlockElement) {
+ // We are working with valid HTML, aLeftContentInBlock is a block element,
+ // and is therefore allowed to contain aRightBlockElement. This is the
+ // simple case, we will simply move the content in aRightBlockElement
+ // out of its block.
+ pointToMoveFirstLineContent = atLeftBlockChild;
+ MOZ_ASSERT(pointToMoveFirstLineContent.GetContainer() ==
+ &aLeftBlockElement);
+ } else {
+ if (NS_WARN_IF(!aLeftContentInBlock.IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // We try to work as well as possible with HTML that's already invalid.
+ // Although "right block" is a block, and a block must not be contained
+ // in inline elements, reality is that broken documents do exist. The
+ // DIRECT parent of "left NODE" might be an inline element. Previous
+ // versions of this code skipped inline parents until the first block
+ // parent was found (and used "left block" as the destination).
+ // However, in some situations this strategy moves the content to an
+ // unexpected position. (see bug 200416) The new idea is to make the
+ // moving content a sibling, next to the previous visible content.
+ pointToMoveFirstLineContent.SetAfter(&aLeftContentInBlock);
+ if (NS_WARN_IF(!pointToMoveFirstLineContent.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ MOZ_ASSERT(pointToMoveFirstLineContent.IsSetAndValid());
+
+ // Because we don't want the moving content to receive the style of the
+ // previous content, we split the previous content's style.
+
+#ifdef DEBUG
+ Result<bool, nsresult> firstLineHasContent =
+ HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
+ EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost);
+#endif // #ifdef DEBUG
+
+ if (&aLeftContentInBlock != &aEditingHost) {
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ aHTMLEditor.SplitAncestorStyledInlineElementsAt(
+ pointToMoveFirstLineContent, EditorInlineStyle::RemoveAllStyles(),
+ HTMLEditor::SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
+ return splitNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ nsresult rv = unwrappedSplitNodeResult.SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ if (!unwrappedSplitNodeResult.DidSplit()) {
+ // If nothing was split, we should move the first line content to after
+ // the parent inline elements.
+ for (EditorDOMPoint parentPoint = pointToMoveFirstLineContent;
+ pointToMoveFirstLineContent.IsEndOfContainer() &&
+ pointToMoveFirstLineContent.IsInContentNode();
+ pointToMoveFirstLineContent = EditorDOMPoint::After(
+ *pointToMoveFirstLineContent.ContainerAs<nsIContent>())) {
+ if (pointToMoveFirstLineContent.GetContainer() ==
+ &aLeftBlockElement ||
+ NS_WARN_IF(pointToMoveFirstLineContent.GetContainer() ==
+ &aEditingHost)) {
+ break;
+ }
+ }
+ if (NS_WARN_IF(!pointToMoveFirstLineContent.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ } else if (unwrappedSplitNodeResult.Handled()) {
+ // If se split something, we should move the first line contents before
+ // the right elements.
+ if (nsIContent* nextContentAtSplitPoint =
+ unwrappedSplitNodeResult.GetNextContent()) {
+ pointToMoveFirstLineContent.Set(nextContentAtSplitPoint);
+ if (NS_WARN_IF(!pointToMoveFirstLineContent.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ } else {
+ pointToMoveFirstLineContent =
+ unwrappedSplitNodeResult.AtSplitPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToMoveFirstLineContent.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ }
+ MOZ_DIAGNOSTIC_ASSERT(pointToMoveFirstLineContent.IsSetAndValid());
+ }
+
+ HTMLEditor::AutoMoveOneLineHandler lineMoverToPoint(
+ pointToMoveFirstLineContent);
+ nsresult rv = lineMoverToPoint.Prepare(
+ aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoMoveOneLineHandler::Prepare() failed");
+ return Err(rv);
+ }
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ lineMoverToPoint.Run(aHTMLEditor, aEditingHost);
+ if (moveNodeResult.isErr()) {
+ NS_WARNING("AutoMoveOneLineHandler::Run() failed");
+ return moveNodeResult.propagateErr();
+ }
+
+#ifdef DEBUG
+ MOZ_ASSERT(!firstLineHasContent.isErr());
+ if (firstLineHasContent.inspect()) {
+ NS_ASSERTION(moveNodeResult.inspect().Handled(),
+ "Failed to consider whether moving or not something");
+ } else {
+ NS_ASSERTION(moveNodeResult.inspect().Ignored(),
+ "Failed to consider whether moving or not something");
+ }
+#endif // #ifdef DEBUG
+
+ // We don't need to update selection here because of dontChangeMySelection
+ // above.
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ ret |= moveNodeResult.unwrap();
+ }
+
+ if (!invisibleBRElementBeforeLeftBlockElement ||
+ !invisibleBRElementBeforeLeftBlockElement->IsInComposedDoc()) {
+ return ret;
+ }
+
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ *invisibleBRElementBeforeLeftBlockElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed, but ignored");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+// static
+Result<EditActionResult, nsresult> WhiteSpaceVisibilityKeeper::
+ MergeFirstLineOfRightBlockElementIntoLeftBlockElement(
+ HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
+ Element& aRightBlockElement, const Maybe<nsAtom*>& aListElementTagName,
+ const HTMLBRElement* aPrecedingInvisibleBRElement,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(
+ !EditorUtils::IsDescendantOf(aLeftBlockElement, aRightBlockElement));
+ MOZ_ASSERT(
+ !EditorUtils::IsDescendantOf(aRightBlockElement, aLeftBlockElement));
+
+ // NOTE: This method may extend deletion range:
+ // - to delete invisible white-spaces at end of aLeftBlockElement
+ // - to delete invisible white-spaces at start of aRightBlockElement
+ // - to delete invisible `<br>` element at end of aLeftBlockElement
+
+ // Adjust white-space at block boundaries
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
+ aHTMLEditor,
+ EditorDOMRange(EditorDOMPoint::AtEndOf(aLeftBlockElement),
+ EditorDOMPoint(&aRightBlockElement, 0)),
+ aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange() "
+ "failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret point suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+ // Do br adjustment.
+ RefPtr<HTMLBRElement> invisibleBRElementAtEndOfLeftBlockElement =
+ WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
+ aHTMLEditor.ComputeEditingHost(),
+ EditorDOMPoint::AtEndOf(aLeftBlockElement),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ NS_ASSERTION(
+ aPrecedingInvisibleBRElement == invisibleBRElementAtEndOfLeftBlockElement,
+ "The preceding invisible BR element computation was different");
+ auto ret = EditActionResult::IgnoredResult();
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+ if (aListElementTagName.isSome() ||
+ // TODO: We should stop merging entire blocks even if they have same
+ // white-space style because Chrome behave so. However, it's risky to
+ // change our behavior in the major cases so that we should do it in
+ // a bug to manage only the change.
+ (aLeftBlockElement.NodeInfo()->NameAtom() ==
+ aRightBlockElement.NodeInfo()->NameAtom() &&
+ EditorUtils::GetComputedWhiteSpaceStyles(aLeftBlockElement) ==
+ EditorUtils::GetComputedWhiteSpaceStyles(aRightBlockElement))) {
+ // Nodes are same type. merge them.
+ EditorDOMPoint atFirstChildOfRightNode;
+ nsresult rv = aHTMLEditor.JoinNearestEditableNodesWithTransaction(
+ aLeftBlockElement, aRightBlockElement, &atFirstChildOfRightNode);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::JoinNearestEditableNodesWithTransaction()"
+ " failed, but ignored");
+ if (aListElementTagName.isSome() && atFirstChildOfRightNode.IsSet()) {
+ Result<CreateElementResult, nsresult> convertListTypeResult =
+ aHTMLEditor.ChangeListElementType(
+ aRightBlockElement, MOZ_KnownLive(*aListElementTagName.ref()),
+ *nsGkAtoms::li);
+ if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
+ if (NS_WARN_IF(convertListTypeResult.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING("HTMLEditor::ChangeListElementType() failed, but ignored");
+ } else {
+ // There is AutoTransactionConserveSelection above, therefore, we don't
+ // need to update selection here.
+ convertListTypeResult.inspect().IgnoreCaretPointSuggestion();
+ }
+ }
+ ret.MarkAsHandled();
+ } else {
+#ifdef DEBUG
+ Result<bool, nsresult> firstLineHasContent =
+ HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
+ EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost);
+#endif // #ifdef DEBUG
+
+ // Nodes are dissimilar types.
+ HTMLEditor::AutoMoveOneLineHandler lineMoverToEndOfLeftBlock(
+ aLeftBlockElement);
+ nsresult rv = lineMoverToEndOfLeftBlock.Prepare(
+ aHTMLEditor, EditorDOMPoint(&aRightBlockElement, 0u), aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoMoveOneLineHandler::Prepare() failed");
+ return Err(rv);
+ }
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ lineMoverToEndOfLeftBlock.Run(aHTMLEditor, aEditingHost);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("AutoMoveOneLineHandler::Run() failed");
+ return moveNodeResult.propagateErr();
+ }
+
+#ifdef DEBUG
+ MOZ_ASSERT(!firstLineHasContent.isErr());
+ if (firstLineHasContent.inspect()) {
+ NS_ASSERTION(moveNodeResult.inspect().Handled(),
+ "Failed to consider whether moving or not something");
+ } else {
+ NS_ASSERTION(moveNodeResult.inspect().Ignored(),
+ "Failed to consider whether moving or not something");
+ }
+#endif // #ifdef DEBUG
+
+ // We don't need to update selection here because of dontChangeMySelection
+ // above.
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ ret |= moveNodeResult.unwrap();
+ }
+
+ if (!invisibleBRElementAtEndOfLeftBlockElement ||
+ !invisibleBRElementAtEndOfLeftBlockElement->IsInComposedDoc()) {
+ ret.MarkAsHandled();
+ return ret;
+ }
+
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
+ *invisibleBRElementAtEndOfLeftBlockElement);
+ // XXX In other top level if blocks, the result of
+ // DeleteNodeWithTransaction() is ignored. Why does only this result
+ // is respected?
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+// static
+Result<CreateElementResult, nsresult>
+WhiteSpaceVisibilityKeeper::InsertBRElement(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert,
+ const Element& aEditingHost) {
+ if (MOZ_UNLIKELY(NS_WARN_IF(!aPointToInsert.IsSet()))) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ // MOOSE: for now, we always assume non-PRE formatting. Fix this later.
+ // meanwhile, the pre case is handled in HandleInsertText() in
+ // HTMLEditSubActionHandler.cpp
+
+ TextFragmentData textFragmentDataAtInsertionPoint(
+ aPointToInsert, &aEditingHost, BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(
+ NS_WARN_IF(!textFragmentDataAtInsertionPoint.IsInitialized()))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorDOMRange invisibleLeadingWhiteSpaceRangeOfNewLine =
+ textFragmentDataAtInsertionPoint
+ .GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(aPointToInsert);
+ EditorDOMRange invisibleTrailingWhiteSpaceRangeOfCurrentLine =
+ textFragmentDataAtInsertionPoint
+ .GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(aPointToInsert);
+ const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpaces =
+ !invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned() ||
+ !invisibleTrailingWhiteSpaceRangeOfCurrentLine.IsPositioned()
+ ? Some(textFragmentDataAtInsertionPoint.VisibleWhiteSpacesDataRef())
+ : Nothing();
+ const PointPosition pointPositionWithVisibleWhiteSpaces =
+ visibleWhiteSpaces.isSome() && visibleWhiteSpaces.ref().IsInitialized()
+ ? visibleWhiteSpaces.ref().ComparePoint(aPointToInsert)
+ : PointPosition::NotInSameDOMTree;
+
+ EditorDOMPoint pointToInsert(aPointToInsert);
+ EditorDOMPoint atNBSPReplaceableWithSP;
+ if (!invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned() &&
+ (pointPositionWithVisibleWhiteSpaces == PointPosition::MiddleOfFragment ||
+ pointPositionWithVisibleWhiteSpaces == PointPosition::EndOfFragment)) {
+ atNBSPReplaceableWithSP =
+ textFragmentDataAtInsertionPoint
+ .GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ pointToInsert)
+ .To<EditorDOMPoint>();
+ }
+
+ {
+ if (invisibleTrailingWhiteSpaceRangeOfCurrentLine.IsPositioned()) {
+ if (!invisibleTrailingWhiteSpaceRangeOfCurrentLine.Collapsed()) {
+ // XXX Why don't we remove all of the invisible white-spaces?
+ MOZ_ASSERT(invisibleTrailingWhiteSpaceRangeOfCurrentLine.StartRef() ==
+ pointToInsert);
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackEndOfLineNBSP(aHTMLEditor.RangeUpdaterRef(),
+ &atNBSPReplaceableWithSP);
+ AutoTrackDOMRange trackLeadingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleLeadingWhiteSpaceRangeOfNewLine);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.StartRef(),
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.Clear();
+ }
+ }
+ // If new line will start with visible white-spaces, it needs to be start
+ // with an NBSP.
+ else if (pointPositionWithVisibleWhiteSpaces ==
+ PointPosition::StartOfFragment ||
+ pointPositionWithVisibleWhiteSpaces ==
+ PointPosition::MiddleOfFragment) {
+ auto atNextCharOfInsertionPoint =
+ textFragmentDataAtInsertionPoint
+ .GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ pointToInsert);
+ if (atNextCharOfInsertionPoint.IsSet() &&
+ !atNextCharOfInsertionPoint.IsEndOfContainer() &&
+ atNextCharOfInsertionPoint.IsCharCollapsibleASCIISpace()) {
+ const EditorDOMPointInText atPreviousCharOfNextCharOfInsertionPoint =
+ textFragmentDataAtInsertionPoint.GetPreviousEditableCharPoint(
+ atNextCharOfInsertionPoint);
+ if (!atPreviousCharOfNextCharOfInsertionPoint.IsSet() ||
+ atPreviousCharOfNextCharOfInsertionPoint.IsEndOfContainer() ||
+ !atPreviousCharOfNextCharOfInsertionPoint.IsCharASCIISpace()) {
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackEndOfLineNBSP(aHTMLEditor.RangeUpdaterRef(),
+ &atNBSPReplaceableWithSP);
+ AutoTrackDOMRange trackLeadingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleLeadingWhiteSpaceRangeOfNewLine);
+ // We are at start of non-nbsps. Convert to a single nbsp.
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ textFragmentDataAtInsertionPoint
+ .GetEndOfCollapsibleASCIIWhiteSpaces(
+ atNextCharOfInsertionPoint, nsIEditor::eNone);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor,
+ EditorDOMRangeInTexts(atNextCharOfInsertionPoint,
+ endOfCollapsibleASCIIWhiteSpaces),
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "ReplaceTextAndRemoveEmptyTextNodes() failed");
+ return Err(rv);
+ }
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.Clear();
+ }
+ }
+ }
+
+ if (invisibleLeadingWhiteSpaceRangeOfNewLine.IsPositioned()) {
+ if (!invisibleLeadingWhiteSpaceRangeOfNewLine.Collapsed()) {
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ // XXX Why don't we remove all of the invisible white-spaces?
+ MOZ_ASSERT(invisibleLeadingWhiteSpaceRangeOfNewLine.EndRef() ==
+ pointToInsert);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ invisibleLeadingWhiteSpaceRangeOfNewLine.StartRef(),
+ invisibleLeadingWhiteSpaceRangeOfNewLine.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ atNBSPReplaceableWithSP.Clear();
+ invisibleLeadingWhiteSpaceRangeOfNewLine.Clear();
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.Clear();
+ }
+ }
+ // If the `<br>` element is put immediately after an NBSP, it should be
+ // replaced with an ASCII white-space.
+ else if (atNBSPReplaceableWithSP.IsInTextNode()) {
+ const EditorDOMPointInText atNBSPReplacedWithASCIIWhiteSpace =
+ atNBSPReplaceableWithSP.AsInText();
+ if (!atNBSPReplacedWithASCIIWhiteSpace.IsEndOfContainer() &&
+ atNBSPReplacedWithASCIIWhiteSpace.IsCharNBSP()) {
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(
+ *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs<Text>()),
+ atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ atNBSPReplaceableWithSP.Clear();
+ invisibleLeadingWhiteSpaceRangeOfNewLine.Clear();
+ invisibleTrailingWhiteSpaceRangeOfCurrentLine.Clear();
+ }
+ }
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ aHTMLEditor.InsertBRElement(WithTransaction::Yes, pointToInsert);
+ NS_WARNING_ASSERTION(
+ insertBRElementResult.isOk(),
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes, eNone) failed");
+ return insertBRElementResult;
+}
+
+// static
+Result<InsertTextResult, nsresult> WhiteSpaceVisibilityKeeper::ReplaceText(
+ HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert,
+ const EditorDOMRange& aRangeToBeReplaced, const Element& aEditingHost) {
+ // MOOSE: for now, we always assume non-PRE formatting. Fix this later.
+ // meanwhile, the pre case is handled in HandleInsertText() in
+ // HTMLEditSubActionHandler.cpp
+
+ // MOOSE: for now, just getting the ws logic straight. This implementation
+ // is very slow. Will need to replace edit rules impl with a more efficient
+ // text sink here that does the minimal amount of searching/replacing/copying
+
+ if (aStringToInsert.IsEmpty()) {
+ MOZ_ASSERT(aRangeToBeReplaced.Collapsed());
+ return InsertTextResult();
+ }
+
+ TextFragmentData textFragmentDataAtStart(
+ aRangeToBeReplaced.StartRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool isInsertionPointEqualsOrIsBeforeStartOfText =
+ aRangeToBeReplaced.StartRef().EqualsOrIsBefore(
+ textFragmentDataAtStart.StartRef());
+ TextFragmentData textFragmentDataAtEnd =
+ aRangeToBeReplaced.Collapsed()
+ ? textFragmentDataAtStart
+ : TextFragmentData(aRangeToBeReplaced.EndRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized()))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool isInsertionPointEqualsOrAfterEndOfText =
+ textFragmentDataAtEnd.EndRef().EqualsOrIsBefore(
+ aRangeToBeReplaced.EndRef());
+
+ EditorDOMRange invisibleLeadingWhiteSpaceRangeAtStart =
+ textFragmentDataAtStart
+ .GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(
+ aRangeToBeReplaced.StartRef());
+ const bool isInvisibleLeadingWhiteSpaceRangeAtStartPositioned =
+ invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned();
+ EditorDOMRange invisibleTrailingWhiteSpaceRangeAtEnd =
+ textFragmentDataAtEnd.GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(
+ aRangeToBeReplaced.EndRef());
+ const bool isInvisibleTrailingWhiteSpaceRangeAtEndPositioned =
+ invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned();
+ const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpacesAtStart =
+ !isInvisibleLeadingWhiteSpaceRangeAtStartPositioned
+ ? Some(textFragmentDataAtStart.VisibleWhiteSpacesDataRef())
+ : Nothing();
+ const PointPosition pointPositionWithVisibleWhiteSpacesAtStart =
+ visibleWhiteSpacesAtStart.isSome() &&
+ visibleWhiteSpacesAtStart.ref().IsInitialized()
+ ? visibleWhiteSpacesAtStart.ref().ComparePoint(
+ aRangeToBeReplaced.StartRef())
+ : PointPosition::NotInSameDOMTree;
+ const Maybe<const VisibleWhiteSpacesData> visibleWhiteSpacesAtEnd =
+ !isInvisibleTrailingWhiteSpaceRangeAtEndPositioned
+ ? Some(textFragmentDataAtEnd.VisibleWhiteSpacesDataRef())
+ : Nothing();
+ const PointPosition pointPositionWithVisibleWhiteSpacesAtEnd =
+ visibleWhiteSpacesAtEnd.isSome() &&
+ visibleWhiteSpacesAtEnd.ref().IsInitialized()
+ ? visibleWhiteSpacesAtEnd.ref().ComparePoint(
+ aRangeToBeReplaced.EndRef())
+ : PointPosition::NotInSameDOMTree;
+
+ EditorDOMPoint pointToPutCaret;
+ EditorDOMPoint pointToInsert(aRangeToBeReplaced.StartRef());
+ EditorDOMPoint atNBSPReplaceableWithSP;
+ if (!invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned() &&
+ (pointPositionWithVisibleWhiteSpacesAtStart ==
+ PointPosition::MiddleOfFragment ||
+ pointPositionWithVisibleWhiteSpacesAtStart ==
+ PointPosition::EndOfFragment)) {
+ atNBSPReplaceableWithSP =
+ textFragmentDataAtStart
+ .GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ pointToInsert)
+ .To<EditorDOMPoint>();
+ }
+ nsAutoString theString(aStringToInsert);
+ {
+ if (invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned()) {
+ if (!invisibleTrailingWhiteSpaceRangeAtEnd.Collapsed()) {
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackPrecedingNBSP(aHTMLEditor.RangeUpdaterRef(),
+ &atNBSPReplaceableWithSP);
+ AutoTrackDOMRange trackInvisibleLeadingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleLeadingWhiteSpaceRangeAtStart);
+ AutoTrackDOMRange trackInvisibleTrailingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleTrailingWhiteSpaceRangeAtEnd);
+ // XXX Why don't we remove all of the invisible white-spaces?
+ MOZ_ASSERT(invisibleTrailingWhiteSpaceRangeAtEnd.StartRef() ==
+ pointToInsert);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ invisibleTrailingWhiteSpaceRangeAtEnd.StartRef(),
+ invisibleTrailingWhiteSpaceRangeAtEnd.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ }
+ // Replace an NBSP at inclusive next character of replacing range to an
+ // ASCII white-space if inserting into a visible white-space sequence.
+ // XXX With modifying the inserting string later, this creates a line break
+ // opportunity after the inserting string, but this causes
+ // inconsistent result with inserting order. E.g., type white-space
+ // n times with various order.
+ else if (pointPositionWithVisibleWhiteSpacesAtEnd ==
+ PointPosition::StartOfFragment ||
+ pointPositionWithVisibleWhiteSpacesAtEnd ==
+ PointPosition::MiddleOfFragment) {
+ EditorDOMPointInText atNBSPReplacedWithASCIIWhiteSpace =
+ textFragmentDataAtEnd
+ .GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ aRangeToBeReplaced.EndRef());
+ if (atNBSPReplacedWithASCIIWhiteSpace.IsSet()) {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackPrecedingNBSP(aHTMLEditor.RangeUpdaterRef(),
+ &atNBSPReplaceableWithSP);
+ AutoTrackDOMRange trackInvisibleLeadingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleLeadingWhiteSpaceRangeAtStart);
+ AutoTrackDOMRange trackInvisibleTrailingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleTrailingWhiteSpaceRangeAtEnd);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(
+ *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs<Text>()),
+ atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ }
+ }
+
+ if (invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned()) {
+ if (!invisibleLeadingWhiteSpaceRangeAtStart.Collapsed()) {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMRange trackInvisibleTrailingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleTrailingWhiteSpaceRangeAtEnd);
+ // XXX Why don't we remove all of the invisible white-spaces?
+ MOZ_ASSERT(invisibleLeadingWhiteSpaceRangeAtStart.EndRef() ==
+ pointToInsert);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ invisibleLeadingWhiteSpaceRangeAtStart.StartRef(),
+ invisibleLeadingWhiteSpaceRangeAtStart.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ atNBSPReplaceableWithSP.Clear();
+ invisibleLeadingWhiteSpaceRangeAtStart.Clear();
+ }
+ }
+ // Replace an NBSP at previous character of insertion point to an ASCII
+ // white-space if inserting into a visible white-space sequence.
+ // XXX With modifying the inserting string later, this creates a line break
+ // opportunity before the inserting string, but this causes
+ // inconsistent result with inserting order. E.g., type white-space
+ // n times with various order.
+ else if (atNBSPReplaceableWithSP.IsInTextNode()) {
+ EditorDOMPointInText atNBSPReplacedWithASCIIWhiteSpace =
+ atNBSPReplaceableWithSP.AsInText();
+ if (!atNBSPReplacedWithASCIIWhiteSpace.IsEndOfContainer() &&
+ atNBSPReplacedWithASCIIWhiteSpace.IsCharNBSP()) {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ AutoTrackDOMPoint trackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMRange trackInvisibleTrailingWhiteSpaceRange(
+ aHTMLEditor.RangeUpdaterRef(),
+ &invisibleTrailingWhiteSpaceRangeAtEnd);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(
+ *atNBSPReplacedWithASCIIWhiteSpace.ContainerAs<Text>()),
+ atNBSPReplacedWithASCIIWhiteSpace.Offset(), 1, u" "_ns);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ // Don't refer the following variables anymore unless tracking the
+ // change.
+ atNBSPReplaceableWithSP.Clear();
+ invisibleLeadingWhiteSpaceRangeAtStart.Clear();
+ }
+ }
+ }
+
+ // If white-space and/or linefeed characters are collapsible, and inserting
+ // string starts and/or ends with a collapsible characters, we need to
+ // replace them with NBSP for making sure the collapsible characters visible.
+ // FYI: There is no case only linefeeds are collapsible. So, we need to
+ // do the things only when white-spaces are collapsible.
+ MOZ_DIAGNOSTIC_ASSERT(!theString.IsEmpty());
+ if (NS_WARN_IF(!pointToInsert.IsInContentNode()) ||
+ !EditorUtils::IsWhiteSpacePreformatted(
+ *pointToInsert.ContainerAs<nsIContent>())) {
+ const bool isNewLineCollapsible =
+ !pointToInsert.IsInContentNode() ||
+ !EditorUtils::IsNewLinePreformatted(
+ *pointToInsert.ContainerAs<nsIContent>());
+ auto IsCollapsibleChar = [&isNewLineCollapsible](char16_t aChar) -> bool {
+ return nsCRT::IsAsciiSpace(aChar) &&
+ (isNewLineCollapsible || aChar != HTMLEditUtils::kNewLine);
+ };
+ if (IsCollapsibleChar(theString[0])) {
+ // If inserting string will follow some invisible leading white-spaces,
+ // the string needs to start with an NBSP.
+ if (isInvisibleLeadingWhiteSpaceRangeAtStartPositioned) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, 0);
+ }
+ // If inserting around visible white-spaces, check whether the previous
+ // character of insertion point is an NBSP or an ASCII white-space.
+ else if (pointPositionWithVisibleWhiteSpacesAtStart ==
+ PointPosition::MiddleOfFragment ||
+ pointPositionWithVisibleWhiteSpacesAtStart ==
+ PointPosition::EndOfFragment) {
+ const auto atPreviousChar =
+ textFragmentDataAtStart
+ .GetPreviousEditableCharPoint<EditorRawDOMPointInText>(
+ pointToInsert);
+ if (atPreviousChar.IsSet() && !atPreviousChar.IsEndOfContainer() &&
+ atPreviousChar.IsCharASCIISpace()) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, 0);
+ }
+ }
+ // If the insertion point is (was) before the start of text and it's
+ // immediately after a hard line break, the first ASCII white-space should
+ // be replaced with an NBSP for making it visible.
+ else if (textFragmentDataAtStart.StartsFromHardLineBreak() &&
+ isInsertionPointEqualsOrIsBeforeStartOfText) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, 0);
+ }
+ }
+
+ // Then the tail. Note that it may be the first character.
+ const uint32_t lastCharIndex = theString.Length() - 1;
+ if (IsCollapsibleChar(theString[lastCharIndex])) {
+ // If inserting string will be followed by some invisible trailing
+ // white-spaces, the string needs to end with an NBSP.
+ if (isInvisibleTrailingWhiteSpaceRangeAtEndPositioned) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex);
+ }
+ // If inserting around visible white-spaces, check whether the inclusive
+ // next character of end of replaced range is an NBSP or an ASCII
+ // white-space.
+ if (pointPositionWithVisibleWhiteSpacesAtEnd ==
+ PointPosition::StartOfFragment ||
+ pointPositionWithVisibleWhiteSpacesAtEnd ==
+ PointPosition::MiddleOfFragment) {
+ const auto atNextChar =
+ textFragmentDataAtEnd
+ .GetInclusiveNextEditableCharPoint<EditorRawDOMPointInText>(
+ pointToInsert);
+ if (atNextChar.IsSet() && !atNextChar.IsEndOfContainer() &&
+ atNextChar.IsCharASCIISpace()) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex);
+ }
+ }
+ // If the end of replacing range is (was) after the end of text and it's
+ // immediately before block boundary, the last ASCII white-space should
+ // be replaced with an NBSP for making it visible.
+ else if (textFragmentDataAtEnd.EndsByBlockBoundary() &&
+ isInsertionPointEqualsOrAfterEndOfText) {
+ theString.SetCharAt(HTMLEditUtils::kNBSP, lastCharIndex);
+ }
+ }
+
+ // Next, scan string for adjacent ws and convert to nbsp/space combos
+ // MOOSE: don't need to convert tabs here since that is done by
+ // WillInsertText() before we are called. Eventually, all that logic will
+ // be pushed down into here and made more efficient.
+ enum class PreviousChar {
+ NonCollapsibleChar,
+ CollapsibleChar,
+ PreformattedNewLine,
+ };
+ PreviousChar previousChar = PreviousChar::NonCollapsibleChar;
+ for (uint32_t i = 0; i <= lastCharIndex; i++) {
+ if (IsCollapsibleChar(theString[i])) {
+ // If current char is collapsible and 2nd or latter character of
+ // collapsible characters, we need to make the previous character an
+ // NBSP for avoiding current character to be collapsed to it.
+ if (previousChar == PreviousChar::CollapsibleChar) {
+ MOZ_ASSERT(i > 0);
+ theString.SetCharAt(HTMLEditUtils::kNBSP, i - 1);
+ // Keep previousChar as PreviousChar::CollapsibleChar.
+ continue;
+ }
+
+ // If current character is a collapsbile white-space and the previous
+ // character is a preformatted linefeed, we need to replace the current
+ // character with an NBSP for avoiding collapsed with the previous
+ // linefeed.
+ if (previousChar == PreviousChar::PreformattedNewLine) {
+ MOZ_ASSERT(i > 0);
+ theString.SetCharAt(HTMLEditUtils::kNBSP, i);
+ previousChar = PreviousChar::NonCollapsibleChar;
+ continue;
+ }
+
+ previousChar = PreviousChar::CollapsibleChar;
+ continue;
+ }
+
+ if (theString[i] != HTMLEditUtils::kNewLine) {
+ previousChar = PreviousChar::NonCollapsibleChar;
+ continue;
+ }
+
+ // If current character is a preformatted linefeed and the previous
+ // character is collapbile white-space, the previous character will be
+ // collapsed into current linefeed. Therefore, we need to replace the
+ // previous character with an NBSP.
+ MOZ_ASSERT(!isNewLineCollapsible);
+ if (previousChar == PreviousChar::CollapsibleChar) {
+ MOZ_ASSERT(i > 0);
+ theString.SetCharAt(HTMLEditUtils::kNBSP, i - 1);
+ }
+ previousChar = PreviousChar::PreformattedNewLine;
+ }
+ }
+
+ // XXX If the point is not editable, InsertTextWithTransaction() returns
+ // error, but we keep handling it. But I think that it wastes the
+ // runtime cost. So, perhaps, we should return error code which couldn't
+ // modify it and make each caller of this method decide whether it should
+ // keep or stop handling the edit action.
+ if (MOZ_UNLIKELY(!aHTMLEditor.GetDocument())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::ReplaceText() lost proper document");
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ OwningNonNull<Document> document = *aHTMLEditor.GetDocument();
+ Result<InsertTextResult, nsresult> insertTextResult =
+ aHTMLEditor.InsertTextWithTransaction(document, theString, pointToInsert);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ if (insertTextResult.inspect().HasCaretPointSuggestion()) {
+ return insertTextResult;
+ }
+ return InsertTextResult(insertTextResult.unwrap(),
+ std::move(pointToPutCaret));
+}
+
+// static
+Result<CaretPoint, nsresult>
+WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint,
+ const Element& aEditingHost) {
+ TextFragmentData textFragmentDataAtDeletion(
+ aPoint, &aEditingHost, BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtDeletion.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorDOMPointInText atPreviousCharOfStart =
+ textFragmentDataAtDeletion.GetPreviousEditableCharPoint(aPoint);
+ if (!atPreviousCharOfStart.IsSet() ||
+ atPreviousCharOfStart.IsEndOfContainer()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ // If the char is a collapsible white-space or a non-collapsible new line
+ // but it can collapse adjacent white-spaces, we need to extend the range
+ // to delete all invisible white-spaces.
+ if (atPreviousCharOfStart.IsCharCollapsibleASCIISpace() ||
+ atPreviousCharOfStart
+ .IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
+ auto startToDelete =
+ textFragmentDataAtDeletion
+ .GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPoint>(
+ atPreviousCharOfStart, nsIEditor::ePrevious);
+ auto endToDelete = textFragmentDataAtDeletion
+ .GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPoint>(
+ atPreviousCharOfStart, nsIEditor::ePrevious);
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
+ aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
+ "failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ if (atPreviousCharOfStart.IsCharCollapsibleNBSP()) {
+ auto startToDelete = atPreviousCharOfStart.To<EditorDOMPoint>();
+ auto endToDelete = startToDelete.NextPoint<EditorDOMPoint>();
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
+ aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
+ "failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ atPreviousCharOfStart, atPreviousCharOfStart.NextPoint(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ NS_WARNING_ASSERTION(
+ caretPointOrError.isOk(),
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+}
+
+// static
+Result<CaretPoint, nsresult>
+WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint,
+ const Element& aEditingHost) {
+ TextFragmentData textFragmentDataAtDeletion(
+ aPoint, &aEditingHost, BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtDeletion.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ auto atNextCharOfStart =
+ textFragmentDataAtDeletion
+ .GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(aPoint);
+ if (!atNextCharOfStart.IsSet() || atNextCharOfStart.IsEndOfContainer()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ // If the char is a collapsible white-space or a non-collapsible new line
+ // but it can collapse adjacent white-spaces, we need to extend the range
+ // to delete all invisible white-spaces.
+ if (atNextCharOfStart.IsCharCollapsibleASCIISpace() ||
+ atNextCharOfStart.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
+ auto startToDelete =
+ textFragmentDataAtDeletion
+ .GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPoint>(
+ atNextCharOfStart, nsIEditor::eNext);
+ auto endToDelete = textFragmentDataAtDeletion
+ .GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPoint>(
+ atNextCharOfStart, nsIEditor::eNext);
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
+ aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
+ "failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ if (atNextCharOfStart.IsCharCollapsibleNBSP()) {
+ auto startToDelete = atNextCharOfStart.To<EditorDOMPoint>();
+ auto endToDelete = startToDelete.NextPoint<EditorDOMPoint>();
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
+ aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
+ "failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ atNextCharOfStart, atNextCharOfStart.NextPoint(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ NS_WARNING_ASSERTION(
+ caretPointOrError.isOk(),
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+}
+
+// static
+Result<CaretPoint, nsresult>
+WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt(
+ HTMLEditor& aHTMLEditor, nsIContent& aContentToDelete,
+ const EditorDOMPoint& aCaretPoint, const Element& aEditingHost) {
+ EditorDOMPoint atContent(&aContentToDelete);
+ if (!atContent.IsSet()) {
+ NS_WARNING("Deleting content node was an orphan node");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (!HTMLEditUtils::IsRemovableNode(aContentToDelete)) {
+ NS_WARNING("Deleting content node wasn't removable");
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorDOMPoint pointToPutCaret(aCaretPoint);
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
+ aHTMLEditor, EditorDOMRange(atContent, atContent.NextPoint()),
+ aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange() "
+ "failed");
+ return caretPointOrError;
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ nsCOMPtr<nsIContent> previousEditableSibling =
+ HTMLEditUtils::GetPreviousSibling(
+ aContentToDelete, {WalkTreeOption::IgnoreNonEditableNode});
+ // Delete the node, and join like nodes if appropriate
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContentToDelete);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ // Are they both text nodes? If so, join them!
+ // XXX This may cause odd behavior if there is non-editable nodes
+ // around the atomic content.
+ if (!aCaretPoint.IsInTextNode() || !previousEditableSibling ||
+ !previousEditableSibling->IsText()) {
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling(
+ *previousEditableSibling, {WalkTreeOption::IgnoreNonEditableNode});
+ if (aCaretPoint.GetContainer() != nextEditableSibling) {
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ nsresult rv = aHTMLEditor.JoinNearestEditableNodesWithTransaction(
+ *previousEditableSibling, MOZ_KnownLive(*aCaretPoint.ContainerAs<Text>()),
+ &pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::JoinNearestEditableNodesWithTransaction() failed");
+ return Err(rv);
+ }
+ if (!pointToPutCaret.IsSet()) {
+ NS_WARNING(
+ "HTMLEditor::JoinNearestEditableNodesWithTransaction() didn't return "
+ "right node position");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+}
+
+template <typename PT, typename CT>
+WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSet());
+
+ if (!TextFragmentDataAtStartRef().IsInitialized()) {
+ return WSScanResult(nullptr, WSType::UnexpectedError, mBlockInlineCheck);
+ }
+
+ // If the range has visible text and start of the visible text is before
+ // aPoint, return previous character in the text.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ TextFragmentDataAtStartRef().VisibleWhiteSpacesDataRef();
+ if (visibleWhiteSpaces.IsInitialized() &&
+ visibleWhiteSpaces.StartRef().IsBefore(aPoint)) {
+ // If the visible things are not editable, we shouldn't scan "editable"
+ // things now. Whether keep scanning editable things or not should be
+ // considered by the caller.
+ if (aPoint.GetChild() && !aPoint.GetChild()->IsEditable()) {
+ return WSScanResult(aPoint.GetChild(), WSType::SpecialContent,
+ mBlockInlineCheck);
+ }
+ const auto atPreviousChar =
+ GetPreviousEditableCharPoint<EditorRawDOMPointInText>(aPoint);
+ // When it's a non-empty text node, return it.
+ if (atPreviousChar.IsSet() && !atPreviousChar.IsContainerEmpty()) {
+ MOZ_ASSERT(!atPreviousChar.IsEndOfContainer());
+ return WSScanResult(atPreviousChar.template NextPoint<EditorDOMPoint>(),
+ atPreviousChar.IsCharCollapsibleASCIISpaceOrNBSP()
+ ? WSType::CollapsibleWhiteSpaces
+ : WSType::NonCollapsibleCharacters,
+ mBlockInlineCheck);
+ }
+ }
+
+ // Otherwise, return the start of the range.
+ if (TextFragmentDataAtStartRef().GetStartReasonContent() !=
+ TextFragmentDataAtStartRef().StartRef().GetContainer()) {
+ // In this case, TextFragmentDataAtStartRef().StartRef().Offset() is not
+ // meaningful.
+ return WSScanResult(TextFragmentDataAtStartRef().GetStartReasonContent(),
+ TextFragmentDataAtStartRef().StartRawReason(),
+ mBlockInlineCheck);
+ }
+ return WSScanResult(TextFragmentDataAtStartRef().StartRef(),
+ TextFragmentDataAtStartRef().StartRawReason(),
+ mBlockInlineCheck);
+}
+
+template <typename PT, typename CT>
+WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSet());
+
+ if (!TextFragmentDataAtStartRef().IsInitialized()) {
+ return WSScanResult(nullptr, WSType::UnexpectedError, mBlockInlineCheck);
+ }
+
+ // If the range has visible text and aPoint equals or is before the end of the
+ // visible text, return inclusive next character in the text.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ TextFragmentDataAtStartRef().VisibleWhiteSpacesDataRef();
+ if (visibleWhiteSpaces.IsInitialized() &&
+ aPoint.EqualsOrIsBefore(visibleWhiteSpaces.EndRef())) {
+ // If the visible things are not editable, we shouldn't scan "editable"
+ // things now. Whether keep scanning editable things or not should be
+ // considered by the caller.
+ if (aPoint.GetChild() && !aPoint.GetChild()->IsEditable()) {
+ return WSScanResult(aPoint.GetChild(), WSType::SpecialContent,
+ mBlockInlineCheck);
+ }
+ const auto atNextChar =
+ GetInclusiveNextEditableCharPoint<EditorDOMPoint>(aPoint);
+ // When it's a non-empty text node, return it.
+ if (atNextChar.IsSet() && !atNextChar.IsContainerEmpty()) {
+ return WSScanResult(atNextChar,
+ !atNextChar.IsEndOfContainer() &&
+ atNextChar.IsCharCollapsibleASCIISpaceOrNBSP()
+ ? WSType::CollapsibleWhiteSpaces
+ : WSType::NonCollapsibleCharacters,
+ mBlockInlineCheck);
+ }
+ }
+
+ // Otherwise, return the end of the range.
+ if (TextFragmentDataAtStartRef().GetEndReasonContent() !=
+ TextFragmentDataAtStartRef().EndRef().GetContainer()) {
+ // In this case, TextFragmentDataAtStartRef().EndRef().Offset() is not
+ // meaningful.
+ return WSScanResult(TextFragmentDataAtStartRef().GetEndReasonContent(),
+ TextFragmentDataAtStartRef().EndRawReason(),
+ mBlockInlineCheck);
+ }
+ return WSScanResult(TextFragmentDataAtStartRef().EndRef(),
+ TextFragmentDataAtStartRef().EndRawReason(),
+ mBlockInlineCheck);
+}
+
+template <typename EditorDOMPointType>
+WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorDOMPointType& aPoint, const Element* aEditingHost,
+ BlockInlineCheck aBlockInlineCheck)
+ : mEditingHost(aEditingHost), mBlockInlineCheck(aBlockInlineCheck) {
+ if (!aPoint.IsSetAndValid()) {
+ NS_WARNING("aPoint was invalid");
+ return;
+ }
+ if (!aPoint.IsInContentNode()) {
+ NS_WARNING("aPoint was in Document or DocumentFragment");
+ // I.e., we're try to modify outside of root element. We don't need to
+ // support such odd case because web apps cannot append text nodes as
+ // direct child of Document node.
+ return;
+ }
+
+ mScanStartPoint = aPoint.template To<EditorDOMPoint>();
+ NS_ASSERTION(
+ EditorUtils::IsEditableContent(*mScanStartPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML),
+ "Given content is not editable");
+ NS_ASSERTION(
+ mScanStartPoint.ContainerAs<nsIContent>()->GetAsElementOrParentElement(),
+ "Given content is not an element and an orphan node");
+ if (NS_WARN_IF(!EditorUtils::IsEditableContent(
+ *mScanStartPoint.ContainerAs<nsIContent>(), EditorType::HTML))) {
+ return;
+ }
+ const Element* editableBlockElementOrInlineEditingHost =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *mScanStartPoint.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost,
+ aBlockInlineCheck);
+ if (!editableBlockElementOrInlineEditingHost) {
+ NS_WARNING(
+ "HTMLEditUtils::GetInclusiveAncestorElement(HTMLEditUtils::"
+ "ClosestEditableBlockElementOrInlineEditingHost) couldn't find "
+ "editing host");
+ return;
+ }
+
+ mStart = BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ mScanStartPoint, *editableBlockElementOrInlineEditingHost, mEditingHost,
+ &mNBSPData, aBlockInlineCheck);
+ MOZ_ASSERT_IF(mStart.IsNonCollapsibleCharacters(),
+ !mStart.PointRef().IsPreviousCharPreformattedNewLine());
+ MOZ_ASSERT_IF(mStart.IsPreformattedLineBreak(),
+ mStart.PointRef().IsPreviousCharPreformattedNewLine());
+ mEnd = BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ mScanStartPoint, *editableBlockElementOrInlineEditingHost, mEditingHost,
+ &mNBSPData, aBlockInlineCheck);
+ MOZ_ASSERT_IF(mEnd.IsNonCollapsibleCharacters(),
+ !mEnd.PointRef().IsCharPreformattedNewLine());
+ MOZ_ASSERT_IF(mEnd.IsPreformattedLineBreak(),
+ mEnd.PointRef().IsCharPreformattedNewLine());
+}
+
+// static
+template <typename EditorDOMPointType>
+Maybe<WSRunScanner::TextFragmentData::BoundaryData> WSRunScanner::
+ TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(
+ const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData,
+ BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ MOZ_DIAGNOSTIC_ASSERT(aPoint.IsInTextNode());
+
+ const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
+ *aPoint.template ContainerAs<Text>());
+ const bool isNewLineCollapsible =
+ !EditorUtils::IsNewLinePreformatted(*aPoint.template ContainerAs<Text>());
+ const nsTextFragment& textFragment =
+ aPoint.template ContainerAs<Text>()->TextFragment();
+ for (uint32_t i = std::min(aPoint.Offset(), textFragment.GetLength()); i;
+ i--) {
+ WSType wsTypeOfNonCollapsibleChar;
+ switch (textFragment.CharAt(i - 1)) {
+ case HTMLEditUtils::kSpace:
+ case HTMLEditUtils::kCarriageReturn:
+ case HTMLEditUtils::kTab:
+ if (isWhiteSpaceCollapsible) {
+ continue; // collapsible white-space or invisible white-space.
+ }
+ // preformatted white-space.
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ case HTMLEditUtils::kNewLine:
+ if (isNewLineCollapsible) {
+ continue; // collapsible linefeed.
+ }
+ // preformatted linefeed.
+ wsTypeOfNonCollapsibleChar = WSType::PreformattedLineBreak;
+ break;
+ case HTMLEditUtils::kNBSP:
+ if (isWhiteSpaceCollapsible) {
+ if (aNBSPData) {
+ aNBSPData->NotifyNBSP(
+ EditorDOMPointInText(aPoint.template ContainerAs<Text>(),
+ i - 1),
+ NoBreakingSpaceData::Scanning::Backward);
+ }
+ continue;
+ }
+ // NBSP is never converted from collapsible white-space/linefeed.
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ default:
+ MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i - 1)));
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ }
+
+ return Some(BoundaryData(
+ EditorDOMPoint(aPoint.template ContainerAs<Text>(), i),
+ *aPoint.template ContainerAs<Text>(), wsTypeOfNonCollapsibleChar));
+ }
+
+ return Nothing();
+}
+
+// static
+template <typename EditorDOMPointType>
+WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData::
+ BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ const EditorDOMPointType& aPoint,
+ const Element& aEditableBlockParentOrTopmostEditableInlineElement,
+ const Element* aEditingHost, NoBreakingSpaceData* aNBSPData,
+ BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (aPoint.IsInTextNode() && !aPoint.IsStartOfContainer()) {
+ Maybe<BoundaryData> startInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(
+ aPoint, aNBSPData, aBlockInlineCheck);
+ if (startInTextNode.isSome()) {
+ return startInTextNode.ref();
+ }
+ // The text node does not have visible character, let's keep scanning
+ // preceding nodes.
+ return BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ EditorDOMPoint(aPoint.template ContainerAs<Text>(), 0),
+ aEditableBlockParentOrTopmostEditableInlineElement, aEditingHost,
+ aNBSPData, aBlockInlineCheck);
+ }
+
+ // Then, we need to check previous leaf node.
+ nsIContent* previousLeafContentOrBlock =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ aPoint, aEditableBlockParentOrTopmostEditableInlineElement,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, aBlockInlineCheck,
+ aEditingHost);
+ if (!previousLeafContentOrBlock) {
+ // no prior node means we exhausted
+ // aEditableBlockParentOrTopmostEditableInlineElement
+ // mReasonContent can be either a block element or any non-editable
+ // content in this case.
+ return BoundaryData(aPoint,
+ const_cast<Element&>(
+ aEditableBlockParentOrTopmostEditableInlineElement),
+ WSType::CurrentBlockBoundary);
+ }
+
+ if (HTMLEditUtils::IsBlockElement(*previousLeafContentOrBlock,
+ aBlockInlineCheck)) {
+ return BoundaryData(aPoint, *previousLeafContentOrBlock,
+ WSType::OtherBlockBoundary);
+ }
+
+ if (!previousLeafContentOrBlock->IsText() ||
+ !previousLeafContentOrBlock->IsEditable()) {
+ // it's a break or a special node, like <img>, that is not a block and
+ // not a break but still serves as a terminator to ws runs.
+ return BoundaryData(aPoint, *previousLeafContentOrBlock,
+ previousLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
+ ? WSType::BRElement
+ : WSType::SpecialContent);
+ }
+
+ if (!previousLeafContentOrBlock->AsText()->TextLength()) {
+ // If it's an empty text node, keep looking for its previous leaf content.
+ // Note that even if the empty text node is preformatted, we should keep
+ // looking for the previous one.
+ return BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ EditorDOMPointInText(previousLeafContentOrBlock->AsText(), 0),
+ aEditableBlockParentOrTopmostEditableInlineElement, aEditingHost,
+ aNBSPData, aBlockInlineCheck);
+ }
+
+ Maybe<BoundaryData> startInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(
+ EditorDOMPointInText::AtEndOf(*previousLeafContentOrBlock->AsText()),
+ aNBSPData, aBlockInlineCheck);
+ if (startInTextNode.isSome()) {
+ return startInTextNode.ref();
+ }
+
+ // The text node does not have visible character, let's keep scanning
+ // preceding nodes.
+ return BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ EditorDOMPointInText(previousLeafContentOrBlock->AsText(), 0),
+ aEditableBlockParentOrTopmostEditableInlineElement, aEditingHost,
+ aNBSPData, aBlockInlineCheck);
+}
+
+// static
+template <typename EditorDOMPointType>
+Maybe<WSRunScanner::TextFragmentData::BoundaryData> WSRunScanner::
+ TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(
+ const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData,
+ BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ MOZ_DIAGNOSTIC_ASSERT(aPoint.IsInTextNode());
+
+ const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
+ *aPoint.template ContainerAs<Text>());
+ const bool isNewLineCollapsible =
+ !EditorUtils::IsNewLinePreformatted(*aPoint.template ContainerAs<Text>());
+ const nsTextFragment& textFragment =
+ aPoint.template ContainerAs<Text>()->TextFragment();
+ for (uint32_t i = aPoint.Offset(); i < textFragment.GetLength(); i++) {
+ WSType wsTypeOfNonCollapsibleChar;
+ switch (textFragment.CharAt(i)) {
+ case HTMLEditUtils::kSpace:
+ case HTMLEditUtils::kCarriageReturn:
+ case HTMLEditUtils::kTab:
+ if (isWhiteSpaceCollapsible) {
+ continue; // collapsible white-space or invisible white-space.
+ }
+ // preformatted white-space.
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ case HTMLEditUtils::kNewLine:
+ if (isNewLineCollapsible) {
+ continue; // collapsible linefeed.
+ }
+ // preformatted linefeed.
+ wsTypeOfNonCollapsibleChar = WSType::PreformattedLineBreak;
+ break;
+ case HTMLEditUtils::kNBSP:
+ if (isWhiteSpaceCollapsible) {
+ if (aNBSPData) {
+ aNBSPData->NotifyNBSP(
+ EditorDOMPointInText(aPoint.template ContainerAs<Text>(), i),
+ NoBreakingSpaceData::Scanning::Forward);
+ }
+ continue;
+ }
+ // NBSP is never converted from collapsible white-space/linefeed.
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ default:
+ MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i)));
+ wsTypeOfNonCollapsibleChar = WSType::NonCollapsibleCharacters;
+ break;
+ }
+
+ return Some(BoundaryData(
+ EditorDOMPoint(aPoint.template ContainerAs<Text>(), i),
+ *aPoint.template ContainerAs<Text>(), wsTypeOfNonCollapsibleChar));
+ }
+
+ return Nothing();
+}
+
+// static
+template <typename EditorDOMPointType>
+WSRunScanner::TextFragmentData::BoundaryData
+WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ const EditorDOMPointType& aPoint,
+ const Element& aEditableBlockParentOrTopmostEditableInlineElement,
+ const Element* aEditingHost, NoBreakingSpaceData* aNBSPData,
+ BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (aPoint.IsInTextNode() && !aPoint.IsEndOfContainer()) {
+ Maybe<BoundaryData> endInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(aPoint, aNBSPData,
+ aBlockInlineCheck);
+ if (endInTextNode.isSome()) {
+ return endInTextNode.ref();
+ }
+ // The text node does not have visible character, let's keep scanning
+ // following nodes.
+ return BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ EditorDOMPointInText::AtEndOf(*aPoint.template ContainerAs<Text>()),
+ aEditableBlockParentOrTopmostEditableInlineElement, aEditingHost,
+ aNBSPData, aBlockInlineCheck);
+ }
+
+ // Then, we need to check next leaf node.
+ nsIContent* nextLeafContentOrBlock =
+ HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ aPoint, aEditableBlockParentOrTopmostEditableInlineElement,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, aBlockInlineCheck,
+ aEditingHost);
+ if (!nextLeafContentOrBlock) {
+ // no next node means we exhausted
+ // aEditableBlockParentOrTopmostEditableInlineElement
+ // mReasonContent can be either a block element or any non-editable
+ // content in this case.
+ return BoundaryData(aPoint.template To<EditorDOMPoint>(),
+ const_cast<Element&>(
+ aEditableBlockParentOrTopmostEditableInlineElement),
+ WSType::CurrentBlockBoundary);
+ }
+
+ if (HTMLEditUtils::IsBlockElement(*nextLeafContentOrBlock,
+ aBlockInlineCheck)) {
+ // we encountered a new block. therefore no more ws.
+ return BoundaryData(aPoint, *nextLeafContentOrBlock,
+ WSType::OtherBlockBoundary);
+ }
+
+ if (!nextLeafContentOrBlock->IsText() ||
+ !nextLeafContentOrBlock->IsEditable()) {
+ // we encountered a break or a special node, like <img>,
+ // that is not a block and not a break but still
+ // serves as a terminator to ws runs.
+ return BoundaryData(aPoint, *nextLeafContentOrBlock,
+ nextLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
+ ? WSType::BRElement
+ : WSType::SpecialContent);
+ }
+
+ if (!nextLeafContentOrBlock->AsText()->TextFragment().GetLength()) {
+ // If it's an empty text node, keep looking for its next leaf content.
+ // Note that even if the empty text node is preformatted, we should keep
+ // looking for the next one.
+ return BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ EditorDOMPointInText(nextLeafContentOrBlock->AsText(), 0),
+ aEditableBlockParentOrTopmostEditableInlineElement, aEditingHost,
+ aNBSPData, aBlockInlineCheck);
+ }
+
+ Maybe<BoundaryData> endInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(
+ EditorDOMPointInText(nextLeafContentOrBlock->AsText(), 0), aNBSPData,
+ aBlockInlineCheck);
+ if (endInTextNode.isSome()) {
+ return endInTextNode.ref();
+ }
+
+ // The text node does not have visible character, let's keep scanning
+ // following nodes.
+ return BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ EditorDOMPointInText::AtEndOf(*nextLeafContentOrBlock->AsText()),
+ aEditableBlockParentOrTopmostEditableInlineElement, aEditingHost,
+ aNBSPData, aBlockInlineCheck);
+}
+
+const EditorDOMRange&
+WSRunScanner::TextFragmentData::InvisibleLeadingWhiteSpaceRangeRef() const {
+ if (mLeadingWhiteSpaceRange.isSome()) {
+ return mLeadingWhiteSpaceRange.ref();
+ }
+
+ // If it's start of line, there is no invisible leading white-spaces.
+ if (!StartsFromHardLineBreak()) {
+ mLeadingWhiteSpaceRange.emplace();
+ return mLeadingWhiteSpaceRange.ref();
+ }
+
+ // If there is no NBSP, all of the given range is leading white-spaces.
+ // Note that this result may be collapsed if there is no leading white-spaces.
+ if (!mNBSPData.FoundNBSP()) {
+ MOZ_ASSERT(mStart.PointRef().IsSet() || mEnd.PointRef().IsSet());
+ mLeadingWhiteSpaceRange.emplace(mStart.PointRef(), mEnd.PointRef());
+ return mLeadingWhiteSpaceRange.ref();
+ }
+
+ MOZ_ASSERT(mNBSPData.LastPointRef().IsSetAndValid());
+
+ // Even if the first NBSP is the start, i.e., there is no invisible leading
+ // white-space, return collapsed range.
+ mLeadingWhiteSpaceRange.emplace(mStart.PointRef(), mNBSPData.FirstPointRef());
+ return mLeadingWhiteSpaceRange.ref();
+}
+
+const EditorDOMRange&
+WSRunScanner::TextFragmentData::InvisibleTrailingWhiteSpaceRangeRef() const {
+ if (mTrailingWhiteSpaceRange.isSome()) {
+ return mTrailingWhiteSpaceRange.ref();
+ }
+
+ // If it's not immediately before a block boundary nor an invisible
+ // preformatted linefeed, there is no invisible trailing white-spaces. Note
+ // that collapsible white-spaces before a `<br>` element is visible.
+ if (!EndsByBlockBoundary() && !EndsByInvisiblePreformattedLineBreak()) {
+ mTrailingWhiteSpaceRange.emplace();
+ return mTrailingWhiteSpaceRange.ref();
+ }
+
+ // If there is no NBSP, all of the given range is trailing white-spaces.
+ // Note that this result may be collapsed if there is no trailing white-
+ // spaces.
+ if (!mNBSPData.FoundNBSP()) {
+ MOZ_ASSERT(mStart.PointRef().IsSet() || mEnd.PointRef().IsSet());
+ mTrailingWhiteSpaceRange.emplace(mStart.PointRef(), mEnd.PointRef());
+ return mTrailingWhiteSpaceRange.ref();
+ }
+
+ MOZ_ASSERT(mNBSPData.LastPointRef().IsSetAndValid());
+
+ // If last NBSP is immediately before the end, there is no trailing white-
+ // spaces.
+ if (mEnd.PointRef().IsSet() &&
+ mNBSPData.LastPointRef().GetContainer() ==
+ mEnd.PointRef().GetContainer() &&
+ mNBSPData.LastPointRef().Offset() == mEnd.PointRef().Offset() - 1) {
+ mTrailingWhiteSpaceRange.emplace();
+ return mTrailingWhiteSpaceRange.ref();
+ }
+
+ // Otherwise, the may be some trailing white-spaces.
+ MOZ_ASSERT(!mNBSPData.LastPointRef().IsEndOfContainer());
+ mTrailingWhiteSpaceRange.emplace(mNBSPData.LastPointRef().NextPoint(),
+ mEnd.PointRef());
+ return mTrailingWhiteSpaceRange.ref();
+}
+
+EditorDOMRangeInTexts
+WSRunScanner::TextFragmentData::GetNonCollapsedRangeInTexts(
+ const EditorDOMRange& aRange) const {
+ if (!aRange.IsPositioned()) {
+ return EditorDOMRangeInTexts();
+ }
+ if (aRange.Collapsed()) {
+ // If collapsed, we can do nothing.
+ return EditorDOMRangeInTexts();
+ }
+ if (aRange.IsInTextNodes()) {
+ // Note that this may return a range which don't include any invisible
+ // white-spaces due to empty text nodes.
+ return aRange.GetAsInTexts();
+ }
+
+ const auto firstPoint =
+ aRange.StartRef().IsInTextNode()
+ ? aRange.StartRef().AsInText()
+ : GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ aRange.StartRef());
+ if (!firstPoint.IsSet()) {
+ return EditorDOMRangeInTexts();
+ }
+ EditorDOMPointInText endPoint;
+ if (aRange.EndRef().IsInTextNode()) {
+ endPoint = aRange.EndRef().AsInText();
+ } else {
+ // FYI: GetPreviousEditableCharPoint() returns last character's point
+ // of preceding text node if it's not empty, but we need end of
+ // the text node here.
+ endPoint = GetPreviousEditableCharPoint(aRange.EndRef());
+ if (endPoint.IsSet() && endPoint.IsAtLastContent()) {
+ MOZ_ALWAYS_TRUE(endPoint.AdvanceOffset());
+ }
+ }
+ if (!endPoint.IsSet() || firstPoint == endPoint) {
+ return EditorDOMRangeInTexts();
+ }
+ return EditorDOMRangeInTexts(firstPoint, endPoint);
+}
+
+const WSRunScanner::VisibleWhiteSpacesData&
+WSRunScanner::TextFragmentData::VisibleWhiteSpacesDataRef() const {
+ if (mVisibleWhiteSpacesData.isSome()) {
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ {
+ // If all things are obviously visible, we can return range for all of the
+ // things quickly.
+ const bool mayHaveInvisibleLeadingSpace =
+ !StartsFromNonCollapsibleCharacters() && !StartsFromSpecialContent();
+ const bool mayHaveInvisibleTrailingWhiteSpace =
+ !EndsByNonCollapsibleCharacters() && !EndsBySpecialContent() &&
+ !EndsByBRElement() && !EndsByInvisiblePreformattedLineBreak();
+
+ if (!mayHaveInvisibleLeadingSpace && !mayHaveInvisibleTrailingWhiteSpace) {
+ VisibleWhiteSpacesData visibleWhiteSpaces;
+ if (mStart.PointRef().IsSet()) {
+ visibleWhiteSpaces.SetStartPoint(mStart.PointRef());
+ }
+ visibleWhiteSpaces.SetStartFrom(mStart.RawReason());
+ if (mEnd.PointRef().IsSet()) {
+ visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
+ }
+ visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+ }
+
+ // If all of the range is invisible leading or trailing white-spaces,
+ // there is no visible content.
+ const EditorDOMRange& leadingWhiteSpaceRange =
+ InvisibleLeadingWhiteSpaceRangeRef();
+ const bool maybeHaveLeadingWhiteSpaces =
+ leadingWhiteSpaceRange.StartRef().IsSet() ||
+ leadingWhiteSpaceRange.EndRef().IsSet();
+ if (maybeHaveLeadingWhiteSpaces &&
+ leadingWhiteSpaceRange.StartRef() == mStart.PointRef() &&
+ leadingWhiteSpaceRange.EndRef() == mEnd.PointRef()) {
+ mVisibleWhiteSpacesData.emplace(VisibleWhiteSpacesData());
+ return mVisibleWhiteSpacesData.ref();
+ }
+ const EditorDOMRange& trailingWhiteSpaceRange =
+ InvisibleTrailingWhiteSpaceRangeRef();
+ const bool maybeHaveTrailingWhiteSpaces =
+ trailingWhiteSpaceRange.StartRef().IsSet() ||
+ trailingWhiteSpaceRange.EndRef().IsSet();
+ if (maybeHaveTrailingWhiteSpaces &&
+ trailingWhiteSpaceRange.StartRef() == mStart.PointRef() &&
+ trailingWhiteSpaceRange.EndRef() == mEnd.PointRef()) {
+ mVisibleWhiteSpacesData.emplace(VisibleWhiteSpacesData());
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ if (!StartsFromHardLineBreak()) {
+ VisibleWhiteSpacesData visibleWhiteSpaces;
+ if (mStart.PointRef().IsSet()) {
+ visibleWhiteSpaces.SetStartPoint(mStart.PointRef());
+ }
+ visibleWhiteSpaces.SetStartFrom(mStart.RawReason());
+ if (!maybeHaveTrailingWhiteSpaces) {
+ visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
+ visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
+ mVisibleWhiteSpacesData = Some(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+ if (trailingWhiteSpaceRange.StartRef().IsSet()) {
+ visibleWhiteSpaces.SetEndPoint(trailingWhiteSpaceRange.StartRef());
+ }
+ visibleWhiteSpaces.SetEndByTrailingWhiteSpaces();
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ MOZ_ASSERT(StartsFromHardLineBreak());
+ MOZ_ASSERT(maybeHaveLeadingWhiteSpaces);
+
+ VisibleWhiteSpacesData visibleWhiteSpaces;
+ if (leadingWhiteSpaceRange.EndRef().IsSet()) {
+ visibleWhiteSpaces.SetStartPoint(leadingWhiteSpaceRange.EndRef());
+ }
+ visibleWhiteSpaces.SetStartFromLeadingWhiteSpaces();
+ if (!EndsByBlockBoundary()) {
+ // then no trailing ws. this normal run ends the overall ws run.
+ if (mEnd.PointRef().IsSet()) {
+ visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
+ }
+ visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ MOZ_ASSERT(EndsByBlockBoundary());
+
+ if (!maybeHaveTrailingWhiteSpaces) {
+ // normal ws runs right up to adjacent block (nbsp next to block)
+ visibleWhiteSpaces.SetEndPoint(mEnd.PointRef());
+ visibleWhiteSpaces.SetEndBy(mEnd.RawReason());
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+ }
+
+ if (trailingWhiteSpaceRange.StartRef().IsSet()) {
+ visibleWhiteSpaces.SetEndPoint(trailingWhiteSpaceRange.StartRef());
+ }
+ visibleWhiteSpaces.SetEndByTrailingWhiteSpaces();
+ mVisibleWhiteSpacesData.emplace(visibleWhiteSpaces);
+ return mVisibleWhiteSpacesData.ref();
+}
+
+// static
+Result<CaretPoint, nsresult> WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
+ HTMLEditor& aHTMLEditor, const EditorDOMRange& aRangeToDelete,
+ const Element& aEditingHost) {
+ if (NS_WARN_IF(!aRangeToDelete.IsPositionedAndValid()) ||
+ NS_WARN_IF(!aRangeToDelete.IsInContentNodes())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ EditorDOMRange rangeToDelete(aRangeToDelete);
+ bool mayBecomeUnexpectedDOMTree = aHTMLEditor.MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED);
+
+ TextFragmentData textFragmentDataAtStart(
+ rangeToDelete.StartRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ TextFragmentData textFragmentDataAtEnd(
+ rangeToDelete.EndRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ ReplaceRangeData replaceRangeDataAtEnd =
+ textFragmentDataAtEnd.GetReplaceRangeDataAtEndOfDeletionRange(
+ textFragmentDataAtStart);
+ EditorDOMPoint pointToPutCaret;
+ if (replaceRangeDataAtEnd.IsSet() && !replaceRangeDataAtEnd.Collapsed()) {
+ MOZ_ASSERT(rangeToDelete.EndRef().EqualsOrIsBefore(
+ replaceRangeDataAtEnd.EndRef()));
+ // If there is some text after deleting range, replacing range start must
+ // equal or be before end of the deleting range.
+ MOZ_ASSERT_IF(rangeToDelete.EndRef().IsInTextNode() &&
+ !rangeToDelete.EndRef().IsEndOfContainer(),
+ replaceRangeDataAtEnd.StartRef().EqualsOrIsBefore(
+ rangeToDelete.EndRef()));
+ // If the deleting range end is end of a text node, the replacing range
+ // should:
+ // - start with another node if the following text node starts with
+ // white-spaces.
+ // - start from prior point because end of the range may be in collapsible
+ // white-spaces.
+ MOZ_ASSERT_IF(rangeToDelete.EndRef().IsInTextNode() &&
+ rangeToDelete.EndRef().IsEndOfContainer(),
+ replaceRangeDataAtEnd.StartRef().EqualsOrIsBefore(
+ rangeToDelete.EndRef()) ||
+ replaceRangeDataAtEnd.StartRef().IsStartOfContainer());
+ MOZ_ASSERT(rangeToDelete.StartRef().EqualsOrIsBefore(
+ replaceRangeDataAtEnd.StartRef()));
+ if (!replaceRangeDataAtEnd.HasReplaceString()) {
+ EditorDOMPoint startToDelete(aRangeToDelete.StartRef());
+ EditorDOMPoint endToDelete(replaceRangeDataAtEnd.StartRef());
+ {
+ AutoEditorDOMPointChildInvalidator lockOffsetOfStart(startToDelete);
+ AutoEditorDOMPointChildInvalidator lockOffsetOfEnd(endToDelete);
+ AutoTrackDOMPoint trackStartToDelete(aHTMLEditor.RangeUpdaterRef(),
+ &startToDelete);
+ AutoTrackDOMPoint trackEndToDelete(aHTMLEditor.RangeUpdaterRef(),
+ &endToDelete);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ replaceRangeDataAtEnd.StartRef(),
+ replaceRangeDataAtEnd.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::
+ KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ if (mayBecomeUnexpectedDOMTree &&
+ (NS_WARN_IF(!startToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!endToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!startToDelete.EqualsOrIsBefore(endToDelete)))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
+ rangeToDelete.SetStartAndEnd(startToDelete, endToDelete);
+ } else {
+ MOZ_ASSERT(replaceRangeDataAtEnd.RangeRef().IsInTextNodes());
+ EditorDOMPoint startToDelete(aRangeToDelete.StartRef());
+ EditorDOMPoint endToDelete(replaceRangeDataAtEnd.StartRef());
+ {
+ AutoTrackDOMPoint trackStartToDelete(aHTMLEditor.RangeUpdaterRef(),
+ &startToDelete);
+ AutoTrackDOMPoint trackEndToDelete(aHTMLEditor.RangeUpdaterRef(),
+ &endToDelete);
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ // FYI: ReplaceTextAndRemoveEmptyTextNodes() does not have any idea of
+ // new caret position.
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor, replaceRangeDataAtEnd.RangeRef().AsInTexts(),
+ replaceRangeDataAtEnd.ReplaceStringRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAtEndOfDeletingRange() "
+ "failed");
+ return Err(rv);
+ }
+ }
+ if (mayBecomeUnexpectedDOMTree &&
+ (NS_WARN_IF(!startToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!endToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!startToDelete.EqualsOrIsBefore(endToDelete)))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
+ rangeToDelete.SetStartAndEnd(startToDelete, endToDelete);
+ }
+
+ if (mayBecomeUnexpectedDOMTree) {
+ // If focus is changed by mutation event listeners, we should stop
+ // handling this edit action.
+ if (&aEditingHost != aHTMLEditor.ComputeEditingHost()) {
+ NS_WARNING("Active editing host was changed");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ if (!rangeToDelete.IsInContentNodes()) {
+ NS_WARNING("The modified range was not in content");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // If the DOM tree might be changed by mutation event listeners, we
+ // should retrieve the latest data for avoiding to delete/replace
+ // unexpected range.
+ textFragmentDataAtStart =
+ TextFragmentData(rangeToDelete.StartRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ textFragmentDataAtEnd =
+ TextFragmentData(rangeToDelete.EndRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ }
+ }
+ ReplaceRangeData replaceRangeDataAtStart =
+ textFragmentDataAtStart.GetReplaceRangeDataAtStartOfDeletionRange(
+ textFragmentDataAtEnd);
+ if (!replaceRangeDataAtStart.IsSet() || replaceRangeDataAtStart.Collapsed()) {
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+ if (!replaceRangeDataAtStart.HasReplaceString()) {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ replaceRangeDataAtStart.StartRef(),
+ replaceRangeDataAtStart.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ // XXX Should we validate the range for making this return
+ // NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE in this case?
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return CaretPoint(std::move(pointToPutCaret));
+ }
+
+ MOZ_ASSERT(replaceRangeDataAtStart.RangeRef().IsInTextNodes());
+ {
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ // FYI: ReplaceTextAndRemoveEmptyTextNodes() does not have any idea of
+ // new caret position.
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor, replaceRangeDataAtStart.RangeRef().AsInTexts(),
+ replaceRangeDataAtStart.ReplaceStringRef());
+ // XXX Should we validate the range for making this return
+ // NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE in this case?
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAtStartOfDeletingRange() "
+ "failed");
+ return Err(rv);
+ }
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+}
+
+ReplaceRangeData
+WSRunScanner::TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange(
+ const TextFragmentData& aTextFragmentDataAtStartToDelete) const {
+ const EditorDOMPoint& startToDelete =
+ aTextFragmentDataAtStartToDelete.ScanStartRef();
+ const EditorDOMPoint& endToDelete = mScanStartPoint;
+
+ MOZ_ASSERT(startToDelete.IsSetAndValid());
+ MOZ_ASSERT(endToDelete.IsSetAndValid());
+ MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
+
+ if (EndRef().EqualsOrIsBefore(endToDelete)) {
+ return ReplaceRangeData();
+ }
+
+ // If deleting range is followed by invisible trailing white-spaces, we need
+ // to remove it for making them not visible.
+ const EditorDOMRange invisibleTrailingWhiteSpaceRangeAtEnd =
+ GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(endToDelete);
+ if (invisibleTrailingWhiteSpaceRangeAtEnd.IsPositioned()) {
+ if (invisibleTrailingWhiteSpaceRangeAtEnd.Collapsed()) {
+ return ReplaceRangeData();
+ }
+ // XXX Why don't we remove all invisible white-spaces?
+ MOZ_ASSERT(invisibleTrailingWhiteSpaceRangeAtEnd.StartRef() == endToDelete);
+ return ReplaceRangeData(invisibleTrailingWhiteSpaceRangeAtEnd, u""_ns);
+ }
+
+ // If end of the deleting range is followed by visible white-spaces which
+ // is not preformatted, we might need to replace the following ASCII
+ // white-spaces with an NBSP.
+ const VisibleWhiteSpacesData& nonPreformattedVisibleWhiteSpacesAtEnd =
+ VisibleWhiteSpacesDataRef();
+ if (!nonPreformattedVisibleWhiteSpacesAtEnd.IsInitialized()) {
+ return ReplaceRangeData();
+ }
+ const PointPosition pointPositionWithNonPreformattedVisibleWhiteSpacesAtEnd =
+ nonPreformattedVisibleWhiteSpacesAtEnd.ComparePoint(endToDelete);
+ if (pointPositionWithNonPreformattedVisibleWhiteSpacesAtEnd !=
+ PointPosition::StartOfFragment &&
+ pointPositionWithNonPreformattedVisibleWhiteSpacesAtEnd !=
+ PointPosition::MiddleOfFragment) {
+ return ReplaceRangeData();
+ }
+ // If start of deleting range follows white-spaces or end of delete
+ // will be start of a line, the following text cannot start with an
+ // ASCII white-space for keeping it visible.
+ if (!aTextFragmentDataAtStartToDelete
+ .FollowingContentMayBecomeFirstVisibleContent(startToDelete)) {
+ return ReplaceRangeData();
+ }
+ auto nextCharOfStartOfEnd =
+ GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(endToDelete);
+ if (!nextCharOfStartOfEnd.IsSet() ||
+ nextCharOfStartOfEnd.IsEndOfContainer() ||
+ !nextCharOfStartOfEnd.IsCharCollapsibleASCIISpace()) {
+ return ReplaceRangeData();
+ }
+ if (nextCharOfStartOfEnd.IsStartOfContainer() ||
+ nextCharOfStartOfEnd.IsPreviousCharCollapsibleASCIISpace()) {
+ nextCharOfStartOfEnd = aTextFragmentDataAtStartToDelete
+ .GetFirstASCIIWhiteSpacePointCollapsedTo(
+ nextCharOfStartOfEnd, nsIEditor::eNone);
+ }
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ aTextFragmentDataAtStartToDelete.GetEndOfCollapsibleASCIIWhiteSpaces(
+ nextCharOfStartOfEnd, nsIEditor::eNone);
+ return ReplaceRangeData(nextCharOfStartOfEnd,
+ endOfCollapsibleASCIIWhiteSpaces,
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+}
+
+ReplaceRangeData
+WSRunScanner::TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange(
+ const TextFragmentData& aTextFragmentDataAtEndToDelete) const {
+ const EditorDOMPoint& startToDelete = mScanStartPoint;
+ const EditorDOMPoint& endToDelete =
+ aTextFragmentDataAtEndToDelete.ScanStartRef();
+
+ MOZ_ASSERT(startToDelete.IsSetAndValid());
+ MOZ_ASSERT(endToDelete.IsSetAndValid());
+ MOZ_ASSERT(startToDelete.EqualsOrIsBefore(endToDelete));
+
+ if (startToDelete.EqualsOrIsBefore(StartRef())) {
+ return ReplaceRangeData();
+ }
+
+ const EditorDOMRange invisibleLeadingWhiteSpaceRangeAtStart =
+ GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(startToDelete);
+
+ // If deleting range follows invisible leading white-spaces, we need to
+ // remove them for making them not visible.
+ if (invisibleLeadingWhiteSpaceRangeAtStart.IsPositioned()) {
+ if (invisibleLeadingWhiteSpaceRangeAtStart.Collapsed()) {
+ return ReplaceRangeData();
+ }
+
+ // XXX Why don't we remove all leading white-spaces?
+ return ReplaceRangeData(invisibleLeadingWhiteSpaceRangeAtStart, u""_ns);
+ }
+
+ // If start of the deleting range follows visible white-spaces which is not
+ // preformatted, we might need to replace previous ASCII white-spaces with
+ // an NBSP.
+ const VisibleWhiteSpacesData& nonPreformattedVisibleWhiteSpacesAtStart =
+ VisibleWhiteSpacesDataRef();
+ if (!nonPreformattedVisibleWhiteSpacesAtStart.IsInitialized()) {
+ return ReplaceRangeData();
+ }
+ const PointPosition
+ pointPositionWithNonPreformattedVisibleWhiteSpacesAtStart =
+ nonPreformattedVisibleWhiteSpacesAtStart.ComparePoint(startToDelete);
+ if (pointPositionWithNonPreformattedVisibleWhiteSpacesAtStart !=
+ PointPosition::MiddleOfFragment &&
+ pointPositionWithNonPreformattedVisibleWhiteSpacesAtStart !=
+ PointPosition::EndOfFragment) {
+ return ReplaceRangeData();
+ }
+ // If end of the deleting range is (was) followed by white-spaces or
+ // previous character of start of deleting range will be immediately
+ // before a block boundary, the text cannot ends with an ASCII white-space
+ // for keeping it visible.
+ if (!aTextFragmentDataAtEndToDelete.PrecedingContentMayBecomeInvisible(
+ endToDelete)) {
+ return ReplaceRangeData();
+ }
+ EditorDOMPointInText atPreviousCharOfStart =
+ GetPreviousEditableCharPoint(startToDelete);
+ if (!atPreviousCharOfStart.IsSet() ||
+ atPreviousCharOfStart.IsEndOfContainer() ||
+ !atPreviousCharOfStart.IsCharCollapsibleASCIISpace()) {
+ return ReplaceRangeData();
+ }
+ if (atPreviousCharOfStart.IsStartOfContainer() ||
+ atPreviousCharOfStart.IsPreviousCharASCIISpace()) {
+ atPreviousCharOfStart = GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atPreviousCharOfStart, nsIEditor::eNone);
+ }
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ GetEndOfCollapsibleASCIIWhiteSpaces(atPreviousCharOfStart,
+ nsIEditor::eNone);
+ return ReplaceRangeData(atPreviousCharOfStart,
+ endOfCollapsibleASCIIWhiteSpaces,
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+}
+
+// static
+nsresult
+WhiteSpaceVisibilityKeeper::MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit) {
+ TextFragmentData textFragmentDataAtSplitPoint(
+ aPointToSplit, aHTMLEditor.ComputeEditingHost(),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtSplitPoint.IsInitialized())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // used to prepare white-space sequence to be split across two blocks.
+ // The main issue here is make sure white-spaces around the split point
+ // doesn't end up becoming non-significant leading or trailing ws after
+ // the split.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ textFragmentDataAtSplitPoint.VisibleWhiteSpacesDataRef();
+ if (!visibleWhiteSpaces.IsInitialized()) {
+ return NS_OK; // No visible white-space sequence.
+ }
+
+ PointPosition pointPositionWithVisibleWhiteSpaces =
+ visibleWhiteSpaces.ComparePoint(aPointToSplit);
+
+ // XXX If we split white-space sequence, the following code modify the DOM
+ // tree twice. This is not reasonable and the latter change may touch
+ // wrong position. We should do this once.
+
+ // If we insert block boundary to start or middle of the white-space sequence,
+ // the character at the insertion point needs to be an NBSP.
+ EditorDOMPoint pointToSplit(aPointToSplit);
+ if (pointPositionWithVisibleWhiteSpaces == PointPosition::StartOfFragment ||
+ pointPositionWithVisibleWhiteSpaces == PointPosition::MiddleOfFragment) {
+ EditorDOMPointInText atNextCharOfStart =
+ textFragmentDataAtSplitPoint.GetInclusiveNextEditableCharPoint(
+ pointToSplit);
+ if (atNextCharOfStart.IsSet() && !atNextCharOfStart.IsEndOfContainer() &&
+ atNextCharOfStart.IsCharCollapsibleASCIISpace()) {
+ // pointToSplit will be referred bellow so that we need to keep
+ // it a valid point.
+ AutoEditorDOMPointChildInvalidator forgetChild(pointToSplit);
+ AutoTrackDOMPoint trackSplitPoint(aHTMLEditor.RangeUpdaterRef(),
+ &pointToSplit);
+ if (atNextCharOfStart.IsStartOfContainer() ||
+ atNextCharOfStart.IsPreviousCharASCIISpace()) {
+ atNextCharOfStart = textFragmentDataAtSplitPoint
+ .GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atNextCharOfStart, nsIEditor::eNone);
+ }
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ textFragmentDataAtSplitPoint.GetEndOfCollapsibleASCIIWhiteSpaces(
+ atNextCharOfStart, nsIEditor::eNone);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor,
+ EditorDOMRangeInTexts(atNextCharOfStart,
+ endOfCollapsibleASCIIWhiteSpaces),
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes() "
+ "failed");
+ return rv;
+ }
+ }
+ }
+
+ // If we insert block boundary to middle of or end of the white-space
+ // sequence, the previous character at the insertion point needs to be an
+ // NBSP.
+ if (pointPositionWithVisibleWhiteSpaces == PointPosition::MiddleOfFragment ||
+ pointPositionWithVisibleWhiteSpaces == PointPosition::EndOfFragment) {
+ EditorDOMPointInText atPreviousCharOfStart =
+ textFragmentDataAtSplitPoint.GetPreviousEditableCharPoint(pointToSplit);
+ if (atPreviousCharOfStart.IsSet() &&
+ !atPreviousCharOfStart.IsEndOfContainer() &&
+ atPreviousCharOfStart.IsCharCollapsibleASCIISpace()) {
+ if (atPreviousCharOfStart.IsStartOfContainer() ||
+ atPreviousCharOfStart.IsPreviousCharASCIISpace()) {
+ atPreviousCharOfStart =
+ textFragmentDataAtSplitPoint
+ .GetFirstASCIIWhiteSpacePointCollapsedTo(atPreviousCharOfStart,
+ nsIEditor::eNone);
+ }
+ const EditorDOMPointInText endOfCollapsibleASCIIWhiteSpaces =
+ textFragmentDataAtSplitPoint.GetEndOfCollapsibleASCIIWhiteSpaces(
+ atPreviousCharOfStart, nsIEditor::eNone);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ aHTMLEditor,
+ EditorDOMRangeInTexts(atPreviousCharOfStart,
+ endOfCollapsibleASCIIWhiteSpaces),
+ nsDependentSubstring(&HTMLEditUtils::kNBSP, 1));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes() "
+ "failed");
+ return rv;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+template <typename EditorDOMPointType, typename PT, typename CT>
+EditorDOMPointType
+WSRunScanner::TextFragmentData::GetInclusiveNextEditableCharPoint(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (NS_WARN_IF(!aPoint.IsInContentNode()) ||
+ NS_WARN_IF(!mScanStartPoint.IsInContentNode())) {
+ return EditorDOMPointType();
+ }
+
+ EditorRawDOMPoint point;
+ if (nsIContent* child =
+ aPoint.CanContainerHaveChildren() ? aPoint.GetChild() : nullptr) {
+ nsIContent* leafContent = child->HasChildren()
+ ? HTMLEditUtils::GetFirstLeafContent(
+ *child, {LeafNodeType::OnlyLeafNode})
+ : child;
+ if (NS_WARN_IF(!leafContent)) {
+ return EditorDOMPointType();
+ }
+ point.Set(leafContent, 0);
+ } else {
+ point = aPoint.template To<EditorRawDOMPoint>();
+ }
+
+ // If it points a character in a text node, return it.
+ // XXX For the performance, this does not check whether the container
+ // is outside of our range.
+ if (point.IsInTextNode() && point.GetContainer()->IsEditable() &&
+ !point.IsEndOfContainer()) {
+ return EditorDOMPointType(point.ContainerAs<Text>(), point.Offset());
+ }
+
+ if (point.GetContainer() == GetEndReasonContent()) {
+ return EditorDOMPointType();
+ }
+
+ NS_ASSERTION(
+ EditorUtils::IsEditableContent(*mScanStartPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML),
+ "Given content is not editable");
+ NS_ASSERTION(
+ mScanStartPoint.ContainerAs<nsIContent>()->GetAsElementOrParentElement(),
+ "Given content is not an element and an orphan node");
+ nsIContent* editableBlockElementOrInlineEditingHost =
+ mScanStartPoint.ContainerAs<nsIContent>() &&
+ EditorUtils::IsEditableContent(
+ *mScanStartPoint.ContainerAs<nsIContent>(), EditorType::HTML)
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *mScanStartPoint.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost,
+ mBlockInlineCheck)
+ : nullptr;
+ if (NS_WARN_IF(!editableBlockElementOrInlineEditingHost)) {
+ // Meaning that the container of `mScanStartPoint` is not editable.
+ editableBlockElementOrInlineEditingHost =
+ mScanStartPoint.ContainerAs<nsIContent>();
+ }
+
+ for (nsIContent* nextContent =
+ HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *point.ContainerAs<nsIContent>(),
+ *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, mBlockInlineCheck,
+ mEditingHost);
+ nextContent;
+ nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *nextContent, *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, mBlockInlineCheck,
+ mEditingHost)) {
+ if (!nextContent->IsText() || !nextContent->IsEditable()) {
+ if (nextContent == GetEndReasonContent()) {
+ break; // Reached end of current runs.
+ }
+ continue;
+ }
+ return EditorDOMPointType(nextContent->AsText(), 0);
+ }
+ return EditorDOMPointType();
+}
+
+template <typename EditorDOMPointType, typename PT, typename CT>
+EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (NS_WARN_IF(!aPoint.IsInContentNode()) ||
+ NS_WARN_IF(!mScanStartPoint.IsInContentNode())) {
+ return EditorDOMPointType();
+ }
+
+ EditorRawDOMPoint point;
+ if (nsIContent* previousChild = aPoint.CanContainerHaveChildren()
+ ? aPoint.GetPreviousSiblingOfChild()
+ : nullptr) {
+ nsIContent* leafContent =
+ previousChild->HasChildren()
+ ? HTMLEditUtils::GetLastLeafContent(*previousChild,
+ {LeafNodeType::OnlyLeafNode})
+ : previousChild;
+ if (NS_WARN_IF(!leafContent)) {
+ return EditorDOMPointType();
+ }
+ point.SetToEndOf(leafContent);
+ } else {
+ point = aPoint.template To<EditorRawDOMPoint>();
+ }
+
+ // If it points a character in a text node and it's not first character
+ // in it, return its previous point.
+ // XXX For the performance, this does not check whether the container
+ // is outside of our range.
+ if (point.IsInTextNode() && point.GetContainer()->IsEditable() &&
+ !point.IsStartOfContainer()) {
+ return EditorDOMPointType(point.ContainerAs<Text>(), point.Offset() - 1);
+ }
+
+ if (point.GetContainer() == GetStartReasonContent()) {
+ return EditorDOMPointType();
+ }
+
+ NS_ASSERTION(
+ EditorUtils::IsEditableContent(*mScanStartPoint.ContainerAs<nsIContent>(),
+ EditorType::HTML),
+ "Given content is not editable");
+ NS_ASSERTION(
+ mScanStartPoint.ContainerAs<nsIContent>()->GetAsElementOrParentElement(),
+ "Given content is not an element and an orphan node");
+ nsIContent* editableBlockElementOrInlineEditingHost =
+ mScanStartPoint.ContainerAs<nsIContent>() &&
+ EditorUtils::IsEditableContent(
+ *mScanStartPoint.ContainerAs<nsIContent>(), EditorType::HTML)
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *mScanStartPoint.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost,
+ mBlockInlineCheck)
+ : nullptr;
+ if (NS_WARN_IF(!editableBlockElementOrInlineEditingHost)) {
+ // Meaning that the container of `mScanStartPoint` is not editable.
+ editableBlockElementOrInlineEditingHost =
+ mScanStartPoint.ContainerAs<nsIContent>();
+ }
+
+ for (nsIContent* previousContent =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *point.ContainerAs<nsIContent>(),
+ *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, mBlockInlineCheck,
+ mEditingHost);
+ previousContent;
+ previousContent =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *previousContent, *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, mBlockInlineCheck,
+ mEditingHost)) {
+ if (!previousContent->IsText() || !previousContent->IsEditable()) {
+ if (previousContent == GetStartReasonContent()) {
+ break; // Reached start of current runs.
+ }
+ continue;
+ }
+ return EditorDOMPointType(previousContent->AsText(),
+ previousContent->AsText()->TextLength()
+ ? previousContent->AsText()->TextLength() - 1
+ : 0);
+ }
+ return EditorDOMPointType();
+}
+
+// static
+template <typename EditorDOMPointType>
+EditorDOMPointType WSRunScanner::GetAfterLastVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter) {
+ EditorDOMPoint atLastCharOfTextNode(
+ &aTextNode, AssertedCast<uint32_t>(std::max<int64_t>(
+ static_cast<int64_t>(aTextNode.Length()) - 1, 0)));
+ if (!atLastCharOfTextNode.IsContainerEmpty() &&
+ !atLastCharOfTextNode.IsCharCollapsibleASCIISpace()) {
+ return EditorDOMPointType::AtEndOf(aTextNode);
+ }
+ TextFragmentData textFragmentData(atLastCharOfTextNode, aAncestorLimiter,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
+ return EditorDOMPointType(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRange& invisibleWhiteSpaceRange =
+ textFragmentData.InvisibleTrailingWhiteSpaceRangeRef();
+ if (!invisibleWhiteSpaceRange.IsPositioned() ||
+ invisibleWhiteSpaceRange.Collapsed()) {
+ return EditorDOMPointType::AtEndOf(aTextNode);
+ }
+ return invisibleWhiteSpaceRange.StartRef().To<EditorDOMPointType>();
+}
+
+// static
+template <typename EditorDOMPointType>
+EditorDOMPointType WSRunScanner::GetFirstVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter) {
+ EditorDOMPoint atStartOfTextNode(&aTextNode, 0);
+ if (!atStartOfTextNode.IsContainerEmpty() &&
+ atStartOfTextNode.IsCharCollapsibleASCIISpace()) {
+ return atStartOfTextNode.To<EditorDOMPointType>();
+ }
+ TextFragmentData textFragmentData(atStartOfTextNode, aAncestorLimiter,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
+ return EditorDOMPointType(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRange& invisibleWhiteSpaceRange =
+ textFragmentData.InvisibleLeadingWhiteSpaceRangeRef();
+ if (!invisibleWhiteSpaceRange.IsPositioned() ||
+ invisibleWhiteSpaceRange.Collapsed()) {
+ return atStartOfTextNode.To<EditorDOMPointType>();
+ }
+ return invisibleWhiteSpaceRange.EndRef().To<EditorDOMPointType>();
+}
+
+template <typename EditorDOMPointType>
+EditorDOMPointType
+WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces(
+ const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
+ nsIEditor::EDirection aDirectionToDelete) const {
+ MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
+ aDirectionToDelete == nsIEditor::eNext ||
+ aDirectionToDelete == nsIEditor::ePrevious);
+ MOZ_ASSERT(aPointAtASCIIWhiteSpace.IsSet());
+ MOZ_ASSERT(!aPointAtASCIIWhiteSpace.IsEndOfContainer());
+ MOZ_ASSERT_IF(!EditorUtils::IsNewLinePreformatted(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
+ aPointAtASCIIWhiteSpace.IsCharCollapsibleASCIISpace());
+ MOZ_ASSERT_IF(EditorUtils::IsNewLinePreformatted(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
+ aPointAtASCIIWhiteSpace.IsCharASCIISpace());
+
+ // If we're deleting text forward and the next visible character is first
+ // preformatted new line but white-spaces can be collapsed, we need to
+ // delete its following collapsible white-spaces too.
+ bool hasSeenPreformattedNewLine =
+ aPointAtASCIIWhiteSpace.IsCharPreformattedNewLine();
+ auto NeedToScanFollowingWhiteSpaces =
+ [&hasSeenPreformattedNewLine, &aDirectionToDelete](
+ const EditorDOMPointInText& aAtNextVisibleCharacter) -> bool {
+ MOZ_ASSERT(!aAtNextVisibleCharacter.IsEndOfContainer());
+ return !hasSeenPreformattedNewLine &&
+ aDirectionToDelete == nsIEditor::eNext &&
+ aAtNextVisibleCharacter
+ .IsCharPreformattedNewLineCollapsedWithWhiteSpaces();
+ };
+ auto ScanNextNonCollapsibleChar =
+ [&hasSeenPreformattedNewLine, &NeedToScanFollowingWhiteSpaces](
+ const EditorDOMPointInText& aPoint) -> EditorDOMPointInText {
+ Maybe<uint32_t> nextVisibleCharOffset =
+ HTMLEditUtils::GetNextNonCollapsibleCharOffset(aPoint);
+ if (!nextVisibleCharOffset.isSome()) {
+ return EditorDOMPointInText(); // Keep scanning following text nodes
+ }
+ EditorDOMPointInText atNextVisibleChar(aPoint.ContainerAs<Text>(),
+ nextVisibleCharOffset.value());
+ if (!NeedToScanFollowingWhiteSpaces(atNextVisibleChar)) {
+ return atNextVisibleChar;
+ }
+ hasSeenPreformattedNewLine |= atNextVisibleChar.IsCharPreformattedNewLine();
+ nextVisibleCharOffset =
+ HTMLEditUtils::GetNextNonCollapsibleCharOffset(atNextVisibleChar);
+ if (nextVisibleCharOffset.isSome()) {
+ MOZ_ASSERT(aPoint.ContainerAs<Text>() ==
+ atNextVisibleChar.ContainerAs<Text>());
+ return EditorDOMPointInText(atNextVisibleChar.ContainerAs<Text>(),
+ nextVisibleCharOffset.value());
+ }
+ return EditorDOMPointInText(); // Keep scanning following text nodes
+ };
+
+ // If it's not the last character in the text node, let's scan following
+ // characters in it.
+ if (!aPointAtASCIIWhiteSpace.IsAtLastContent()) {
+ const EditorDOMPointInText atNextVisibleChar(
+ ScanNextNonCollapsibleChar(aPointAtASCIIWhiteSpace));
+ if (atNextVisibleChar.IsSet()) {
+ return atNextVisibleChar.To<EditorDOMPointType>();
+ }
+ }
+
+ // Otherwise, i.e., the text node ends with ASCII white-space, keep scanning
+ // the following text nodes.
+ // XXX Perhaps, we should stop scanning if there is non-editable and visible
+ // content.
+ EditorDOMPointInText afterLastWhiteSpace = EditorDOMPointInText::AtEndOf(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>());
+ for (EditorDOMPointInText atEndOfPreviousTextNode = afterLastWhiteSpace;;) {
+ const auto atStartOfNextTextNode =
+ GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ atEndOfPreviousTextNode);
+ if (!atStartOfNextTextNode.IsSet()) {
+ // There is no more text nodes. Return end of the previous text node.
+ return afterLastWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // We can ignore empty text nodes (even if it's preformatted).
+ if (atStartOfNextTextNode.IsContainerEmpty()) {
+ atEndOfPreviousTextNode = atStartOfNextTextNode;
+ continue;
+ }
+
+ // If next node starts with non-white-space character or next node is
+ // preformatted, return end of previous text node. However, if it
+ // starts with a preformatted linefeed but white-spaces are collapsible,
+ // we need to scan following collapsible white-spaces when we're deleting
+ // text forward.
+ if (!atStartOfNextTextNode.IsCharCollapsibleASCIISpace() &&
+ !NeedToScanFollowingWhiteSpaces(atStartOfNextTextNode)) {
+ return afterLastWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // Otherwise, scan the text node.
+ const EditorDOMPointInText atNextVisibleChar(
+ ScanNextNonCollapsibleChar(atStartOfNextTextNode));
+ if (atNextVisibleChar.IsSet()) {
+ return atNextVisibleChar.To<EditorDOMPointType>();
+ }
+
+ // The next text nodes ends with white-space too. Try next one.
+ afterLastWhiteSpace = atEndOfPreviousTextNode =
+ EditorDOMPointInText::AtEndOf(
+ *atStartOfNextTextNode.ContainerAs<Text>());
+ }
+}
+
+template <typename EditorDOMPointType>
+EditorDOMPointType
+WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo(
+ const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
+ nsIEditor::EDirection aDirectionToDelete) const {
+ MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
+ aDirectionToDelete == nsIEditor::eNext ||
+ aDirectionToDelete == nsIEditor::ePrevious);
+ MOZ_ASSERT(aPointAtASCIIWhiteSpace.IsSet());
+ MOZ_ASSERT(!aPointAtASCIIWhiteSpace.IsEndOfContainer());
+ MOZ_ASSERT_IF(!EditorUtils::IsNewLinePreformatted(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
+ aPointAtASCIIWhiteSpace.IsCharCollapsibleASCIISpace());
+ MOZ_ASSERT_IF(EditorUtils::IsNewLinePreformatted(
+ *aPointAtASCIIWhiteSpace.ContainerAs<Text>()),
+ aPointAtASCIIWhiteSpace.IsCharASCIISpace());
+
+ // If we're deleting text backward and the previous visible character is first
+ // preformatted new line but white-spaces can be collapsed, we need to delete
+ // its preceding collapsible white-spaces too.
+ bool hasSeenPreformattedNewLine =
+ aPointAtASCIIWhiteSpace.IsCharPreformattedNewLine();
+ auto NeedToScanPrecedingWhiteSpaces =
+ [&hasSeenPreformattedNewLine, &aDirectionToDelete](
+ const EditorDOMPointInText& aAtPreviousVisibleCharacter) -> bool {
+ MOZ_ASSERT(!aAtPreviousVisibleCharacter.IsEndOfContainer());
+ return !hasSeenPreformattedNewLine &&
+ aDirectionToDelete == nsIEditor::ePrevious &&
+ aAtPreviousVisibleCharacter
+ .IsCharPreformattedNewLineCollapsedWithWhiteSpaces();
+ };
+ auto ScanPreviousNonCollapsibleChar =
+ [&hasSeenPreformattedNewLine, &NeedToScanPrecedingWhiteSpaces](
+ const EditorDOMPointInText& aPoint) -> EditorDOMPointInText {
+ Maybe<uint32_t> previousVisibleCharOffset =
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(aPoint);
+ if (previousVisibleCharOffset.isNothing()) {
+ return EditorDOMPointInText(); // Keep scanning preceding text nodes
+ }
+ EditorDOMPointInText atPreviousVisibleCharacter(
+ aPoint.ContainerAs<Text>(), previousVisibleCharOffset.value());
+ if (!NeedToScanPrecedingWhiteSpaces(atPreviousVisibleCharacter)) {
+ return atPreviousVisibleCharacter.NextPoint();
+ }
+ hasSeenPreformattedNewLine |=
+ atPreviousVisibleCharacter.IsCharPreformattedNewLine();
+ previousVisibleCharOffset =
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
+ atPreviousVisibleCharacter);
+ if (previousVisibleCharOffset.isSome()) {
+ MOZ_ASSERT(aPoint.ContainerAs<Text>() ==
+ atPreviousVisibleCharacter.ContainerAs<Text>());
+ return EditorDOMPointInText(
+ atPreviousVisibleCharacter.ContainerAs<Text>(),
+ previousVisibleCharOffset.value() + 1);
+ }
+ return EditorDOMPointInText(); // Keep scanning preceding text nodes
+ };
+
+ // If there is some characters before it, scan it in the text node first.
+ if (!aPointAtASCIIWhiteSpace.IsStartOfContainer()) {
+ EditorDOMPointInText atFirstASCIIWhiteSpace(
+ ScanPreviousNonCollapsibleChar(aPointAtASCIIWhiteSpace));
+ if (atFirstASCIIWhiteSpace.IsSet()) {
+ return atFirstASCIIWhiteSpace.To<EditorDOMPointType>();
+ }
+ }
+
+ // Otherwise, i.e., the text node starts with ASCII white-space, keep scanning
+ // the preceding text nodes.
+ // XXX Perhaps, we should stop scanning if there is non-editable and visible
+ // content.
+ EditorDOMPointInText atLastWhiteSpace =
+ EditorDOMPointInText(aPointAtASCIIWhiteSpace.ContainerAs<Text>(), 0u);
+ for (EditorDOMPointInText atStartOfPreviousTextNode = atLastWhiteSpace;;) {
+ const EditorDOMPointInText atLastCharOfPreviousTextNode =
+ GetPreviousEditableCharPoint(atStartOfPreviousTextNode);
+ if (!atLastCharOfPreviousTextNode.IsSet()) {
+ // There is no more text nodes. Return end of last text node.
+ return atLastWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // We can ignore empty text nodes (even if it's preformatted).
+ if (atLastCharOfPreviousTextNode.IsContainerEmpty()) {
+ atStartOfPreviousTextNode = atLastCharOfPreviousTextNode;
+ continue;
+ }
+
+ // If next node ends with non-white-space character or next node is
+ // preformatted, return start of previous text node.
+ if (!atLastCharOfPreviousTextNode.IsCharCollapsibleASCIISpace() &&
+ !NeedToScanPrecedingWhiteSpaces(atLastCharOfPreviousTextNode)) {
+ return atLastWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // Otherwise, scan the text node.
+ const EditorDOMPointInText atFirstASCIIWhiteSpace(
+ ScanPreviousNonCollapsibleChar(atLastCharOfPreviousTextNode));
+ if (atFirstASCIIWhiteSpace.IsSet()) {
+ return atFirstASCIIWhiteSpace.To<EditorDOMPointType>();
+ }
+
+ // The next text nodes starts with white-space too. Try next one.
+ atLastWhiteSpace = atStartOfPreviousTextNode = EditorDOMPointInText(
+ atLastCharOfPreviousTextNode.ContainerAs<Text>(), 0u);
+ }
+}
+
+// static
+nsresult WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes(
+ HTMLEditor& aHTMLEditor, const EditorDOMRangeInTexts& aRangeToReplace,
+ const nsAString& aReplaceString) {
+ MOZ_ASSERT(aRangeToReplace.IsPositioned());
+ MOZ_ASSERT(aRangeToReplace.StartRef().IsSetAndValid());
+ MOZ_ASSERT(aRangeToReplace.EndRef().IsSetAndValid());
+ MOZ_ASSERT(aRangeToReplace.StartRef().IsBefore(aRangeToReplace.EndRef()));
+
+ {
+ Result<InsertTextResult, nsresult> caretPointOrError =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(*aRangeToReplace.StartRef().ContainerAs<Text>()),
+ aRangeToReplace.StartRef().Offset(),
+ aRangeToReplace.InSameContainer()
+ ? aRangeToReplace.EndRef().Offset() -
+ aRangeToReplace.StartRef().Offset()
+ : aRangeToReplace.StartRef().ContainerAs<Text>()->TextLength() -
+ aRangeToReplace.StartRef().Offset(),
+ aReplaceString);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ }
+
+ if (aRangeToReplace.InSameContainer()) {
+ return NS_OK;
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ EditorDOMPointInText::AtEndOf(
+ *aRangeToReplace.StartRef().ContainerAs<Text>()),
+ aRangeToReplace.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ // Ignore caret suggestion because there was
+ // AutoTransactionsConserveSelection.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+char16_t WSRunScanner::GetCharAt(Text* aTextNode, uint32_t aOffset) const {
+ // return 0 if we can't get a char, for whatever reason
+ if (NS_WARN_IF(!aTextNode) ||
+ NS_WARN_IF(aOffset >= aTextNode->TextDataLength())) {
+ return 0;
+ }
+ return aTextNode->TextFragment().CharAt(aOffset);
+}
+
+// static
+template <typename EditorDOMPointType>
+nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ HTMLEditor& aHTMLEditor, const EditorDOMPointType& aPoint) {
+ MOZ_ASSERT(aPoint.IsInContentNode());
+ MOZ_ASSERT(EditorUtils::IsEditableContent(
+ *aPoint.template ContainerAs<nsIContent>(), EditorType::HTML));
+ Element* editingHost = aHTMLEditor.ComputeEditingHost();
+ TextFragmentData textFragmentData(aPoint, editingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // this routine examines a run of ws and tries to get rid of some unneeded
+ // nbsp's, replacing them with regular ascii space if possible. Keeping
+ // things simple for now and just trying to fix up the trailing ws in the run.
+ if (!textFragmentData.FoundNoBreakingWhiteSpaces()) {
+ // nothing to do!
+ return NS_OK;
+ }
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ textFragmentData.VisibleWhiteSpacesDataRef();
+ if (!visibleWhiteSpaces.IsInitialized()) {
+ return NS_OK;
+ }
+
+ // Remove this block if we ship Blink-compat white-space normalization.
+ if (!StaticPrefs::editor_white_space_normalization_blink_compatible()) {
+ // now check that what is to the left of it is compatible with replacing
+ // nbsp with space
+ const EditorDOMPoint& atEndOfVisibleWhiteSpaces =
+ visibleWhiteSpaces.EndRef();
+ EditorDOMPointInText atPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atEndOfVisibleWhiteSpaces);
+ if (!atPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() ||
+ atPreviousCharOfEndOfVisibleWhiteSpaces.IsEndOfContainer() ||
+ // If the NBSP is never replaced from an ASCII white-space, we cannot
+ // replace it with an ASCII white-space.
+ !atPreviousCharOfEndOfVisibleWhiteSpaces.IsCharCollapsibleNBSP()) {
+ return NS_OK;
+ }
+
+ // now check that what is to the left of it is compatible with replacing
+ // nbsp with space
+ EditorDOMPointInText atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atPreviousCharOfEndOfVisibleWhiteSpaces);
+ bool isPreviousCharCollapsibleASCIIWhiteSpace =
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ !atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsEndOfContainer() &&
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsCharCollapsibleASCIISpace();
+ const bool maybeNBSPFollowsVisibleContent =
+ (atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ !isPreviousCharCollapsibleASCIIWhiteSpace) ||
+ (!atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ (visibleWhiteSpaces.StartsFromNonCollapsibleCharacters() ||
+ visibleWhiteSpaces.StartsFromSpecialContent()));
+ bool followedByVisibleContent =
+ visibleWhiteSpaces.EndsByNonCollapsibleCharacters() ||
+ visibleWhiteSpaces.EndsBySpecialContent();
+ bool followedByBRElement = visibleWhiteSpaces.EndsByBRElement();
+ bool followedByPreformattedLineBreak =
+ visibleWhiteSpaces.EndsByPreformattedLineBreak();
+
+ // If the NBSP follows a visible content or a collapsible ASCII white-space,
+ // i.e., unless NBSP is first character and start of a block, we may need to
+ // insert <br> element and restore the NBSP to an ASCII white-space.
+ if (maybeNBSPFollowsVisibleContent ||
+ isPreviousCharCollapsibleASCIIWhiteSpace) {
+ // First, try to insert <br> element if NBSP is at end of a block.
+ // XXX We should stop this if there is a visible content.
+ if (visibleWhiteSpaces.EndsByBlockBoundary() &&
+ aPoint.IsInContentNode()) {
+ bool insertBRElement = HTMLEditUtils::IsBlockElement(
+ *aPoint.template ContainerAs<nsIContent>(),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (!insertBRElement) {
+ NS_ASSERTION(
+ EditorUtils::IsEditableContent(
+ *aPoint.template ContainerAs<nsIContent>(), EditorType::HTML),
+ "Given content is not editable");
+ NS_ASSERTION(aPoint.template ContainerAs<nsIContent>()
+ ->GetAsElementOrParentElement(),
+ "Given content is not an element and an orphan node");
+ const Element* editableBlockElement =
+ EditorUtils::IsEditableContent(
+ *aPoint.template ContainerAs<nsIContent>(), EditorType::HTML)
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *aPoint.template ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle)
+ : nullptr;
+ insertBRElement = !!editableBlockElement;
+ }
+ if (insertBRElement) {
+ // We are at a block boundary. Insert a <br>. Why? Well, first note
+ // that the br will have no visible effect since it is up against a
+ // block boundary. |foo<br><p>bar| renders like |foo<p>bar| and
+ // similarly |<p>foo<br></p>bar| renders like |<p>foo</p>bar|. What
+ // this <br> addition gets us is the ability to convert a trailing
+ // nbsp to a space. Consider: |<body>foo. '</body>|, where '
+ // represents selection. User types space attempting to put 2 spaces
+ // after the end of their sentence. We used to do this as:
+ // |<body>foo. &nbsp</body>| This caused problems with soft wrapping:
+ // the nbsp would wrap to the next line, which looked attrocious. If
+ // you try to do: |<body>foo.&nbsp </body>| instead, the trailing
+ // space is invisible because it is against a block boundary. If you
+ // do:
+ // |<body>foo.&nbsp&nbsp</body>| then you get an even uglier soft
+ // wrapping problem, where foo is on one line until you type the final
+ // space, and then "foo " jumps down to the next line. Ugh. The
+ // best way I can find out of this is to throw in a harmless <br>
+ // here, which allows us to do: |<body>foo.&nbsp <br></body>|, which
+ // doesn't cause foo to jump lines, doesn't cause spaces to show up at
+ // the beginning of soft wrapped lines, and lets the user see 2 spaces
+ // when they type 2 spaces.
+
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ aHTMLEditor.InsertBRElement(WithTransaction::Yes,
+ atEndOfVisibleWhiteSpaces);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method.
+ insertBRElementResult.unwrap().IgnoreCaretPointSuggestion();
+
+ atPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atEndOfVisibleWhiteSpaces);
+ if (NS_WARN_IF(!atPreviousCharOfEndOfVisibleWhiteSpaces.IsSet())) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atPreviousCharOfEndOfVisibleWhiteSpaces);
+ isPreviousCharCollapsibleASCIIWhiteSpace =
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ !atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsEndOfContainer() &&
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsCharCollapsibleASCIISpace();
+ followedByBRElement = true;
+ followedByVisibleContent = followedByPreformattedLineBreak = false;
+ }
+ }
+
+ // Once insert a <br>, the remaining work is only for normalizing
+ // white-space sequence in white-space collapsible text node.
+ // So, if the the text node's white-spaces are preformatted, we need
+ // to do nothing anymore.
+ if (EditorUtils::IsWhiteSpacePreformatted(
+ *atPreviousCharOfEndOfVisibleWhiteSpaces.ContainerAs<Text>())) {
+ return NS_OK;
+ }
+
+ // Next, replace the NBSP with an ASCII white-space if it's surrounded
+ // by visible contents (or immediately before a <br> element).
+ // However, if it follows or is followed by a preformatted linefeed,
+ // we shouldn't do this because an ASCII white-space will be collapsed
+ // **into** the linefeed.
+ if (maybeNBSPFollowsVisibleContent &&
+ (followedByVisibleContent || followedByBRElement) &&
+ !visibleWhiteSpaces.StartsFromPreformattedLineBreak()) {
+ MOZ_ASSERT(!followedByPreformattedLineBreak);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(*atPreviousCharOfEndOfVisibleWhiteSpaces
+ .ContainerAs<Text>()),
+ atPreviousCharOfEndOfVisibleWhiteSpaces.Offset(), 1, u" "_ns);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+ }
+ }
+ // If the text node is not preformatted, and the NBSP is followed by a <br>
+ // element and following (maybe multiple) collapsible ASCII white-spaces,
+ // remove the NBSP, but inserts a NBSP before the spaces. This makes a line
+ // break opportunity to wrap the line.
+ // XXX This is different behavior from Blink. Blink generates pairs of
+ // an NBSP and an ASCII white-space, but put NBSP at the end of the
+ // sequence. We should follow the behavior for web-compat.
+ if (maybeNBSPFollowsVisibleContent ||
+ !isPreviousCharCollapsibleASCIIWhiteSpace ||
+ !(followedByVisibleContent || followedByBRElement) ||
+ EditorUtils::IsWhiteSpacePreformatted(
+ *atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .ContainerAs<Text>())) {
+ return NS_OK;
+ }
+
+ // Currently, we're at an NBSP following an ASCII space, and we need to
+ // replace them with `"&nbsp; "` for avoiding collapsing white-spaces.
+ MOZ_ASSERT(!atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsEndOfContainer());
+ const EditorDOMPointInText atFirstASCIIWhiteSpace =
+ textFragmentData.GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces,
+ nsIEditor::eNone);
+ uint32_t numberOfASCIIWhiteSpacesInStartNode =
+ atFirstASCIIWhiteSpace.ContainerAs<Text>() ==
+ atPreviousCharOfEndOfVisibleWhiteSpaces.ContainerAs<Text>()
+ ? atPreviousCharOfEndOfVisibleWhiteSpaces.Offset() -
+ atFirstASCIIWhiteSpace.Offset()
+ : atFirstASCIIWhiteSpace.ContainerAs<Text>()->Length() -
+ atFirstASCIIWhiteSpace.Offset();
+ // Replace all preceding ASCII white-spaces **and** the NBSP.
+ uint32_t replaceLengthInStartNode =
+ numberOfASCIIWhiteSpacesInStartNode +
+ (atFirstASCIIWhiteSpace.ContainerAs<Text>() ==
+ atPreviousCharOfEndOfVisibleWhiteSpaces.ContainerAs<Text>()
+ ? 1
+ : 0);
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ aHTMLEditor.ReplaceTextWithTransaction(
+ MOZ_KnownLive(*atFirstASCIIWhiteSpace.ContainerAs<Text>()),
+ atFirstASCIIWhiteSpace.Offset(), replaceLengthInStartNode,
+ textFragmentData.StartsFromPreformattedLineBreak() &&
+ textFragmentData.EndsByPreformattedLineBreak()
+ ? u"\x00A0\x00A0"_ns
+ : (textFragmentData.EndsByPreformattedLineBreak()
+ ? u" \x00A0"_ns
+ : u"\x00A0 "_ns));
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+
+ if (atFirstASCIIWhiteSpace.GetContainer() ==
+ atPreviousCharOfEndOfVisibleWhiteSpaces.GetContainer()) {
+ return NS_OK;
+ }
+
+ // We need to remove the following unnecessary ASCII white-spaces and
+ // NBSP at atPreviousCharOfEndOfVisibleWhiteSpaces because we collapsed them
+ // into the start node.
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ EditorDOMPointInText::AtEndOf(
+ *atFirstASCIIWhiteSpace.ContainerAs<Text>()),
+ atPreviousCharOfEndOfVisibleWhiteSpaces.NextPoint(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method. }
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+ }
+
+ // XXX This is called when top-level edit sub-action handling ends for
+ // 3 points at most. However, this is not compatible with Blink.
+ // Blink touches white-space sequence which includes new character
+ // or following white-space sequence of new <br> element or, if and
+ // only if deleting range is followed by white-space sequence (i.e.,
+ // not touched previous white-space sequence of deleting range).
+ // This should be done when we change to make each edit action
+ // handler directly normalize white-space sequence rather than
+ // OnEndHandlingTopLevelEditSucAction().
+
+ // First, check if the last character is an NBSP. Otherwise, we don't need
+ // to do nothing here.
+ const EditorDOMPoint& atEndOfVisibleWhiteSpaces = visibleWhiteSpaces.EndRef();
+ const EditorDOMPointInText atPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(atEndOfVisibleWhiteSpaces);
+ if (!atPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() ||
+ atPreviousCharOfEndOfVisibleWhiteSpaces.IsEndOfContainer() ||
+ !atPreviousCharOfEndOfVisibleWhiteSpaces.IsCharCollapsibleNBSP() ||
+ // If the next character of the NBSP is a preformatted linefeed, we
+ // shouldn't replace it with an ASCII white-space for avoiding collapsed
+ // into the linefeed.
+ visibleWhiteSpaces.EndsByPreformattedLineBreak()) {
+ return NS_OK;
+ }
+
+ // Next, consider the range to collapse ASCII white-spaces before there.
+ EditorDOMPointInText startToDelete, endToDelete;
+
+ const EditorDOMPointInText
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces =
+ textFragmentData.GetPreviousEditableCharPoint(
+ atPreviousCharOfEndOfVisibleWhiteSpaces);
+ // If there are some preceding ASCII white-spaces, we need to treat them
+ // as one white-space. I.e., we need to collapse them.
+ if (atPreviousCharOfEndOfVisibleWhiteSpaces.IsCharNBSP() &&
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces.IsSet() &&
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces
+ .IsCharCollapsibleASCIISpace()) {
+ startToDelete = textFragmentData.GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces,
+ nsIEditor::eNone);
+ endToDelete = atPreviousCharOfPreviousCharOfEndOfVisibleWhiteSpaces;
+ }
+ // Otherwise, we don't need to remove any white-spaces, but we may need
+ // to normalize the white-space sequence containing the previous NBSP.
+ else {
+ startToDelete = endToDelete =
+ atPreviousCharOfEndOfVisibleWhiteSpaces.NextPoint();
+ }
+
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndNormalizeSurroundingWhiteSpaces(
+ startToDelete, endToDelete,
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries,
+ HTMLEditor::DeleteDirection::Forward);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpace() failed");
+ return caretPointOrError.unwrapErr();
+ }
+ // Ignore caret suggestion because the caller must want to restore
+ // `Selection` due to the purpose of this method.
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+EditorDOMPointInText WSRunScanner::TextFragmentData::
+ GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ const EditorDOMPoint& aPointToInsert) const {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+ MOZ_ASSERT(VisibleWhiteSpacesDataRef().IsInitialized());
+ NS_ASSERTION(VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
+ PointPosition::MiddleOfFragment ||
+ VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
+ PointPosition::EndOfFragment,
+ "Previous char of aPoint should be in the visible white-spaces");
+
+ // Try to change an NBSP to a space, if possible, just to prevent NBSP
+ // proliferation. This routine is called when we are about to make this
+ // point in the ws abut an inserted break or text, so we don't have to worry
+ // about what is after it. What is after it now will end up after the
+ // inserted object.
+ const EditorDOMPointInText atPreviousChar =
+ GetPreviousEditableCharPoint(aPointToInsert);
+ if (!atPreviousChar.IsSet() || atPreviousChar.IsEndOfContainer() ||
+ !atPreviousChar.IsCharNBSP() ||
+ EditorUtils::IsWhiteSpacePreformatted(
+ *atPreviousChar.ContainerAs<Text>())) {
+ return EditorDOMPointInText();
+ }
+
+ const EditorDOMPointInText atPreviousCharOfPreviousChar =
+ GetPreviousEditableCharPoint(atPreviousChar);
+ if (atPreviousCharOfPreviousChar.IsSet()) {
+ // If the previous char is in different text node and it's preformatted,
+ // we shouldn't touch it.
+ if (atPreviousChar.ContainerAs<Text>() !=
+ atPreviousCharOfPreviousChar.ContainerAs<Text>() &&
+ EditorUtils::IsWhiteSpacePreformatted(
+ *atPreviousCharOfPreviousChar.ContainerAs<Text>())) {
+ return EditorDOMPointInText();
+ }
+ // If the previous char of the NBSP at previous position of aPointToInsert
+ // is an ASCII white-space, we don't need to replace it with same character.
+ if (!atPreviousCharOfPreviousChar.IsEndOfContainer() &&
+ atPreviousCharOfPreviousChar.IsCharASCIISpace()) {
+ return EditorDOMPointInText();
+ }
+ return atPreviousChar;
+ }
+
+ // If previous content of the NBSP is block boundary, we cannot replace the
+ // NBSP with an ASCII white-space to keep it rendered.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ VisibleWhiteSpacesDataRef();
+ if (!visibleWhiteSpaces.StartsFromNonCollapsibleCharacters() &&
+ !visibleWhiteSpaces.StartsFromSpecialContent()) {
+ return EditorDOMPointInText();
+ }
+ return atPreviousChar;
+}
+
+EditorDOMPointInText WSRunScanner::TextFragmentData::
+ GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ const EditorDOMPoint& aPointToInsert) const {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+ MOZ_ASSERT(VisibleWhiteSpacesDataRef().IsInitialized());
+ NS_ASSERTION(VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
+ PointPosition::StartOfFragment ||
+ VisibleWhiteSpacesDataRef().ComparePoint(aPointToInsert) ==
+ PointPosition::MiddleOfFragment,
+ "Inclusive next char of aPointToInsert should be in the visible "
+ "white-spaces");
+
+ // Try to change an nbsp to a space, if possible, just to prevent nbsp
+ // proliferation This routine is called when we are about to make this point
+ // in the ws abut an inserted text, so we don't have to worry about what is
+ // before it. What is before it now will end up before the inserted text.
+ auto atNextChar =
+ GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(aPointToInsert);
+ if (!atNextChar.IsSet() || NS_WARN_IF(atNextChar.IsEndOfContainer()) ||
+ !atNextChar.IsCharNBSP() ||
+ EditorUtils::IsWhiteSpacePreformatted(*atNextChar.ContainerAs<Text>())) {
+ return EditorDOMPointInText();
+ }
+
+ const auto atNextCharOfNextCharOfNBSP =
+ GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ atNextChar.NextPoint<EditorRawDOMPointInText>());
+ if (atNextCharOfNextCharOfNBSP.IsSet()) {
+ // If the next char is in different text node and it's preformatted,
+ // we shouldn't touch it.
+ if (atNextChar.ContainerAs<Text>() !=
+ atNextCharOfNextCharOfNBSP.ContainerAs<Text>() &&
+ EditorUtils::IsWhiteSpacePreformatted(
+ *atNextCharOfNextCharOfNBSP.ContainerAs<Text>())) {
+ return EditorDOMPointInText();
+ }
+ // If following character of an NBSP is an ASCII white-space, we don't
+ // need to replace it with same character.
+ if (!atNextCharOfNextCharOfNBSP.IsEndOfContainer() &&
+ atNextCharOfNextCharOfNBSP.IsCharASCIISpace()) {
+ return EditorDOMPointInText();
+ }
+ return atNextChar;
+ }
+
+ // If the NBSP is last character in the hard line, we don't need to
+ // replace it because it's required to render multiple white-spaces.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ VisibleWhiteSpacesDataRef();
+ if (!visibleWhiteSpaces.EndsByNonCollapsibleCharacters() &&
+ !visibleWhiteSpaces.EndsBySpecialContent() &&
+ !visibleWhiteSpaces.EndsByBRElement()) {
+ return EditorDOMPointInText();
+ }
+
+ return atNextChar;
+}
+
+// static
+Result<CaretPoint, nsresult>
+WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) {
+ MOZ_ASSERT(aPoint.IsSet());
+ Element* editingHost = aHTMLEditor.ComputeEditingHost();
+ TextFragmentData textFragmentData(aPoint, editingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentData.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorDOMRange& leadingWhiteSpaceRange =
+ textFragmentData.InvisibleLeadingWhiteSpaceRangeRef();
+ // XXX Getting trailing white-space range now must be wrong because
+ // mutation event listener may invalidate it.
+ const EditorDOMRange& trailingWhiteSpaceRange =
+ textFragmentData.InvisibleTrailingWhiteSpaceRangeRef();
+ EditorDOMPoint pointToPutCaret;
+ DebugOnly<bool> leadingWhiteSpacesDeleted = false;
+ if (leadingWhiteSpaceRange.IsPositioned() &&
+ !leadingWhiteSpaceRange.Collapsed()) {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ leadingWhiteSpaceRange.StartRef(), leadingWhiteSpaceRange.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError;
+ }
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ leadingWhiteSpacesDeleted = true;
+ }
+ if (trailingWhiteSpaceRange.IsPositioned() &&
+ !trailingWhiteSpaceRange.Collapsed() &&
+ leadingWhiteSpaceRange != trailingWhiteSpaceRange) {
+ NS_ASSERTION(!leadingWhiteSpacesDeleted,
+ "We're trying to remove trailing white-spaces with maybe "
+ "outdated range");
+ AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ aHTMLEditor.DeleteTextAndTextNodesWithTransaction(
+ trailingWhiteSpaceRange.StartRef(),
+ trailingWhiteSpaceRange.EndRef(),
+ HTMLEditor::TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING("HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ trackPointToPutCaret.FlushAndStopTracking();
+ caretPointOrError.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return CaretPoint(std::move(pointToPutCaret));
+}
+
+/*****************************************************************************
+ * Implementation for new white-space normalizer
+ *****************************************************************************/
+
+// static
+EditorDOMRangeInTexts
+WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
+ const TextFragmentData& aStart, const TextFragmentData& aEnd) {
+ // Corresponding to handling invisible white-spaces part of
+ // `TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange()` and
+ // `TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange()`
+
+ MOZ_ASSERT(aStart.ScanStartRef().IsSetAndValid());
+ MOZ_ASSERT(aEnd.ScanStartRef().IsSetAndValid());
+ MOZ_ASSERT(aStart.ScanStartRef().EqualsOrIsBefore(aEnd.ScanStartRef()));
+ MOZ_ASSERT(aStart.ScanStartRef().IsInTextNode());
+ MOZ_ASSERT(aEnd.ScanStartRef().IsInTextNode());
+
+ // XXX `GetReplaceRangeDataAtEndOfDeletionRange()` and
+ // `GetReplaceRangeDataAtStartOfDeletionRange()` use
+ // `GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt()` and
+ // `GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt()`.
+ // However, they are really odd as mentioned with "XXX" comments
+ // in them. For the new white-space normalizer, we need to treat
+ // invisible white-spaces stricter because the legacy path handles
+ // white-spaces multiple times (e.g., calling `HTMLEditor::
+ // DeleteNodeIfInvisibleAndEditableTextNode()` later) and that hides
+ // the bug, but in the new path, we should stop doing same things
+ // multiple times for both performance and footprint. Therefore,
+ // even though the result might be different in some edge cases,
+ // we should use clean path for now. Perhaps, we should fix the odd
+ // cases before shipping `beforeinput` event in release channel.
+
+ const EditorDOMRange& invisibleLeadingWhiteSpaceRange =
+ aStart.InvisibleLeadingWhiteSpaceRangeRef();
+ const EditorDOMRange& invisibleTrailingWhiteSpaceRange =
+ aEnd.InvisibleTrailingWhiteSpaceRangeRef();
+ const bool hasInvisibleLeadingWhiteSpaces =
+ invisibleLeadingWhiteSpaceRange.IsPositioned() &&
+ !invisibleLeadingWhiteSpaceRange.Collapsed();
+ const bool hasInvisibleTrailingWhiteSpaces =
+ invisibleLeadingWhiteSpaceRange != invisibleTrailingWhiteSpaceRange &&
+ invisibleTrailingWhiteSpaceRange.IsPositioned() &&
+ !invisibleTrailingWhiteSpaceRange.Collapsed();
+
+ EditorDOMRangeInTexts result(aStart.ScanStartRef().AsInText(),
+ aEnd.ScanStartRef().AsInText());
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ if (!hasInvisibleLeadingWhiteSpaces && !hasInvisibleTrailingWhiteSpaces) {
+ return result;
+ }
+
+ MOZ_ASSERT_IF(
+ hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces,
+ invisibleLeadingWhiteSpaceRange.StartRef().IsBefore(
+ invisibleTrailingWhiteSpaceRange.StartRef()));
+ const EditorDOMPoint& aroundFirstInvisibleWhiteSpace =
+ hasInvisibleLeadingWhiteSpaces
+ ? invisibleLeadingWhiteSpaceRange.StartRef()
+ : invisibleTrailingWhiteSpaceRange.StartRef();
+ if (aroundFirstInvisibleWhiteSpace.IsBefore(result.StartRef())) {
+ if (aroundFirstInvisibleWhiteSpace.IsInTextNode()) {
+ result.SetStart(aroundFirstInvisibleWhiteSpace.AsInText());
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ } else {
+ const auto atFirstInvisibleWhiteSpace =
+ hasInvisibleLeadingWhiteSpaces
+ ? aStart.GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ aroundFirstInvisibleWhiteSpace)
+ : aEnd.GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(
+ aroundFirstInvisibleWhiteSpace);
+ MOZ_ASSERT(atFirstInvisibleWhiteSpace.IsSet());
+ MOZ_ASSERT(
+ atFirstInvisibleWhiteSpace.EqualsOrIsBefore(result.StartRef()));
+ result.SetStart(atFirstInvisibleWhiteSpace);
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ }
+ }
+ MOZ_ASSERT_IF(
+ hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces,
+ invisibleLeadingWhiteSpaceRange.EndRef().IsBefore(
+ invisibleTrailingWhiteSpaceRange.EndRef()));
+ const EditorDOMPoint& afterLastInvisibleWhiteSpace =
+ hasInvisibleTrailingWhiteSpaces
+ ? invisibleTrailingWhiteSpaceRange.EndRef()
+ : invisibleLeadingWhiteSpaceRange.EndRef();
+ if (afterLastInvisibleWhiteSpace.EqualsOrIsBefore(result.EndRef())) {
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ return result;
+ }
+ if (afterLastInvisibleWhiteSpace.IsInTextNode()) {
+ result.SetEnd(afterLastInvisibleWhiteSpace.AsInText());
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ return result;
+ }
+ const auto atLastInvisibleWhiteSpace =
+ hasInvisibleTrailingWhiteSpaces
+ ? aEnd.GetPreviousEditableCharPoint(afterLastInvisibleWhiteSpace)
+ : aStart.GetPreviousEditableCharPoint(afterLastInvisibleWhiteSpace);
+ MOZ_ASSERT(atLastInvisibleWhiteSpace.IsSet());
+ MOZ_ASSERT(atLastInvisibleWhiteSpace.IsContainerEmpty() ||
+ atLastInvisibleWhiteSpace.IsAtLastContent());
+ MOZ_ASSERT(result.EndRef().EqualsOrIsBefore(atLastInvisibleWhiteSpace));
+ result.SetEnd(atLastInvisibleWhiteSpace.IsEndOfContainer()
+ ? atLastInvisibleWhiteSpace
+ : atLastInvisibleWhiteSpace.NextPoint());
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ return result;
+}
+
+// static
+Result<EditorDOMRangeInTexts, nsresult>
+WSRunScanner::GetRangeInTextNodesToBackspaceFrom(const EditorDOMPoint& aPoint,
+ const Element& aEditingHost) {
+ // Corresponding to computing delete range part of
+ // `WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace()`
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ TextFragmentData textFragmentDataAtCaret(
+ aPoint, &aEditingHost, BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtCaret.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorDOMPointInText atPreviousChar =
+ textFragmentDataAtCaret.GetPreviousEditableCharPoint(aPoint);
+ if (!atPreviousChar.IsSet()) {
+ return EditorDOMRangeInTexts(); // There is no content in the block.
+ }
+
+ // XXX When previous char point is in an empty text node, we do nothing,
+ // but this must look odd from point of user view. We should delete
+ // something before aPoint.
+ if (atPreviousChar.IsEndOfContainer()) {
+ return EditorDOMRangeInTexts();
+ }
+
+ // Extend delete range if previous char is a low surrogate following
+ // a high surrogate.
+ EditorDOMPointInText atNextChar = atPreviousChar.NextPoint();
+ if (!atPreviousChar.IsStartOfContainer()) {
+ if (atPreviousChar.IsCharLowSurrogateFollowingHighSurrogate()) {
+ atPreviousChar = atPreviousChar.PreviousPoint();
+ }
+ // If caret is in middle of a surrogate pair, delete the surrogate pair
+ // (blink-compat).
+ else if (atPreviousChar.IsCharHighSurrogateFollowedByLowSurrogate()) {
+ atNextChar = atNextChar.NextPoint();
+ }
+ }
+
+ // If previous char is an collapsible white-spaces, delete all adjcent
+ // white-spaces which are collapsed together.
+ EditorDOMRangeInTexts rangeToDelete;
+ if (atPreviousChar.IsCharCollapsibleASCIISpace() ||
+ atPreviousChar.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
+ const EditorDOMPointInText startToDelete =
+ textFragmentDataAtCaret.GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atPreviousChar, nsIEditor::ePrevious);
+ if (!startToDelete.IsSet()) {
+ NS_WARNING(
+ "WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorDOMPointInText endToDelete =
+ textFragmentDataAtCaret.GetEndOfCollapsibleASCIIWhiteSpaces(
+ atPreviousChar, nsIEditor::ePrevious);
+ if (!endToDelete.IsSet()) {
+ NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete);
+ }
+ // if previous char is not a collapsible white-space, remove it.
+ else {
+ rangeToDelete = EditorDOMRangeInTexts(atPreviousChar, atNextChar);
+ }
+
+ // If there is no removable and visible content, we should do nothing.
+ if (rangeToDelete.Collapsed()) {
+ return EditorDOMRangeInTexts();
+ }
+
+ // And also delete invisible white-spaces if they become visible.
+ TextFragmentData textFragmentDataAtStart =
+ rangeToDelete.StartRef() != aPoint
+ ? TextFragmentData(rangeToDelete.StartRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle)
+ : textFragmentDataAtCaret;
+ TextFragmentData textFragmentDataAtEnd =
+ rangeToDelete.EndRef() != aPoint
+ ? TextFragmentData(rangeToDelete.EndRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle)
+ : textFragmentDataAtCaret;
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()) ||
+ NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorDOMRangeInTexts extendedRangeToDelete =
+ WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
+ textFragmentDataAtStart, textFragmentDataAtEnd);
+ MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid());
+ return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete
+ : rangeToDelete;
+}
+
+// static
+Result<EditorDOMRangeInTexts, nsresult>
+WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom(
+ const EditorDOMPoint& aPoint, const Element& aEditingHost) {
+ // Corresponding to computing delete range part of
+ // `WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace()`
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ TextFragmentData textFragmentDataAtCaret(
+ aPoint, &aEditingHost, BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtCaret.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ auto atCaret =
+ textFragmentDataAtCaret
+ .GetInclusiveNextEditableCharPoint<EditorDOMPointInText>(aPoint);
+ if (!atCaret.IsSet()) {
+ return EditorDOMRangeInTexts(); // There is no content in the block.
+ }
+ // If caret is in middle of a surrogate pair, we should remove next
+ // character (blink-compat).
+ if (!atCaret.IsEndOfContainer() &&
+ atCaret.IsCharLowSurrogateFollowingHighSurrogate()) {
+ atCaret = atCaret.NextPoint();
+ }
+
+ // XXX When next char point is in an empty text node, we do nothing,
+ // but this must look odd from point of user view. We should delete
+ // something after aPoint.
+ if (atCaret.IsEndOfContainer()) {
+ return EditorDOMRangeInTexts();
+ }
+
+ // Extend delete range if previous char is a low surrogate following
+ // a high surrogate.
+ EditorDOMPointInText atNextChar = atCaret.NextPoint();
+ if (atCaret.IsCharHighSurrogateFollowedByLowSurrogate()) {
+ atNextChar = atNextChar.NextPoint();
+ }
+
+ // If next char is a collapsible white-space, delete all adjcent white-spaces
+ // which are collapsed together.
+ EditorDOMRangeInTexts rangeToDelete;
+ if (atCaret.IsCharCollapsibleASCIISpace() ||
+ atCaret.IsCharPreformattedNewLineCollapsedWithWhiteSpaces()) {
+ const EditorDOMPointInText startToDelete =
+ textFragmentDataAtCaret.GetFirstASCIIWhiteSpacePointCollapsedTo(
+ atCaret, nsIEditor::eNext);
+ if (!startToDelete.IsSet()) {
+ NS_WARNING(
+ "WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorDOMPointInText endToDelete =
+ textFragmentDataAtCaret.GetEndOfCollapsibleASCIIWhiteSpaces(
+ atCaret, nsIEditor::eNext);
+ if (!endToDelete.IsSet()) {
+ NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete);
+ }
+ // if next char is not a collapsible white-space, remove it.
+ else {
+ rangeToDelete = EditorDOMRangeInTexts(atCaret, atNextChar);
+ }
+
+ // If there is no removable and visible content, we should do nothing.
+ if (rangeToDelete.Collapsed()) {
+ return EditorDOMRangeInTexts();
+ }
+
+ // And also delete invisible white-spaces if they become visible.
+ TextFragmentData textFragmentDataAtStart =
+ rangeToDelete.StartRef() != aPoint
+ ? TextFragmentData(rangeToDelete.StartRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle)
+ : textFragmentDataAtCaret;
+ TextFragmentData textFragmentDataAtEnd =
+ rangeToDelete.EndRef() != aPoint
+ ? TextFragmentData(rangeToDelete.EndRef(), &aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle)
+ : textFragmentDataAtCaret;
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized()) ||
+ NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ EditorDOMRangeInTexts extendedRangeToDelete =
+ WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
+ textFragmentDataAtStart, textFragmentDataAtEnd);
+ MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid());
+ return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete
+ : rangeToDelete;
+}
+
+// static
+EditorDOMRange WSRunScanner::GetRangesForDeletingAtomicContent(
+ Element* aEditingHost, const nsIContent& aAtomicContent) {
+ if (aAtomicContent.IsHTMLElement(nsGkAtoms::br)) {
+ // Preceding white-spaces should be preserved, but the following
+ // white-spaces should be invisible around `<br>` element.
+ TextFragmentData textFragmentDataAfterBRElement(
+ EditorDOMPoint::After(aAtomicContent), aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAfterBRElement.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts followingInvisibleWhiteSpaces =
+ textFragmentDataAfterBRElement.GetNonCollapsedRangeInTexts(
+ textFragmentDataAfterBRElement
+ .InvisibleLeadingWhiteSpaceRangeRef());
+ return followingInvisibleWhiteSpaces.IsPositioned() &&
+ !followingInvisibleWhiteSpaces.Collapsed()
+ ? EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ followingInvisibleWhiteSpaces.EndRef())
+ : EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ EditorDOMPoint::After(aAtomicContent));
+ }
+
+ if (!HTMLEditUtils::IsBlockElement(
+ aAtomicContent, BlockInlineCheck::UseComputedDisplayStyle)) {
+ // Both preceding and following white-spaces around it should be preserved
+ // around inline elements like `<img>`.
+ return EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ EditorDOMPoint::After(aAtomicContent));
+ }
+
+ // Both preceding and following white-spaces can be invisible around a
+ // block element.
+ TextFragmentData textFragmentDataBeforeAtomicContent(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)), aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataBeforeAtomicContent.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts precedingInvisibleWhiteSpaces =
+ textFragmentDataBeforeAtomicContent.GetNonCollapsedRangeInTexts(
+ textFragmentDataBeforeAtomicContent
+ .InvisibleTrailingWhiteSpaceRangeRef());
+ TextFragmentData textFragmentDataAfterAtomicContent(
+ EditorDOMPoint::After(aAtomicContent), aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAfterAtomicContent.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts followingInvisibleWhiteSpaces =
+ textFragmentDataAfterAtomicContent.GetNonCollapsedRangeInTexts(
+ textFragmentDataAfterAtomicContent
+ .InvisibleLeadingWhiteSpaceRangeRef());
+ if (precedingInvisibleWhiteSpaces.StartRef().IsSet() &&
+ followingInvisibleWhiteSpaces.EndRef().IsSet()) {
+ return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(),
+ followingInvisibleWhiteSpaces.EndRef());
+ }
+ if (precedingInvisibleWhiteSpaces.StartRef().IsSet()) {
+ return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(),
+ EditorDOMPoint::After(aAtomicContent));
+ }
+ if (followingInvisibleWhiteSpaces.EndRef().IsSet()) {
+ return EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ followingInvisibleWhiteSpaces.EndRef());
+ }
+ return EditorDOMRange(
+ EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
+ EditorDOMPoint::After(aAtomicContent));
+}
+
+// static
+EditorDOMRange WSRunScanner::GetRangeForDeletingBlockElementBoundaries(
+ const HTMLEditor& aHTMLEditor, const Element& aLeftBlockElement,
+ const Element& aRightBlockElement,
+ const EditorDOMPoint& aPointContainingTheOtherBlock) {
+ MOZ_ASSERT(&aLeftBlockElement != &aRightBlockElement);
+ MOZ_ASSERT_IF(
+ aPointContainingTheOtherBlock.IsSet(),
+ aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement ||
+ aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement);
+ MOZ_ASSERT_IF(
+ aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement,
+ aRightBlockElement.IsInclusiveDescendantOf(
+ aPointContainingTheOtherBlock.GetChild()));
+ MOZ_ASSERT_IF(
+ aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement,
+ aLeftBlockElement.IsInclusiveDescendantOf(
+ aPointContainingTheOtherBlock.GetChild()));
+ MOZ_ASSERT_IF(
+ !aPointContainingTheOtherBlock.IsSet(),
+ !aRightBlockElement.IsInclusiveDescendantOf(&aLeftBlockElement));
+ MOZ_ASSERT_IF(
+ !aPointContainingTheOtherBlock.IsSet(),
+ !aLeftBlockElement.IsInclusiveDescendantOf(&aRightBlockElement));
+ MOZ_ASSERT_IF(!aPointContainingTheOtherBlock.IsSet(),
+ EditorRawDOMPoint(const_cast<Element*>(&aLeftBlockElement))
+ .IsBefore(EditorRawDOMPoint(
+ const_cast<Element*>(&aRightBlockElement))));
+
+ const Element* editingHost = aHTMLEditor.ComputeEditingHost();
+
+ EditorDOMRange range;
+ // Include trailing invisible white-spaces in aLeftBlockElement.
+ TextFragmentData textFragmentDataAtEndOfLeftBlockElement(
+ aPointContainingTheOtherBlock.GetContainer() == &aLeftBlockElement
+ ? aPointContainingTheOtherBlock
+ : EditorDOMPoint::AtEndOf(const_cast<Element&>(aLeftBlockElement)),
+ editingHost, BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (NS_WARN_IF(!textFragmentDataAtEndOfLeftBlockElement.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ if (textFragmentDataAtEndOfLeftBlockElement.StartsFromInvisibleBRElement()) {
+ // If the left block element ends with an invisible `<br>` element,
+ // it'll be deleted (and it means there is no invisible trailing
+ // white-spaces). Therefore, the range should start from the invisible
+ // `<br>` element.
+ range.SetStart(EditorDOMPoint(
+ textFragmentDataAtEndOfLeftBlockElement.StartReasonBRElementPtr()));
+ } else {
+ const EditorDOMRange& trailingWhiteSpaceRange =
+ textFragmentDataAtEndOfLeftBlockElement
+ .InvisibleTrailingWhiteSpaceRangeRef();
+ if (trailingWhiteSpaceRange.StartRef().IsSet()) {
+ range.SetStart(trailingWhiteSpaceRange.StartRef());
+ } else {
+ range.SetStart(textFragmentDataAtEndOfLeftBlockElement.ScanStartRef());
+ }
+ }
+ // Include leading invisible white-spaces in aRightBlockElement.
+ TextFragmentData textFragmentDataAtStartOfRightBlockElement(
+ aPointContainingTheOtherBlock.GetContainer() == &aRightBlockElement &&
+ !aPointContainingTheOtherBlock.IsEndOfContainer()
+ ? aPointContainingTheOtherBlock.NextPoint()
+ : EditorDOMPoint(const_cast<Element*>(&aRightBlockElement), 0u),
+ editingHost, BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (NS_WARN_IF(!textFragmentDataAtStartOfRightBlockElement.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRange& leadingWhiteSpaceRange =
+ textFragmentDataAtStartOfRightBlockElement
+ .InvisibleLeadingWhiteSpaceRangeRef();
+ if (leadingWhiteSpaceRange.EndRef().IsSet()) {
+ range.SetEnd(leadingWhiteSpaceRange.EndRef());
+ } else {
+ range.SetEnd(textFragmentDataAtStartOfRightBlockElement.ScanStartRef());
+ }
+ return range;
+}
+
+// static
+EditorDOMRange
+WSRunScanner::GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries(
+ Element* aEditingHost, const EditorDOMRange& aRange) {
+ MOZ_ASSERT(aRange.IsPositionedAndValid());
+ MOZ_ASSERT(aRange.EndRef().IsSetAndValid());
+ MOZ_ASSERT(aRange.StartRef().IsSetAndValid());
+
+ EditorDOMRange result;
+ TextFragmentData textFragmentDataAtStart(
+ aRange.StartRef(), aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtStart =
+ textFragmentDataAtStart.GetNonCollapsedRangeInTexts(
+ textFragmentDataAtStart.InvisibleLeadingWhiteSpaceRangeRef());
+ if (invisibleLeadingWhiteSpacesAtStart.IsPositioned() &&
+ !invisibleLeadingWhiteSpacesAtStart.Collapsed()) {
+ result.SetStart(invisibleLeadingWhiteSpacesAtStart.StartRef());
+ } else {
+ const EditorDOMRangeInTexts invisibleTrailingWhiteSpacesAtStart =
+ textFragmentDataAtStart.GetNonCollapsedRangeInTexts(
+ textFragmentDataAtStart.InvisibleTrailingWhiteSpaceRangeRef());
+ if (invisibleTrailingWhiteSpacesAtStart.IsPositioned() &&
+ !invisibleTrailingWhiteSpacesAtStart.Collapsed()) {
+ MOZ_ASSERT(
+ invisibleTrailingWhiteSpacesAtStart.StartRef().EqualsOrIsBefore(
+ aRange.StartRef()));
+ result.SetStart(invisibleTrailingWhiteSpacesAtStart.StartRef());
+ }
+ // If there is no invisible white-space and the line starts with a
+ // text node, shrink the range to start of the text node.
+ else if (!aRange.StartRef().IsInTextNode() &&
+ textFragmentDataAtStart.StartsFromBlockBoundary() &&
+ textFragmentDataAtStart.EndRef().IsInTextNode()) {
+ result.SetStart(textFragmentDataAtStart.EndRef());
+ }
+ }
+ if (!result.StartRef().IsSet()) {
+ result.SetStart(aRange.StartRef());
+ }
+
+ TextFragmentData textFragmentDataAtEnd(
+ aRange.EndRef(), aEditingHost, BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return EditorDOMRange(); // TODO: Make here return error with Err.
+ }
+ const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtEnd =
+ textFragmentDataAtEnd.GetNonCollapsedRangeInTexts(
+ textFragmentDataAtEnd.InvisibleTrailingWhiteSpaceRangeRef());
+ if (invisibleLeadingWhiteSpacesAtEnd.IsPositioned() &&
+ !invisibleLeadingWhiteSpacesAtEnd.Collapsed()) {
+ result.SetEnd(invisibleLeadingWhiteSpacesAtEnd.EndRef());
+ } else {
+ const EditorDOMRangeInTexts invisibleLeadingWhiteSpacesAtEnd =
+ textFragmentDataAtEnd.GetNonCollapsedRangeInTexts(
+ textFragmentDataAtEnd.InvisibleLeadingWhiteSpaceRangeRef());
+ if (invisibleLeadingWhiteSpacesAtEnd.IsPositioned() &&
+ !invisibleLeadingWhiteSpacesAtEnd.Collapsed()) {
+ MOZ_ASSERT(aRange.EndRef().EqualsOrIsBefore(
+ invisibleLeadingWhiteSpacesAtEnd.EndRef()));
+ result.SetEnd(invisibleLeadingWhiteSpacesAtEnd.EndRef());
+ }
+ // If there is no invisible white-space and the line ends with a text
+ // node, shrink the range to end of the text node.
+ else if (!aRange.EndRef().IsInTextNode() &&
+ textFragmentDataAtEnd.EndsByBlockBoundary() &&
+ textFragmentDataAtEnd.StartRef().IsInTextNode()) {
+ result.SetEnd(EditorDOMPoint::AtEndOf(
+ *textFragmentDataAtEnd.StartRef().ContainerAs<Text>()));
+ }
+ }
+ if (!result.EndRef().IsSet()) {
+ result.SetEnd(aRange.EndRef());
+ }
+ MOZ_ASSERT(result.IsPositionedAndValid());
+ return result;
+}
+
+/******************************************************************************
+ * Utilities for other things.
+ ******************************************************************************/
+
+// static
+Result<bool, nsresult>
+WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
+ const HTMLEditor& aHTMLEditor, nsRange& aRange,
+ const Element* aEditingHost) {
+ MOZ_ASSERT(aRange.IsPositioned());
+ MOZ_ASSERT(!aRange.IsInAnySelection(),
+ "Changing range in selection may cause running script");
+
+ if (NS_WARN_IF(!aRange.GetStartContainer()) ||
+ NS_WARN_IF(!aRange.GetEndContainer())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (!aRange.GetStartContainer()->IsContent() ||
+ !aRange.GetEndContainer()->IsContent()) {
+ return false;
+ }
+
+ // If the range crosses a block boundary, we should do nothing for now
+ // because it hits a bug of inserting a padding `<br>` element after
+ // joining the blocks.
+ if (HTMLEditUtils::GetInclusiveAncestorElement(
+ *aRange.GetStartContainer()->AsContent(),
+ HTMLEditUtils::ClosestEditableBlockElementExceptHRElement,
+ BlockInlineCheck::UseComputedDisplayStyle) !=
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *aRange.GetEndContainer()->AsContent(),
+ HTMLEditUtils::ClosestEditableBlockElementExceptHRElement,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ return false;
+ }
+
+ nsIContent* startContent = nullptr;
+ if (aRange.GetStartContainer() && aRange.GetStartContainer()->IsText() &&
+ aRange.GetStartContainer()->AsText()->Length() == aRange.StartOffset()) {
+ // If next content is a visible `<br>` element, special inline content
+ // (e.g., `<img>`, non-editable text node, etc) or a block level void
+ // element like `<hr>`, the range should start with it.
+ TextFragmentData textFragmentDataAtStart(
+ EditorRawDOMPoint(aRange.StartRef()), aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (textFragmentDataAtStart.EndsByVisibleBRElement()) {
+ startContent = textFragmentDataAtStart.EndReasonBRElementPtr();
+ } else if (textFragmentDataAtStart.EndsBySpecialContent() ||
+ (textFragmentDataAtStart.EndsByOtherBlockElement() &&
+ !HTMLEditUtils::IsContainerNode(
+ *textFragmentDataAtStart
+ .EndReasonOtherBlockElementPtr()))) {
+ startContent = textFragmentDataAtStart.GetEndReasonContent();
+ }
+ }
+
+ nsIContent* endContent = nullptr;
+ if (aRange.GetEndContainer() && aRange.GetEndContainer()->IsText() &&
+ !aRange.EndOffset()) {
+ // If previous content is a visible `<br>` element, special inline content
+ // (e.g., `<img>`, non-editable text node, etc) or a block level void
+ // element like `<hr>`, the range should end after it.
+ TextFragmentData textFragmentDataAtEnd(
+ EditorRawDOMPoint(aRange.EndRef()), aEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (NS_WARN_IF(!textFragmentDataAtEnd.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (textFragmentDataAtEnd.StartsFromVisibleBRElement()) {
+ endContent = textFragmentDataAtEnd.StartReasonBRElementPtr();
+ } else if (textFragmentDataAtEnd.StartsFromSpecialContent() ||
+ (textFragmentDataAtEnd.StartsFromOtherBlockElement() &&
+ !HTMLEditUtils::IsContainerNode(
+ *textFragmentDataAtEnd
+ .StartReasonOtherBlockElementPtr()))) {
+ endContent = textFragmentDataAtEnd.GetStartReasonContent();
+ }
+ }
+
+ if (!startContent && !endContent) {
+ return false;
+ }
+
+ nsresult rv = aRange.SetStartAndEnd(
+ startContent ? RangeBoundary(
+ startContent->GetParentNode(),
+ startContent->GetPreviousSibling()) // at startContent
+ : aRange.StartRef(),
+ endContent ? RangeBoundary(endContent->GetParentNode(),
+ endContent) // after endContent
+ : aRange.EndRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return Err(rv);
+ }
+ return true;
+}
+
+} // namespace mozilla
diff --git a/editor/libeditor/WSRunObject.h b/editor/libeditor/WSRunObject.h
new file mode 100644
index 0000000000..9328b24eb2
--- /dev/null
+++ b/editor/libeditor/WSRunObject.h
@@ -0,0 +1,1685 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WSRunObject_h
+#define WSRunObject_h
+
+#include "EditAction.h"
+#include "EditorBase.h"
+#include "EditorForwards.h"
+#include "EditorDOMPoint.h" // for EditorDOMPoint
+#include "EditorUtils.h" // for CaretPoint
+#include "HTMLEditHelpers.h"
+#include "HTMLEditor.h"
+#include "HTMLEditUtils.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/Text.h"
+#include "nsCOMPtr.h"
+#include "nsIContent.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+/**
+ * WSScanResult is result of ScanNextVisibleNodeOrBlockBoundaryFrom(),
+ * ScanPreviousVisibleNodeOrBlockBoundaryFrom(), and their static wrapper
+ * methods. This will have information of found visible content (and its
+ * position) or reached block element or topmost editable content at the
+ * start of scanner.
+ */
+class MOZ_STACK_CLASS WSScanResult final {
+ private:
+ enum class WSType : uint8_t {
+ NotInitialized,
+ // Could be the DOM tree is broken as like crash tests.
+ UnexpectedError,
+ // The run is maybe collapsible white-spaces at start of a hard line.
+ LeadingWhiteSpaces,
+ // The run is maybe collapsible white-spaces at end of a hard line.
+ TrailingWhiteSpaces,
+ // Collapsible, but visible white-spaces.
+ CollapsibleWhiteSpaces,
+ // Visible characters except collapsible white-spaces.
+ NonCollapsibleCharacters,
+ // Special content such as `<img>`, etc.
+ SpecialContent,
+ // <br> element.
+ BRElement,
+ // A linefeed which is preformatted.
+ PreformattedLineBreak,
+ // Other block's boundary (child block of current block, maybe).
+ OtherBlockBoundary,
+ // Current block's boundary.
+ CurrentBlockBoundary,
+ };
+
+ friend std::ostream& operator<<(std::ostream& aStream, const WSType& aType) {
+ switch (aType) {
+ case WSType::NotInitialized:
+ return aStream << "WSType::NotInitialized";
+ case WSType::UnexpectedError:
+ return aStream << "WSType::UnexpectedError";
+ case WSType::LeadingWhiteSpaces:
+ return aStream << "WSType::LeadingWhiteSpaces";
+ case WSType::TrailingWhiteSpaces:
+ return aStream << "WSType::TrailingWhiteSpaces";
+ case WSType::CollapsibleWhiteSpaces:
+ return aStream << "WSType::CollapsibleWhiteSpaces";
+ case WSType::NonCollapsibleCharacters:
+ return aStream << "WSType::NonCollapsibleCharacters";
+ case WSType::SpecialContent:
+ return aStream << "WSType::SpecialContent";
+ case WSType::BRElement:
+ return aStream << "WSType::BRElement";
+ case WSType::PreformattedLineBreak:
+ return aStream << "WSType::PreformattedLineBreak";
+ case WSType::OtherBlockBoundary:
+ return aStream << "WSType::OtherBlockBoundary";
+ case WSType::CurrentBlockBoundary:
+ return aStream << "WSType::CurrentBlockBoundary";
+ }
+ return aStream << "<Illegal value>";
+ }
+
+ friend class WSRunScanner; // Because of WSType.
+
+ public:
+ WSScanResult() = delete;
+ MOZ_NEVER_INLINE_DEBUG WSScanResult(nsIContent* aContent, WSType aReason,
+ BlockInlineCheck aBlockInlineCheck)
+ : mContent(aContent), mReason(aReason) {
+ AssertIfInvalidData(aBlockInlineCheck);
+ }
+ MOZ_NEVER_INLINE_DEBUG WSScanResult(const EditorDOMPoint& aPoint,
+ WSType aReason,
+ BlockInlineCheck aBlockInlineCheck)
+ : mContent(aPoint.GetContainerAs<nsIContent>()),
+ mOffset(Some(aPoint.Offset())),
+ mReason(aReason) {
+ AssertIfInvalidData(aBlockInlineCheck);
+ }
+
+ MOZ_NEVER_INLINE_DEBUG void AssertIfInvalidData(
+ BlockInlineCheck aBlockInlineCheck) const {
+#ifdef DEBUG
+ MOZ_ASSERT(mReason == WSType::UnexpectedError ||
+ mReason == WSType::NonCollapsibleCharacters ||
+ mReason == WSType::CollapsibleWhiteSpaces ||
+ mReason == WSType::BRElement ||
+ mReason == WSType::PreformattedLineBreak ||
+ mReason == WSType::SpecialContent ||
+ mReason == WSType::CurrentBlockBoundary ||
+ mReason == WSType::OtherBlockBoundary);
+ MOZ_ASSERT_IF(mReason == WSType::UnexpectedError, !mContent);
+ MOZ_ASSERT_IF(mReason == WSType::NonCollapsibleCharacters ||
+ mReason == WSType::CollapsibleWhiteSpaces,
+ mContent && mContent->IsText());
+ MOZ_ASSERT_IF(mReason == WSType::BRElement,
+ mContent && mContent->IsHTMLElement(nsGkAtoms::br));
+ MOZ_ASSERT_IF(mReason == WSType::PreformattedLineBreak,
+ mContent && mContent->IsText() &&
+ EditorUtils::IsNewLinePreformatted(*mContent));
+ MOZ_ASSERT_IF(
+ mReason == WSType::SpecialContent,
+ mContent &&
+ ((mContent->IsText() && !mContent->IsEditable()) ||
+ (!mContent->IsHTMLElement(nsGkAtoms::br) &&
+ !HTMLEditUtils::IsBlockElement(*mContent, aBlockInlineCheck))));
+ MOZ_ASSERT_IF(mReason == WSType::OtherBlockBoundary,
+ mContent && HTMLEditUtils::IsBlockElement(*mContent,
+ aBlockInlineCheck));
+ // If mReason is WSType::CurrentBlockBoundary, mContent can be any content.
+ // In most cases, it's current block element which is editable. However, if
+ // there is no editable block parent, this is topmost editable inline
+ // content. Additionally, if there is no editable content, this is the
+ // container start of scanner and is not editable.
+ if (mReason == WSType::CurrentBlockBoundary) {
+ if (!mContent ||
+ // Although not expected that scanning in orphan document fragment,
+ // it's okay.
+ !mContent->IsInComposedDoc() ||
+ // This is what the most preferred result is mContent itself is a
+ // block.
+ HTMLEditUtils::IsBlockElement(*mContent, aBlockInlineCheck) ||
+ // If mContent is not editable, we cannot check whether there is no
+ // block ancestor in the limiter which we don't have. Therefore,
+ // let's skip the ancestor check.
+ !mContent->IsEditable()) {
+ return;
+ }
+ const DebugOnly<Element*> closestAncestorEditableBlockElement =
+ HTMLEditUtils::GetAncestorElement(
+ *mContent, HTMLEditUtils::ClosestEditableBlockElement,
+ aBlockInlineCheck);
+ MOZ_ASSERT_IF(
+ mReason == WSType::CurrentBlockBoundary,
+ // There is no editable block ancestor, it's fine.
+ !closestAncestorEditableBlockElement ||
+ // If we found an editable block, but mContent can be inline if
+ // it's an editing host (root or its parent is not editable).
+ !closestAncestorEditableBlockElement->GetParentElement() ||
+ !closestAncestorEditableBlockElement->GetParentElement()
+ ->IsEditable());
+ }
+#endif // #ifdef DEBUG
+ }
+
+ bool Failed() const {
+ return mReason == WSType::NotInitialized ||
+ mReason == WSType::UnexpectedError;
+ }
+
+ /**
+ * GetContent() returns found visible and editable content/element.
+ * See MOZ_ASSERT_IF()s in AssertIfInvalidData() for the detail.
+ */
+ nsIContent* GetContent() const { return mContent; }
+
+ [[nodiscard]] bool ContentIsElement() const {
+ return mContent && mContent->IsElement();
+ }
+
+ /**
+ * The following accessors makes it easier to understand each callers.
+ */
+ MOZ_NEVER_INLINE_DEBUG Element* ElementPtr() const {
+ MOZ_DIAGNOSTIC_ASSERT(mContent->IsElement());
+ return mContent->AsElement();
+ }
+ MOZ_NEVER_INLINE_DEBUG HTMLBRElement* BRElementPtr() const {
+ MOZ_DIAGNOSTIC_ASSERT(mContent->IsHTMLElement(nsGkAtoms::br));
+ return static_cast<HTMLBRElement*>(mContent.get());
+ }
+ MOZ_NEVER_INLINE_DEBUG Text* TextPtr() const {
+ MOZ_DIAGNOSTIC_ASSERT(mContent->IsText());
+ return mContent->AsText();
+ }
+
+ /**
+ * Returns true if found or reached content is ediable.
+ */
+ bool IsContentEditable() const { return mContent && mContent->IsEditable(); }
+
+ /**
+ * Offset() returns meaningful value only when
+ * InVisibleOrCollapsibleCharacters() returns true or the scanner
+ * reached to start or end of its scanning range and that is same as start or
+ * end container which are specified when the scanner is initialized. If it's
+ * result of scanning backward, this offset means before the found point.
+ * Otherwise, i.e., scanning forward, this offset means after the found point.
+ */
+ MOZ_NEVER_INLINE_DEBUG uint32_t Offset() const {
+ NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful offset");
+ return mOffset.valueOr(0);
+ }
+
+ /**
+ * Point() and RawPoint() return the position in found visible node or
+ * reached block boundary. So, they return meaningful point only when
+ * Offset() returns meaningful value.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType Point() const {
+ NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful point");
+ return EditorDOMPointType(mContent, mOffset.valueOr(0));
+ }
+
+ /**
+ * PointAtContent() and RawPointAtContent() return the position of found
+ * visible content or reached block element.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType PointAtContent() const {
+ MOZ_ASSERT(mContent);
+ return EditorDOMPointType(mContent);
+ }
+
+ /**
+ * PointAfterContent() and RawPointAfterContent() retrun the position after
+ * found visible content or reached block element.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMPointType PointAfterContent() const {
+ MOZ_ASSERT(mContent);
+ return mContent ? EditorDOMPointType::After(mContent)
+ : EditorDOMPointType();
+ }
+
+ /**
+ * The scanner reached <img> or something which is inline and is not a
+ * container.
+ */
+ bool ReachedSpecialContent() const {
+ return mReason == WSType::SpecialContent;
+ }
+
+ /**
+ * The point is in visible characters or collapsible white-spaces.
+ */
+ bool InVisibleOrCollapsibleCharacters() const {
+ return mReason == WSType::CollapsibleWhiteSpaces ||
+ mReason == WSType::NonCollapsibleCharacters;
+ }
+
+ /**
+ * The point is in collapsible white-spaces.
+ */
+ bool InCollapsibleWhiteSpaces() const {
+ return mReason == WSType::CollapsibleWhiteSpaces;
+ }
+
+ /**
+ * The point is in visible non-collapsible characters.
+ */
+ bool InNonCollapsibleCharacters() const {
+ return mReason == WSType::NonCollapsibleCharacters;
+ }
+
+ /**
+ * The scanner reached a <br> element.
+ */
+ bool ReachedBRElement() const { return mReason == WSType::BRElement; }
+ bool ReachedVisibleBRElement() const {
+ return ReachedBRElement() &&
+ HTMLEditUtils::IsVisibleBRElement(*BRElementPtr());
+ }
+ bool ReachedInvisibleBRElement() const {
+ return ReachedBRElement() &&
+ HTMLEditUtils::IsInvisibleBRElement(*BRElementPtr());
+ }
+
+ bool ReachedPreformattedLineBreak() const {
+ return mReason == WSType::PreformattedLineBreak;
+ }
+
+ /**
+ * The scanner reached a <hr> element.
+ */
+ bool ReachedHRElement() const {
+ return mContent && mContent->IsHTMLElement(nsGkAtoms::hr);
+ }
+
+ /**
+ * The scanner reached current block boundary or other block element.
+ */
+ bool ReachedBlockBoundary() const {
+ return mReason == WSType::CurrentBlockBoundary ||
+ mReason == WSType::OtherBlockBoundary;
+ }
+
+ /**
+ * The scanner reached current block element boundary.
+ */
+ bool ReachedCurrentBlockBoundary() const {
+ return mReason == WSType::CurrentBlockBoundary;
+ }
+
+ /**
+ * The scanner reached other block element.
+ */
+ bool ReachedOtherBlockElement() const {
+ return mReason == WSType::OtherBlockBoundary;
+ }
+
+ /**
+ * The scanner reached other block element that isn't editable
+ */
+ bool ReachedNonEditableOtherBlockElement() const {
+ return ReachedOtherBlockElement() && !GetContent()->IsEditable();
+ }
+
+ /**
+ * The scanner reached something non-text node.
+ */
+ bool ReachedSomethingNonTextContent() const {
+ return !InVisibleOrCollapsibleCharacters();
+ }
+
+ private:
+ nsCOMPtr<nsIContent> mContent;
+ Maybe<uint32_t> mOffset;
+ WSType mReason;
+};
+
+class MOZ_STACK_CLASS WSRunScanner final {
+ public:
+ using WSType = WSScanResult::WSType;
+
+ template <typename EditorDOMPointType>
+ WSRunScanner(const Element* aEditingHost,
+ const EditorDOMPointType& aScanStartPoint,
+ BlockInlineCheck aBlockInlineCheck)
+ : mScanStartPoint(aScanStartPoint.template To<EditorDOMPoint>()),
+ mEditingHost(const_cast<Element*>(aEditingHost)),
+ mTextFragmentDataAtStart(mScanStartPoint, mEditingHost,
+ aBlockInlineCheck),
+ mBlockInlineCheck(aBlockInlineCheck) {}
+
+ // ScanNextVisibleNodeOrBlockBoundaryForwardFrom() returns the first visible
+ // node after aPoint. If there is no visible nodes after aPoint, returns
+ // topmost editable inline ancestor at end of current block. See comments
+ // around WSScanResult for the detail.
+ template <typename PT, typename CT>
+ WSScanResult ScanNextVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPointBase<PT, CT>& aPoint) const;
+ template <typename PT, typename CT>
+ static WSScanResult ScanNextVisibleNodeOrBlockBoundary(
+ const Element* aEditingHost, const EditorDOMPointBase<PT, CT>& aPoint,
+ BlockInlineCheck aBlockInlineCheck) {
+ return WSRunScanner(aEditingHost, aPoint, aBlockInlineCheck)
+ .ScanNextVisibleNodeOrBlockBoundaryFrom(aPoint);
+ }
+
+ // ScanPreviousVisibleNodeOrBlockBoundaryFrom() returns the first visible node
+ // before aPoint. If there is no visible nodes before aPoint, returns topmost
+ // editable inline ancestor at start of current block. See comments around
+ // WSScanResult for the detail.
+ template <typename PT, typename CT>
+ WSScanResult ScanPreviousVisibleNodeOrBlockBoundaryFrom(
+ const EditorDOMPointBase<PT, CT>& aPoint) const;
+ template <typename PT, typename CT>
+ static WSScanResult ScanPreviousVisibleNodeOrBlockBoundary(
+ const Element* aEditingHost, const EditorDOMPointBase<PT, CT>& aPoint,
+ BlockInlineCheck aBlockInlineCheck) {
+ return WSRunScanner(aEditingHost, aPoint, aBlockInlineCheck)
+ .ScanPreviousVisibleNodeOrBlockBoundaryFrom(aPoint);
+ }
+
+ /**
+ * GetInclusiveNextEditableCharPoint() returns a point in a text node which
+ * is at current editable character or next editable character if aPoint
+ * does not points an editable character.
+ */
+ template <typename EditorDOMPointType = EditorDOMPointInText, typename PT,
+ typename CT>
+ static EditorDOMPointType GetInclusiveNextEditableCharPoint(
+ Element* aEditingHost, const EditorDOMPointBase<PT, CT>& aPoint,
+ BlockInlineCheck aBlockInlineCheck) {
+ if (aPoint.IsInTextNode() && !aPoint.IsEndOfContainer() &&
+ HTMLEditUtils::IsSimplyEditableNode(
+ *aPoint.template ContainerAs<Text>())) {
+ return EditorDOMPointType(aPoint.template ContainerAs<Text>(),
+ aPoint.Offset());
+ }
+ return WSRunScanner(aEditingHost, aPoint, aBlockInlineCheck)
+ .GetInclusiveNextEditableCharPoint<EditorDOMPointType>(aPoint);
+ }
+
+ /**
+ * GetPreviousEditableCharPoint() returns a point in a text node which
+ * is at previous editable character.
+ */
+ template <typename EditorDOMPointType = EditorDOMPointInText, typename PT,
+ typename CT>
+ static EditorDOMPointType GetPreviousEditableCharPoint(
+ Element* aEditingHost, const EditorDOMPointBase<PT, CT>& aPoint,
+ BlockInlineCheck aBlockInlineCheck) {
+ if (aPoint.IsInTextNode() && !aPoint.IsStartOfContainer() &&
+ HTMLEditUtils::IsSimplyEditableNode(
+ *aPoint.template ContainerAs<Text>())) {
+ return EditorDOMPointType(aPoint.template ContainerAs<Text>(),
+ aPoint.Offset() - 1);
+ }
+ return WSRunScanner(aEditingHost, aPoint, aBlockInlineCheck)
+ .GetPreviousEditableCharPoint<EditorDOMPointType>(aPoint);
+ }
+
+ /**
+ * Scan aTextNode from end or start to find last or first visible things.
+ * I.e., this returns a point immediately before or after invisible
+ * white-spaces of aTextNode if aTextNode ends or begins with some invisible
+ * white-spaces.
+ * Note that the result may not be in different text node if aTextNode has
+ * only invisible white-spaces and there is previous or next text node.
+ */
+ template <typename EditorDOMPointType>
+ static EditorDOMPointType GetAfterLastVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+ template <typename EditorDOMPointType>
+ static EditorDOMPointType GetFirstVisiblePoint(
+ Text& aTextNode, const Element* aAncestorLimiter);
+
+ /**
+ * GetRangeInTextNodesToForwardDeleteFrom() returns the range to remove
+ * text when caret is at aPoint.
+ */
+ static Result<EditorDOMRangeInTexts, nsresult>
+ GetRangeInTextNodesToForwardDeleteFrom(const EditorDOMPoint& aPoint,
+ const Element& aEditingHost);
+
+ /**
+ * GetRangeInTextNodesToBackspaceFrom() returns the range to remove text
+ * when caret is at aPoint.
+ */
+ static Result<EditorDOMRangeInTexts, nsresult>
+ GetRangeInTextNodesToBackspaceFrom(const EditorDOMPoint& aPoint,
+ const Element& aEditingHost);
+
+ /**
+ * GetRangesForDeletingAtomicContent() returns the range to delete
+ * aAtomicContent. If it's followed by invisible white-spaces, they will
+ * be included into the range.
+ */
+ static EditorDOMRange GetRangesForDeletingAtomicContent(
+ Element* aEditingHost, const nsIContent& aAtomicContent);
+
+ /**
+ * GetRangeForDeleteBlockElementBoundaries() returns a range starting from end
+ * of aLeftBlockElement to start of aRightBlockElement and extend invisible
+ * white-spaces around them.
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aLeftBlockElement The block element which will be joined with
+ * aRightBlockElement.
+ * @param aRightBlockElement The block element which will be joined with
+ * aLeftBlockElement. This must be an element
+ * after aLeftBlockElement.
+ * @param aPointContainingTheOtherBlock
+ * When aRightBlockElement is an ancestor of
+ * aLeftBlockElement, this must be set and the
+ * container must be aRightBlockElement.
+ * When aLeftBlockElement is an ancestor of
+ * aRightBlockElement, this must be set and the
+ * container must be aLeftBlockElement.
+ * Otherwise, must not be set.
+ */
+ static EditorDOMRange GetRangeForDeletingBlockElementBoundaries(
+ const HTMLEditor& aHTMLEditor, const Element& aLeftBlockElement,
+ const Element& aRightBlockElement,
+ const EditorDOMPoint& aPointContainingTheOtherBlock);
+
+ /**
+ * ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() may shrink aRange if it
+ * starts and/or ends with an atomic content, but the range boundary
+ * is in adjacent text nodes. Returns true if this modifies the range.
+ */
+ static Result<bool, nsresult> ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
+ const HTMLEditor& aHTMLEditor, nsRange& aRange,
+ const Element* aEditingHost);
+
+ /**
+ * GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries() returns
+ * extended range if range boundaries of aRange are in invisible white-spaces.
+ */
+ static EditorDOMRange GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries(
+ Element* aEditingHost, const EditorDOMRange& aRange);
+
+ /**
+ * GetPrecedingBRElementUnlessVisibleContentFound() scans a `<br>` element
+ * backward, but stops scanning it if the scanner finds visible character
+ * or something. In other words, this method ignores only invisible
+ * white-spaces between `<br>` element and aPoint.
+ */
+ template <typename EditorDOMPointType>
+ MOZ_NEVER_INLINE_DEBUG static HTMLBRElement*
+ GetPrecedingBRElementUnlessVisibleContentFound(
+ Element* aEditingHost, const EditorDOMPointType& aPoint,
+ BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ // XXX This method behaves differently even in similar point.
+ // If aPoint is in a text node following `<br>` element, reaches the
+ // `<br>` element when all characters between the `<br>` and
+ // aPoint are ASCII whitespaces.
+ // But if aPoint is not in a text node, e.g., at start of an inline
+ // element which is immediately after a `<br>` element, returns the
+ // `<br>` element even if there is no invisible white-spaces.
+ if (aPoint.IsStartOfContainer()) {
+ return nullptr;
+ }
+ // TODO: Scan for end boundary is redundant in this case, we should optimize
+ // it.
+ TextFragmentData textFragmentData(aPoint, aEditingHost, aBlockInlineCheck);
+ return textFragmentData.StartsFromBRElement()
+ ? textFragmentData.StartReasonBRElementPtr()
+ : nullptr;
+ }
+
+ const EditorDOMPoint& ScanStartRef() const { return mScanStartPoint; }
+
+ /**
+ * GetStartReasonContent() and GetEndReasonContent() return a node which
+ * was found by scanning from mScanStartPoint backward or forward. If there
+ * was white-spaces or text from the point, returns the text node. Otherwise,
+ * returns an element which is explained by the following methods. Note that
+ * when the reason is WSType::CurrentBlockBoundary, In most cases, it's
+ * current block element which is editable, but also may be non-element and/or
+ * non-editable. See MOZ_ASSERT_IF()s in WSScanResult::AssertIfInvalidData()
+ * for the detail.
+ */
+ nsIContent* GetStartReasonContent() const {
+ return TextFragmentDataAtStartRef().GetStartReasonContent();
+ }
+ nsIContent* GetEndReasonContent() const {
+ return TextFragmentDataAtStartRef().GetEndReasonContent();
+ }
+
+ bool StartsFromNonCollapsibleCharacters() const {
+ return TextFragmentDataAtStartRef().StartsFromNonCollapsibleCharacters();
+ }
+ bool StartsFromSpecialContent() const {
+ return TextFragmentDataAtStartRef().StartsFromSpecialContent();
+ }
+ bool StartsFromBRElement() const {
+ return TextFragmentDataAtStartRef().StartsFromBRElement();
+ }
+ bool StartsFromVisibleBRElement() const {
+ return TextFragmentDataAtStartRef().StartsFromVisibleBRElement();
+ }
+ bool StartsFromInvisibleBRElement() const {
+ return TextFragmentDataAtStartRef().StartsFromInvisibleBRElement();
+ }
+ bool StartsFromPreformattedLineBreak() const {
+ return TextFragmentDataAtStartRef().StartsFromPreformattedLineBreak();
+ }
+ bool StartsFromCurrentBlockBoundary() const {
+ return TextFragmentDataAtStartRef().StartsFromCurrentBlockBoundary();
+ }
+ bool StartsFromOtherBlockElement() const {
+ return TextFragmentDataAtStartRef().StartsFromOtherBlockElement();
+ }
+ bool StartsFromBlockBoundary() const {
+ return TextFragmentDataAtStartRef().StartsFromBlockBoundary();
+ }
+ bool StartsFromHardLineBreak() const {
+ return TextFragmentDataAtStartRef().StartsFromHardLineBreak();
+ }
+ bool EndsByNonCollapsibleCharacters() const {
+ return TextFragmentDataAtStartRef().EndsByNonCollapsibleCharacters();
+ }
+ bool EndsBySpecialContent() const {
+ return TextFragmentDataAtStartRef().EndsBySpecialContent();
+ }
+ bool EndsByBRElement() const {
+ return TextFragmentDataAtStartRef().EndsByBRElement();
+ }
+ bool EndsByVisibleBRElement() const {
+ return TextFragmentDataAtStartRef().EndsByVisibleBRElement();
+ }
+ bool EndsByInvisibleBRElement() const {
+ return TextFragmentDataAtStartRef().EndsByInvisibleBRElement();
+ }
+ bool EndsByPreformattedLineBreak() const {
+ return TextFragmentDataAtStartRef().EndsByPreformattedLineBreak();
+ }
+ bool EndsByCurrentBlockBoundary() const {
+ return TextFragmentDataAtStartRef().EndsByCurrentBlockBoundary();
+ }
+ bool EndsByOtherBlockElement() const {
+ return TextFragmentDataAtStartRef().EndsByOtherBlockElement();
+ }
+ bool EndsByBlockBoundary() const {
+ return TextFragmentDataAtStartRef().EndsByBlockBoundary();
+ }
+
+ MOZ_NEVER_INLINE_DEBUG Element* StartReasonOtherBlockElementPtr() const {
+ return TextFragmentDataAtStartRef().StartReasonOtherBlockElementPtr();
+ }
+ MOZ_NEVER_INLINE_DEBUG HTMLBRElement* StartReasonBRElementPtr() const {
+ return TextFragmentDataAtStartRef().StartReasonBRElementPtr();
+ }
+ MOZ_NEVER_INLINE_DEBUG Element* EndReasonOtherBlockElementPtr() const {
+ return TextFragmentDataAtStartRef().EndReasonOtherBlockElementPtr();
+ }
+ MOZ_NEVER_INLINE_DEBUG HTMLBRElement* EndReasonBRElementPtr() const {
+ return TextFragmentDataAtStartRef().EndReasonBRElementPtr();
+ }
+
+ /**
+ * Active editing host when this instance is created.
+ */
+ Element* GetEditingHost() const { return mEditingHost; }
+
+ protected:
+ using EditorType = EditorBase::EditorType;
+
+ class TextFragmentData;
+
+ // VisibleWhiteSpacesData represents 0 or more visible white-spaces.
+ class MOZ_STACK_CLASS VisibleWhiteSpacesData final {
+ public:
+ bool IsInitialized() const {
+ return mLeftWSType != WSType::NotInitialized ||
+ mRightWSType != WSType::NotInitialized;
+ }
+
+ EditorDOMPoint StartRef() const { return mStartPoint; }
+ EditorDOMPoint EndRef() const { return mEndPoint; }
+
+ /**
+ * Information why the white-spaces start from (i.e., this indicates the
+ * previous content type of the fragment).
+ */
+ bool StartsFromNonCollapsibleCharacters() const {
+ return mLeftWSType == WSType::NonCollapsibleCharacters;
+ }
+ bool StartsFromSpecialContent() const {
+ return mLeftWSType == WSType::SpecialContent;
+ }
+ bool StartsFromPreformattedLineBreak() const {
+ return mLeftWSType == WSType::PreformattedLineBreak;
+ }
+
+ /**
+ * Information why the white-spaces end by (i.e., this indicates the
+ * next content type of the fragment).
+ */
+ bool EndsByNonCollapsibleCharacters() const {
+ return mRightWSType == WSType::NonCollapsibleCharacters;
+ }
+ bool EndsByTrailingWhiteSpaces() const {
+ return mRightWSType == WSType::TrailingWhiteSpaces;
+ }
+ bool EndsBySpecialContent() const {
+ return mRightWSType == WSType::SpecialContent;
+ }
+ bool EndsByBRElement() const { return mRightWSType == WSType::BRElement; }
+ bool EndsByPreformattedLineBreak() const {
+ return mRightWSType == WSType::PreformattedLineBreak;
+ }
+ bool EndsByBlockBoundary() const {
+ return mRightWSType == WSType::CurrentBlockBoundary ||
+ mRightWSType == WSType::OtherBlockBoundary;
+ }
+
+ /**
+ * ComparePoint() compares aPoint with the white-spaces.
+ */
+ enum class PointPosition {
+ BeforeStartOfFragment,
+ StartOfFragment,
+ MiddleOfFragment,
+ EndOfFragment,
+ AfterEndOfFragment,
+ NotInSameDOMTree,
+ };
+ template <typename EditorDOMPointType>
+ PointPosition ComparePoint(const EditorDOMPointType& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ if (StartRef() == aPoint) {
+ return PointPosition::StartOfFragment;
+ }
+ if (EndRef() == aPoint) {
+ return PointPosition::EndOfFragment;
+ }
+ const bool startIsBeforePoint = StartRef().IsBefore(aPoint);
+ const bool pointIsBeforeEnd = aPoint.IsBefore(EndRef());
+ if (startIsBeforePoint && pointIsBeforeEnd) {
+ return PointPosition::MiddleOfFragment;
+ }
+ if (startIsBeforePoint) {
+ return PointPosition::AfterEndOfFragment;
+ }
+ if (pointIsBeforeEnd) {
+ return PointPosition::BeforeStartOfFragment;
+ }
+ return PointPosition::NotInSameDOMTree;
+ }
+
+ private:
+ // Initializers should be accessible only from `TextFragmentData`.
+ friend class WSRunScanner::TextFragmentData;
+ VisibleWhiteSpacesData()
+ : mLeftWSType(WSType::NotInitialized),
+ mRightWSType(WSType::NotInitialized) {}
+
+ template <typename EditorDOMPointType>
+ void SetStartPoint(const EditorDOMPointType& aStartPoint) {
+ mStartPoint = aStartPoint;
+ }
+ template <typename EditorDOMPointType>
+ void SetEndPoint(const EditorDOMPointType& aEndPoint) {
+ mEndPoint = aEndPoint;
+ }
+ void SetStartFrom(WSType aLeftWSType) { mLeftWSType = aLeftWSType; }
+ void SetStartFromLeadingWhiteSpaces() {
+ mLeftWSType = WSType::LeadingWhiteSpaces;
+ }
+ void SetEndBy(WSType aRightWSType) { mRightWSType = aRightWSType; }
+ void SetEndByTrailingWhiteSpaces() {
+ mRightWSType = WSType::TrailingWhiteSpaces;
+ }
+
+ EditorDOMPoint mStartPoint;
+ EditorDOMPoint mEndPoint;
+ WSType mLeftWSType, mRightWSType;
+ };
+
+ using PointPosition = VisibleWhiteSpacesData::PointPosition;
+
+ /**
+ * GetInclusiveNextEditableCharPoint() returns aPoint if it points a character
+ * in an editable text node, or start of next editable text node otherwise.
+ * FYI: For the performance, this does not check whether given container
+ * is not after mStart.mReasonContent or not.
+ */
+ template <typename EditorDOMPointType = EditorDOMPointInText, typename PT,
+ typename CT>
+ EditorDOMPointType GetInclusiveNextEditableCharPoint(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ return TextFragmentDataAtStartRef()
+ .GetInclusiveNextEditableCharPoint<EditorDOMPointType>(aPoint);
+ }
+
+ /**
+ * GetPreviousEditableCharPoint() returns previous editable point in a
+ * text node. Note that this returns last character point when it meets
+ * non-empty text node, otherwise, returns a point in an empty text node.
+ * FYI: For the performance, this does not check whether given container
+ * is not before mEnd.mReasonContent or not.
+ */
+ template <typename EditorDOMPointType = EditorDOMPointInText, typename PT,
+ typename CT>
+ EditorDOMPointType GetPreviousEditableCharPoint(
+ const EditorDOMPointBase<PT, CT>& aPoint) const {
+ return TextFragmentDataAtStartRef()
+ .GetPreviousEditableCharPoint<EditorDOMPointType>(aPoint);
+ }
+
+ /**
+ * GetEndOfCollapsibleASCIIWhiteSpaces() returns the next visible char
+ * (meaning a character except ASCII white-spaces) point or end of last text
+ * node scanning from aPointAtASCIIWhiteSpace.
+ * Note that this may return different text node from the container of
+ * aPointAtASCIIWhiteSpace.
+ */
+ template <typename EditorDOMPointType = EditorDOMPointInText>
+ EditorDOMPointType GetEndOfCollapsibleASCIIWhiteSpaces(
+ const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
+ nsIEditor::EDirection aDirectionToDelete) const {
+ MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
+ aDirectionToDelete == nsIEditor::eNext ||
+ aDirectionToDelete == nsIEditor::ePrevious);
+ return TextFragmentDataAtStartRef()
+ .GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointType>(
+ aPointAtASCIIWhiteSpace, aDirectionToDelete);
+ }
+
+ /**
+ * GetFirstASCIIWhiteSpacePointCollapsedTo() returns the first ASCII
+ * white-space which aPointAtASCIIWhiteSpace belongs to. In other words,
+ * the white-space at aPointAtASCIIWhiteSpace should be collapsed into
+ * the result.
+ * Note that this may return different text node from the container of
+ * aPointAtASCIIWhiteSpace.
+ */
+ template <typename EditorDOMPointType = EditorDOMPointInText>
+ EditorDOMPointType GetFirstASCIIWhiteSpacePointCollapsedTo(
+ const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
+ nsIEditor::EDirection aDirectionToDelete) const {
+ MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone ||
+ aDirectionToDelete == nsIEditor::eNext ||
+ aDirectionToDelete == nsIEditor::ePrevious);
+ return TextFragmentDataAtStartRef()
+ .GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointType>(
+ aPointAtASCIIWhiteSpace, aDirectionToDelete);
+ }
+
+ EditorDOMPointInText GetPreviousCharPointFromPointInText(
+ const EditorDOMPointInText& aPoint) const;
+
+ char16_t GetCharAt(Text* aTextNode, uint32_t aOffset) const;
+
+ /**
+ * TextFragmentData stores the information of white-space sequence which
+ * contains `aPoint` of the constructor.
+ */
+ class MOZ_STACK_CLASS TextFragmentData final {
+ private:
+ class NoBreakingSpaceData;
+ class MOZ_STACK_CLASS BoundaryData final {
+ public:
+ using NoBreakingSpaceData =
+ WSRunScanner::TextFragmentData::NoBreakingSpaceData;
+
+ /**
+ * ScanCollapsibleWhiteSpaceStartFrom() returns start boundary data of
+ * white-spaces containing aPoint. When aPoint is in a text node and
+ * points a non-white-space character or the text node is preformatted,
+ * this returns the data at aPoint.
+ *
+ * @param aPoint Scan start point.
+ * @param aEditableBlockParentOrTopmostEditableInlineElement
+ * Nearest editable block parent element of
+ * aPoint if there is. Otherwise, inline editing
+ * host.
+ * @param aEditingHost Active editing host.
+ * @param aNBSPData Optional. If set, this recodes first and last
+ * NBSP positions.
+ */
+ template <typename EditorDOMPointType>
+ static BoundaryData ScanCollapsibleWhiteSpaceStartFrom(
+ const EditorDOMPointType& aPoint,
+ const Element& aEditableBlockParentOrTopmostEditableInlineElement,
+ const Element* aEditingHost, NoBreakingSpaceData* aNBSPData,
+ BlockInlineCheck aBlockInlineCheck);
+
+ /**
+ * ScanCollapsibleWhiteSpaceEndFrom() returns end boundary data of
+ * white-spaces containing aPoint. When aPoint is in a text node and
+ * points a non-white-space character or the text node is preformatted,
+ * this returns the data at aPoint.
+ *
+ * @param aPoint Scan start point.
+ * @param aEditableBlockParentOrTopmostEditableInlineElement
+ * Nearest editable block parent element of
+ * aPoint if there is. Otherwise, inline editing
+ * host.
+ * @param aEditingHost Active editing host.
+ * @param aNBSPData Optional. If set, this recodes first and last
+ * NBSP positions.
+ */
+ template <typename EditorDOMPointType>
+ static BoundaryData ScanCollapsibleWhiteSpaceEndFrom(
+ const EditorDOMPointType& aPoint,
+ const Element& aEditableBlockParentOrTopmostEditableInlineElement,
+ const Element* aEditingHost, NoBreakingSpaceData* aNBSPData,
+ BlockInlineCheck aBlockInlineCheck);
+
+ BoundaryData() = default;
+ template <typename EditorDOMPointType>
+ BoundaryData(const EditorDOMPointType& aPoint, nsIContent& aReasonContent,
+ WSType aReason)
+ : mReasonContent(&aReasonContent),
+ mPoint(aPoint.template To<EditorDOMPoint>()),
+ mReason(aReason) {}
+ bool Initialized() const { return mReasonContent && mPoint.IsSet(); }
+
+ nsIContent* GetReasonContent() const { return mReasonContent; }
+ const EditorDOMPoint& PointRef() const { return mPoint; }
+ WSType RawReason() const { return mReason; }
+
+ bool IsNonCollapsibleCharacters() const {
+ return mReason == WSType::NonCollapsibleCharacters;
+ }
+ bool IsSpecialContent() const {
+ return mReason == WSType::SpecialContent;
+ }
+ bool IsBRElement() const { return mReason == WSType::BRElement; }
+ bool IsPreformattedLineBreak() const {
+ return mReason == WSType::PreformattedLineBreak;
+ }
+ bool IsCurrentBlockBoundary() const {
+ return mReason == WSType::CurrentBlockBoundary;
+ }
+ bool IsOtherBlockBoundary() const {
+ return mReason == WSType::OtherBlockBoundary;
+ }
+ bool IsBlockBoundary() const {
+ return mReason == WSType::CurrentBlockBoundary ||
+ mReason == WSType::OtherBlockBoundary;
+ }
+ bool IsHardLineBreak() const {
+ return mReason == WSType::CurrentBlockBoundary ||
+ mReason == WSType::OtherBlockBoundary ||
+ mReason == WSType::BRElement ||
+ mReason == WSType::PreformattedLineBreak;
+ }
+ MOZ_NEVER_INLINE_DEBUG Element* OtherBlockElementPtr() const {
+ MOZ_DIAGNOSTIC_ASSERT(mReasonContent->IsElement());
+ return mReasonContent->AsElement();
+ }
+ MOZ_NEVER_INLINE_DEBUG HTMLBRElement* BRElementPtr() const {
+ MOZ_DIAGNOSTIC_ASSERT(mReasonContent->IsHTMLElement(nsGkAtoms::br));
+ return static_cast<HTMLBRElement*>(mReasonContent.get());
+ }
+
+ private:
+ /**
+ * Helper methods of ScanCollapsibleWhiteSpaceStartFrom() and
+ * ScanCollapsibleWhiteSpaceEndFrom() when they need to scan in a text
+ * node.
+ */
+ template <typename EditorDOMPointType>
+ static Maybe<BoundaryData> ScanCollapsibleWhiteSpaceStartInTextNode(
+ const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData,
+ BlockInlineCheck aBlockInlineCheck);
+ template <typename EditorDOMPointType>
+ static Maybe<BoundaryData> ScanCollapsibleWhiteSpaceEndInTextNode(
+ const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData,
+ BlockInlineCheck aBlockInlineCheck);
+
+ nsCOMPtr<nsIContent> mReasonContent;
+ EditorDOMPoint mPoint;
+ // Must be one of WSType::NotInitialized,
+ // WSType::NonCollapsibleCharacters, WSType::SpecialContent,
+ // WSType::BRElement, WSType::CurrentBlockBoundary or
+ // WSType::OtherBlockBoundary.
+ WSType mReason = WSType::NotInitialized;
+ };
+
+ class MOZ_STACK_CLASS NoBreakingSpaceData final {
+ public:
+ enum class Scanning { Forward, Backward };
+ void NotifyNBSP(const EditorDOMPointInText& aPoint,
+ Scanning aScanningDirection) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ MOZ_ASSERT(aPoint.IsCharNBSP());
+ if (!mFirst.IsSet() || aScanningDirection == Scanning::Backward) {
+ mFirst = aPoint;
+ }
+ if (!mLast.IsSet() || aScanningDirection == Scanning::Forward) {
+ mLast = aPoint;
+ }
+ }
+
+ const EditorDOMPointInText& FirstPointRef() const { return mFirst; }
+ const EditorDOMPointInText& LastPointRef() const { return mLast; }
+
+ bool FoundNBSP() const {
+ MOZ_ASSERT(mFirst.IsSet() == mLast.IsSet());
+ return mFirst.IsSet();
+ }
+
+ private:
+ EditorDOMPointInText mFirst;
+ EditorDOMPointInText mLast;
+ };
+
+ public:
+ TextFragmentData() = delete;
+ template <typename EditorDOMPointType>
+ TextFragmentData(const WSRunScanner& aWSRunScanner,
+ const EditorDOMPointType& aPoint)
+ : TextFragmentData(aPoint, aWSRunScanner.mEditingHost,
+ aWSRunScanner.mBlockInlineCheck) {}
+ template <typename EditorDOMPointType>
+ TextFragmentData(const EditorDOMPointType& aPoint,
+ const Element* aEditingHost,
+ BlockInlineCheck aBlockInlineCheck);
+
+ bool IsInitialized() const {
+ return mStart.Initialized() && mEnd.Initialized();
+ }
+
+ nsIContent* GetStartReasonContent() const {
+ return mStart.GetReasonContent();
+ }
+ nsIContent* GetEndReasonContent() const { return mEnd.GetReasonContent(); }
+
+ bool StartsFromNonCollapsibleCharacters() const {
+ return mStart.IsNonCollapsibleCharacters();
+ }
+ bool StartsFromSpecialContent() const { return mStart.IsSpecialContent(); }
+ bool StartsFromBRElement() const { return mStart.IsBRElement(); }
+ bool StartsFromVisibleBRElement() const {
+ return StartsFromBRElement() &&
+ HTMLEditUtils::IsVisibleBRElement(*GetStartReasonContent());
+ }
+ bool StartsFromInvisibleBRElement() const {
+ return StartsFromBRElement() &&
+ HTMLEditUtils::IsInvisibleBRElement(*GetStartReasonContent());
+ }
+ bool StartsFromPreformattedLineBreak() const {
+ return mStart.IsPreformattedLineBreak();
+ }
+ bool StartsFromCurrentBlockBoundary() const {
+ return mStart.IsCurrentBlockBoundary();
+ }
+ bool StartsFromOtherBlockElement() const {
+ return mStart.IsOtherBlockBoundary();
+ }
+ bool StartsFromBlockBoundary() const { return mStart.IsBlockBoundary(); }
+ bool StartsFromHardLineBreak() const { return mStart.IsHardLineBreak(); }
+ bool EndsByNonCollapsibleCharacters() const {
+ return mEnd.IsNonCollapsibleCharacters();
+ }
+ bool EndsBySpecialContent() const { return mEnd.IsSpecialContent(); }
+ bool EndsByBRElement() const { return mEnd.IsBRElement(); }
+ bool EndsByVisibleBRElement() const {
+ return EndsByBRElement() &&
+ HTMLEditUtils::IsVisibleBRElement(*GetEndReasonContent());
+ }
+ bool EndsByInvisibleBRElement() const {
+ return EndsByBRElement() &&
+ HTMLEditUtils::IsInvisibleBRElement(*GetEndReasonContent());
+ }
+ bool EndsByPreformattedLineBreak() const {
+ return mEnd.IsPreformattedLineBreak();
+ }
+ bool EndsByInvisiblePreformattedLineBreak() const {
+ return mEnd.IsPreformattedLineBreak() &&
+ HTMLEditUtils::IsInvisiblePreformattedNewLine(mEnd.PointRef());
+ }
+ bool EndsByCurrentBlockBoundary() const {
+ return mEnd.IsCurrentBlockBoundary();
+ }
+ bool EndsByOtherBlockElement() const { return mEnd.IsOtherBlockBoundary(); }
+ bool EndsByBlockBoundary() const { return mEnd.IsBlockBoundary(); }
+
+ WSType StartRawReason() const { return mStart.RawReason(); }
+ WSType EndRawReason() const { return mEnd.RawReason(); }
+
+ MOZ_NEVER_INLINE_DEBUG Element* StartReasonOtherBlockElementPtr() const {
+ return mStart.OtherBlockElementPtr();
+ }
+ MOZ_NEVER_INLINE_DEBUG HTMLBRElement* StartReasonBRElementPtr() const {
+ return mStart.BRElementPtr();
+ }
+ MOZ_NEVER_INLINE_DEBUG Element* EndReasonOtherBlockElementPtr() const {
+ return mEnd.OtherBlockElementPtr();
+ }
+ MOZ_NEVER_INLINE_DEBUG HTMLBRElement* EndReasonBRElementPtr() const {
+ return mEnd.BRElementPtr();
+ }
+
+ const EditorDOMPoint& StartRef() const { return mStart.PointRef(); }
+ const EditorDOMPoint& EndRef() const { return mEnd.PointRef(); }
+
+ const EditorDOMPoint& ScanStartRef() const { return mScanStartPoint; }
+
+ bool FoundNoBreakingWhiteSpaces() const { return mNBSPData.FoundNBSP(); }
+ const EditorDOMPointInText& FirstNBSPPointRef() const {
+ return mNBSPData.FirstPointRef();
+ }
+ const EditorDOMPointInText& LastNBSPPointRef() const {
+ return mNBSPData.LastPointRef();
+ }
+
+ template <typename EditorDOMPointType = EditorDOMPointInText, typename PT,
+ typename CT>
+ EditorDOMPointType GetInclusiveNextEditableCharPoint(
+ const EditorDOMPointBase<PT, CT>& aPoint) const;
+ template <typename EditorDOMPointType = EditorDOMPointInText, typename PT,
+ typename CT>
+ EditorDOMPointType GetPreviousEditableCharPoint(
+ const EditorDOMPointBase<PT, CT>& aPoint) const;
+
+ template <typename EditorDOMPointType = EditorDOMPointInText>
+ EditorDOMPointType GetEndOfCollapsibleASCIIWhiteSpaces(
+ const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
+ nsIEditor::EDirection aDirectionToDelete) const;
+ template <typename EditorDOMPointType = EditorDOMPointInText>
+ EditorDOMPointType GetFirstASCIIWhiteSpacePointCollapsedTo(
+ const EditorDOMPointInText& aPointAtASCIIWhiteSpace,
+ nsIEditor::EDirection aDirectionToDelete) const;
+
+ /**
+ * GetNonCollapsedRangeInTexts() returns non-empty range in texts which
+ * is the largest range in aRange if there is some text nodes.
+ */
+ EditorDOMRangeInTexts GetNonCollapsedRangeInTexts(
+ const EditorDOMRange& aRange) const;
+
+ /**
+ * InvisibleLeadingWhiteSpaceRangeRef() retruns reference to two DOM points,
+ * start of the line and first visible point or end of the hard line. When
+ * this returns non-positioned range or positioned but collapsed range,
+ * there is no invisible leading white-spaces.
+ * Note that if there are only invisible white-spaces in a hard line,
+ * this returns all of the white-spaces.
+ */
+ const EditorDOMRange& InvisibleLeadingWhiteSpaceRangeRef() const;
+
+ /**
+ * InvisibleTrailingWhiteSpaceRangeRef() returns reference to two DOM
+ * points, first invisible white-space and end of the hard line. When this
+ * returns non-positioned range or positioned but collapsed range,
+ * there is no invisible trailing white-spaces.
+ * Note that if there are only invisible white-spaces in a hard line,
+ * this returns all of the white-spaces.
+ */
+ const EditorDOMRange& InvisibleTrailingWhiteSpaceRangeRef() const;
+
+ /**
+ * GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt() returns new
+ * invisible leading white-space range which should be removed if
+ * splitting invisible white-space sequence at aPointToSplit creates
+ * new invisible leading white-spaces in the new line.
+ * Note that the result may be collapsed range if the point is around
+ * invisible white-spaces.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMRange GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt(
+ const EditorDOMPointType& aPointToSplit) const {
+ // If there are invisible trailing white-spaces and some or all of them
+ // become invisible leading white-spaces in the new line, although we
+ // don't need to delete them, but for aesthetically and backward
+ // compatibility, we should remove them.
+ const EditorDOMRange& trailingWhiteSpaceRange =
+ InvisibleTrailingWhiteSpaceRangeRef();
+ // XXX Why don't we check leading white-spaces too?
+ if (!trailingWhiteSpaceRange.IsPositioned()) {
+ return trailingWhiteSpaceRange;
+ }
+ // If the point is before the trailing white-spaces, the new line won't
+ // start with leading white-spaces.
+ if (aPointToSplit.IsBefore(trailingWhiteSpaceRange.StartRef())) {
+ return EditorDOMRange();
+ }
+ // If the point is in the trailing white-spaces, the new line may
+ // start with some leading white-spaces. Returning collapsed range
+ // is intentional because the caller may want to know whether the
+ // point is in trailing white-spaces or not.
+ if (aPointToSplit.EqualsOrIsBefore(trailingWhiteSpaceRange.EndRef())) {
+ return EditorDOMRange(trailingWhiteSpaceRange.StartRef(),
+ aPointToSplit);
+ }
+ // Otherwise, if the point is after the trailing white-spaces, it may
+ // be just outside of the text node. E.g., end of parent element.
+ // This is possible case but the validation cost is not worthwhile
+ // due to the runtime cost in the worst case. Therefore, we should just
+ // return collapsed range at the end of trailing white-spaces. Then,
+ // callers can know the point is immediately after the trailing
+ // white-spaces.
+ return EditorDOMRange(trailingWhiteSpaceRange.EndRef());
+ }
+
+ /**
+ * GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt() returns new
+ * invisible trailing white-space range which should be removed if
+ * splitting invisible white-space sequence at aPointToSplit creates
+ * new invisible trailing white-spaces in the new line.
+ * Note that the result may be collapsed range if the point is around
+ * invisible white-spaces.
+ */
+ template <typename EditorDOMPointType>
+ EditorDOMRange GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt(
+ const EditorDOMPointType& aPointToSplit) const {
+ // If there are invisible leading white-spaces and some or all of them
+ // become end of current line, they will become visible. Therefore, we
+ // need to delete the invisible leading white-spaces before insertion
+ // point.
+ const EditorDOMRange& leadingWhiteSpaceRange =
+ InvisibleLeadingWhiteSpaceRangeRef();
+ if (!leadingWhiteSpaceRange.IsPositioned()) {
+ return leadingWhiteSpaceRange;
+ }
+ // If the point equals or is after the leading white-spaces, the line
+ // will end without trailing white-spaces.
+ if (leadingWhiteSpaceRange.EndRef().IsBefore(aPointToSplit)) {
+ return EditorDOMRange();
+ }
+ // If the point is in the leading white-spaces, the line may
+ // end with some trailing white-spaces. Returning collapsed range
+ // is intentional because the caller may want to know whether the
+ // point is in leading white-spaces or not.
+ if (leadingWhiteSpaceRange.StartRef().EqualsOrIsBefore(aPointToSplit)) {
+ return EditorDOMRange(aPointToSplit, leadingWhiteSpaceRange.EndRef());
+ }
+ // Otherwise, if the point is before the leading white-spaces, it may
+ // be just outside of the text node. E.g., start of parent element.
+ // This is possible case but the validation cost is not worthwhile
+ // due to the runtime cost in the worst case. Therefore, we should
+ // just return collapsed range at start of the leading white-spaces.
+ // Then, callers can know the point is immediately before the leading
+ // white-spaces.
+ return EditorDOMRange(leadingWhiteSpaceRange.StartRef());
+ }
+
+ /**
+ * FollowingContentMayBecomeFirstVisibleContent() returns true if some
+ * content may be first visible content after removing content after aPoint.
+ * Note that it's completely broken what this does. Don't use this method
+ * with new code.
+ */
+ template <typename EditorDOMPointType>
+ bool FollowingContentMayBecomeFirstVisibleContent(
+ const EditorDOMPointType& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ if (!mStart.IsHardLineBreak()) {
+ return false;
+ }
+ // If the point is before start of text fragment, that means that the
+ // point may be at the block boundary or inline element boundary.
+ if (aPoint.EqualsOrIsBefore(mStart.PointRef())) {
+ return true;
+ }
+ // VisibleWhiteSpacesData is marked as start of line only when it
+ // represents leading white-spaces.
+ const EditorDOMRange& leadingWhiteSpaceRange =
+ InvisibleLeadingWhiteSpaceRangeRef();
+ if (!leadingWhiteSpaceRange.StartRef().IsSet()) {
+ return false;
+ }
+ if (aPoint.EqualsOrIsBefore(leadingWhiteSpaceRange.StartRef())) {
+ return true;
+ }
+ if (!leadingWhiteSpaceRange.EndRef().IsSet()) {
+ return false;
+ }
+ return aPoint.EqualsOrIsBefore(leadingWhiteSpaceRange.EndRef());
+ }
+
+ /**
+ * PrecedingContentMayBecomeInvisible() returns true if end of preceding
+ * content is collapsed (when ends with an ASCII white-space).
+ * Note that it's completely broken what this does. Don't use this method
+ * with new code.
+ */
+ template <typename EditorDOMPointType>
+ bool PrecedingContentMayBecomeInvisible(
+ const EditorDOMPointType& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+ // If this fragment is ends by block boundary, always the caller needs
+ // additional check.
+ if (mEnd.IsBlockBoundary()) {
+ return true;
+ }
+
+ // If the point is in visible white-spaces and ends with an ASCII
+ // white-space, it may be collapsed even if it won't be end of line.
+ const VisibleWhiteSpacesData& visibleWhiteSpaces =
+ VisibleWhiteSpacesDataRef();
+ if (!visibleWhiteSpaces.IsInitialized()) {
+ return false;
+ }
+ // XXX Odd case, but keep traditional behavior of `FindNearestRun()`.
+ if (!visibleWhiteSpaces.StartRef().IsSet()) {
+ return true;
+ }
+ if (!visibleWhiteSpaces.StartRef().EqualsOrIsBefore(aPoint)) {
+ return false;
+ }
+ // XXX Odd case, but keep traditional behavior of `FindNearestRun()`.
+ if (visibleWhiteSpaces.EndsByTrailingWhiteSpaces()) {
+ return true;
+ }
+ // XXX Must be a bug. This claims that the caller needs additional
+ // check even when there is no white-spaces.
+ if (visibleWhiteSpaces.StartRef() == visibleWhiteSpaces.EndRef()) {
+ return true;
+ }
+ return aPoint.IsBefore(visibleWhiteSpaces.EndRef());
+ }
+
+ /**
+ * GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace() may return an
+ * NBSP point which should be replaced with an ASCII white-space when we're
+ * inserting text into aPointToInsert. Note that this is a helper method for
+ * the traditional white-space normalizer. Don't use this with the new
+ * white-space normalizer.
+ * Must be called only when VisibleWhiteSpacesDataRef() returns initialized
+ * instance and previous character of aPointToInsert is in the range.
+ */
+ EditorDOMPointInText GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ const EditorDOMPoint& aPointToInsert) const;
+
+ /**
+ * GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace() may return
+ * an NBSP point which should be replaced with an ASCII white-space when
+ * the caller inserts text into aPointToInsert.
+ * Note that this is a helper method for the traditional white-space
+ * normalizer. Don't use this with the new white-space normalizer.
+ * Must be called only when VisibleWhiteSpacesDataRef() returns initialized
+ * instance, and inclusive next char of aPointToInsert is in the range.
+ */
+ EditorDOMPointInText
+ GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace(
+ const EditorDOMPoint& aPointToInsert) const;
+
+ /**
+ * GetReplaceRangeDataAtEndOfDeletionRange() and
+ * GetReplaceRangeDataAtStartOfDeletionRange() return delete range if
+ * end or start of deleting range splits invisible trailing/leading
+ * white-spaces and it may become visible, or return replace range if
+ * end or start of deleting range splits visible white-spaces and it
+ * causes some ASCII white-spaces become invisible unless replacing
+ * with an NBSP.
+ */
+ ReplaceRangeData GetReplaceRangeDataAtEndOfDeletionRange(
+ const TextFragmentData& aTextFragmentDataAtStartToDelete) const;
+ ReplaceRangeData GetReplaceRangeDataAtStartOfDeletionRange(
+ const TextFragmentData& aTextFragmentDataAtEndToDelete) const;
+
+ /**
+ * VisibleWhiteSpacesDataRef() returns reference to visible white-spaces
+ * data. That is zero or more white-spaces which are visible.
+ * Note that when there is no visible content, it's not initialized.
+ * Otherwise, even if there is no white-spaces, it's initialized and
+ * the range is collapsed in such case.
+ */
+ const VisibleWhiteSpacesData& VisibleWhiteSpacesDataRef() const;
+
+ private:
+ EditorDOMPoint mScanStartPoint;
+ BoundaryData mStart;
+ BoundaryData mEnd;
+ NoBreakingSpaceData mNBSPData;
+ RefPtr<const Element> mEditingHost;
+ mutable Maybe<EditorDOMRange> mLeadingWhiteSpaceRange;
+ mutable Maybe<EditorDOMRange> mTrailingWhiteSpaceRange;
+ mutable Maybe<VisibleWhiteSpacesData> mVisibleWhiteSpacesData;
+ BlockInlineCheck mBlockInlineCheck;
+ };
+
+ const TextFragmentData& TextFragmentDataAtStartRef() const {
+ return mTextFragmentDataAtStart;
+ }
+
+ // The node passed to our constructor.
+ EditorDOMPoint mScanStartPoint;
+ // Together, the above represent the point at which we are building up ws
+ // info.
+
+ // The editing host when the instance is created.
+ RefPtr<Element> mEditingHost;
+
+ private:
+ /**
+ * ComputeRangeInTextNodesContainingInvisibleWhiteSpaces() returns range
+ * containing invisible white-spaces if deleting between aStart and aEnd
+ * causes them become visible.
+ *
+ * @param aStart TextFragmentData at start of deleting range.
+ * This must be initialized with DOM point in a text node.
+ * @param aEnd TextFragmentData at end of deleting range.
+ * This must be initialized with DOM point in a text node.
+ */
+ static EditorDOMRangeInTexts
+ ComputeRangeInTextNodesContainingInvisibleWhiteSpaces(
+ const TextFragmentData& aStart, const TextFragmentData& aEnd);
+
+ TextFragmentData mTextFragmentDataAtStart;
+
+ const BlockInlineCheck mBlockInlineCheck;
+
+ friend class WhiteSpaceVisibilityKeeper;
+};
+
+/**
+ * WhiteSpaceVisibilityKeeper class helps `HTMLEditor` modifying the DOM tree
+ * with keeps white-space sequence visibility automatically. E.g., invisible
+ * leading/trailing white-spaces becomes visible, this class members delete
+ * them. E.g., when splitting visible-white-space sequence, this class may
+ * replace ASCII white-spaces at split edges with NBSPs.
+ */
+class WhiteSpaceVisibilityKeeper final {
+ private:
+ using AutoTransactionsConserveSelection =
+ EditorBase::AutoTransactionsConserveSelection;
+ using EditorType = EditorBase::EditorType;
+ using PointPosition = WSRunScanner::PointPosition;
+ using TextFragmentData = WSRunScanner::TextFragmentData;
+ using VisibleWhiteSpacesData = WSRunScanner::VisibleWhiteSpacesData;
+
+ public:
+ WhiteSpaceVisibilityKeeper() = delete;
+ explicit WhiteSpaceVisibilityKeeper(
+ const WhiteSpaceVisibilityKeeper& aOther) = delete;
+ WhiteSpaceVisibilityKeeper(WhiteSpaceVisibilityKeeper&& aOther) = delete;
+
+ /**
+ * Remove invisible leading white-spaces and trailing white-spaces if there
+ * are around aPoint.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
+ DeleteInvisibleASCIIWhiteSpaces(HTMLEditor& aHTMLEditor,
+ const EditorDOMPoint& aPoint);
+
+ /**
+ * Fix up white-spaces before aStartPoint and after aEndPoint in preparation
+ * for content to keep the white-spaces visibility after the range is deleted.
+ * Note that the nodes and offsets are adjusted in response to any dom changes
+ * we make while adjusting white-spaces.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
+ PrepareToDeleteRangeAndTrackPoints(HTMLEditor& aHTMLEditor,
+ EditorDOMPoint* aStartPoint,
+ EditorDOMPoint* aEndPoint,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aStartPoint->IsSetAndValid());
+ MOZ_ASSERT(aEndPoint->IsSetAndValid());
+ AutoTrackDOMPoint trackerStart(aHTMLEditor.RangeUpdaterRef(), aStartPoint);
+ AutoTrackDOMPoint trackerEnd(aHTMLEditor.RangeUpdaterRef(), aEndPoint);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRange(
+ aHTMLEditor, EditorDOMRange(*aStartPoint, *aEndPoint),
+ aEditingHost);
+ NS_WARNING_ASSERTION(
+ caretPointOrError.isOk(),
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRange() failed");
+ return caretPointOrError;
+ }
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
+ PrepareToDeleteRange(HTMLEditor& aHTMLEditor,
+ const EditorDOMPoint& aStartPoint,
+ const EditorDOMPoint& aEndPoint,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aStartPoint.IsSetAndValid());
+ MOZ_ASSERT(aEndPoint.IsSetAndValid());
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToDeleteRange(
+ aHTMLEditor, EditorDOMRange(aStartPoint, aEndPoint), aEditingHost);
+ NS_WARNING_ASSERTION(
+ caretPointOrError.isOk(),
+ "WhiteSpaceVisibilityKeeper::PrepareToDeleteRange() failed");
+ return caretPointOrError;
+ }
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
+ PrepareToDeleteRange(HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(aRange.IsPositionedAndValid());
+ Result<CaretPoint, nsresult> caretPointOrError =
+ WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
+ aHTMLEditor, aRange, aEditingHost);
+ NS_WARNING_ASSERTION(
+ caretPointOrError.isOk(),
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange() failed");
+ return caretPointOrError;
+ }
+
+ /**
+ * PrepareToSplitBlockElement() makes sure that the invisible white-spaces
+ * not to become visible and returns splittable point.
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aPointToSplit The splitting point in aSplittingBlockElement.
+ * @param aSplittingBlockElement A block element which will be split.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditorDOMPoint, nsresult>
+ PrepareToSplitBlockElement(HTMLEditor& aHTMLEditor,
+ const EditorDOMPoint& aPointToSplit,
+ const Element& aSplittingBlockElement);
+
+ /**
+ * MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement() merges
+ * first line in aRightBlockElement into end of aLeftBlockElement which
+ * is a descendant of aRightBlockElement.
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aLeftBlockElement The content will be merged into end of
+ * this element.
+ * @param aRightBlockElement The first line in this element will be
+ * moved to aLeftBlockElement.
+ * @param aAtRightBlockChild At a child of aRightBlockElement and inclusive
+ * ancestor of aLeftBlockElement.
+ * @param aListElementTagName Set some if aRightBlockElement is a list
+ * element and it'll be merged with another
+ * list element.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditActionResult, nsresult>
+ MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement(
+ HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
+ Element& aRightBlockElement, const EditorDOMPoint& aAtRightBlockChild,
+ const Maybe<nsAtom*>& aListElementTagName,
+ const HTMLBRElement* aPrecedingInvisibleBRElement,
+ const Element& aEditingHost);
+
+ /**
+ * MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement() merges
+ * first line in aRightBlockElement into end of aLeftBlockElement which
+ * is an ancestor of aRightBlockElement, then, removes aRightBlockElement
+ * if it becomes empty.
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aLeftBlockElement The content will be merged into end of
+ * this element.
+ * @param aRightBlockElement The first line in this element will be
+ * moved to aLeftBlockElement and maybe
+ * removed when this becomes empty.
+ * @param aAtLeftBlockChild At a child of aLeftBlockElement and inclusive
+ * ancestor of aRightBlockElement.
+ * @param aLeftContentInBlock The content whose inclusive ancestor is
+ * aLeftBlockElement.
+ * @param aListElementTagName Set some if aRightBlockElement is a list
+ * element and it'll be merged with another
+ * list element.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditActionResult, nsresult>
+ MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement(
+ HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
+ Element& aRightBlockElement, const EditorDOMPoint& aAtLeftBlockChild,
+ nsIContent& aLeftContentInBlock,
+ const Maybe<nsAtom*>& aListElementTagName,
+ const HTMLBRElement* aPrecedingInvisibleBRElement,
+ const Element& aEditingHost);
+
+ /**
+ * MergeFirstLineOfRightBlockElementIntoLeftBlockElement() merges first
+ * line in aRightBlockElement into end of aLeftBlockElement and removes
+ * aRightBlockElement when it has only one line.
+ *
+ * @param aHTMLEditor The HTML editor.
+ * @param aLeftBlockElement The content will be merged into end of
+ * this element.
+ * @param aRightBlockElement The first line in this element will be
+ * moved to aLeftBlockElement and maybe
+ * removed when this becomes empty.
+ * @param aListElementTagName Set some if aRightBlockElement is a list
+ * element and its type needs to be changed.
+ * @param aEditingHost The editing host.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<EditActionResult, nsresult>
+ MergeFirstLineOfRightBlockElementIntoLeftBlockElement(
+ HTMLEditor& aHTMLEditor, Element& aLeftBlockElement,
+ Element& aRightBlockElement, const Maybe<nsAtom*>& aListElementTagName,
+ const HTMLBRElement* aPrecedingInvisibleBRElement,
+ const Element& aEditingHost);
+
+ /**
+ * InsertBRElement() inserts a <br> node at (before) aPointToInsert and delete
+ * unnecessary white-spaces around there and/or replaces white-spaces with
+ * non-breaking spaces. Note that if the point is in a text node, the
+ * text node will be split and insert new <br> node between the left node
+ * and the right node.
+ *
+ * @param aPointToInsert The point to insert new <br> element. Note that
+ * it'll be inserted before this point. I.e., the
+ * point will be the point of new <br>.
+ * @return If succeeded, returns the new <br> element and
+ * point to put caret.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CreateElementResult, nsresult>
+ InsertBRElement(HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert,
+ const Element& aEditingHost);
+
+ /**
+ * Insert aStringToInsert to aPointToInsert and makes any needed adjustments
+ * to white-spaces around the insertion point.
+ *
+ * @param aStringToInsert The string to insert.
+ * @param aRangeToBeReplaced The range to be replaced.
+ */
+ template <typename EditorDOMPointType>
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<InsertTextResult, nsresult>
+ InsertText(HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert,
+ const EditorDOMPointType& aPointToInsert,
+ const Element& aEditingHost) {
+ return WhiteSpaceVisibilityKeeper::ReplaceText(
+ aHTMLEditor, aStringToInsert, EditorDOMRange(aPointToInsert),
+ aEditingHost);
+ }
+
+ /**
+ * Replace aRangeToReplace with aStringToInsert and makes any needed
+ * adjustments to white-spaces around both start of the range and end of the
+ * range.
+ *
+ * @param aStringToInsert The string to insert.
+ * @param aRangeToBeReplaced The range to be replaced.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<InsertTextResult, nsresult>
+ ReplaceText(HTMLEditor& aHTMLEditor, const nsAString& aStringToInsert,
+ const EditorDOMRange& aRangeToBeReplaced,
+ const Element& aEditingHost);
+
+ /**
+ * Delete previous white-space of aPoint. This automatically keeps visibility
+ * of white-spaces around aPoint. E.g., may remove invisible leading
+ * white-spaces.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
+ DeletePreviousWhiteSpace(HTMLEditor& aHTMLEditor,
+ const EditorDOMPoint& aPoint,
+ const Element& aEditingHost);
+
+ /**
+ * Delete inclusive next white-space of aPoint. This automatically keeps
+ * visiblity of white-spaces around aPoint. E.g., may remove invisible
+ * trailing white-spaces.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
+ DeleteInclusiveNextWhiteSpace(HTMLEditor& aHTMLEditor,
+ const EditorDOMPoint& aPoint,
+ const Element& aEditingHost);
+
+ /**
+ * Delete aContentToDelete and may remove/replace white-spaces around it.
+ * Then, if deleting content makes 2 text nodes around it are adjacent
+ * siblings, this joins them and put selection at the joined point.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
+ DeleteContentNodeAndJoinTextNodesAroundIt(HTMLEditor& aHTMLEditor,
+ nsIContent& aContentToDelete,
+ const EditorDOMPoint& aCaretPoint,
+ const Element& aEditingHost);
+
+ /**
+ * Try to normalize visible white-space sequence around aPoint.
+ * This may collapse `Selection` after replaced text. Therefore, the callers
+ * of this need to restore `Selection` by themselves (this does not do it for
+ * performance reason of multiple calls).
+ */
+ template <typename EditorDOMPointType>
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
+ NormalizeVisibleWhiteSpacesAt(HTMLEditor& aHTMLEditor,
+ const EditorDOMPointType& aPoint);
+
+ private:
+ /**
+ * Maybe delete invisible white-spaces for keeping make them invisible and/or
+ * may replace ASCII white-spaces with NBSPs for making visible white-spaces
+ * to keep visible.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<CaretPoint, nsresult>
+ MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
+ HTMLEditor& aHTMLEditor, const EditorDOMRange& aRangeToDelete,
+ const Element& aEditingHost);
+
+ /**
+ * MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit() replaces ASCII white-
+ * spaces which becomes invisible after split with NBSPs.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
+ MakeSureToKeepVisibleWhiteSpacesVisibleAfterSplit(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToSplit);
+
+ /**
+ * ReplaceTextAndRemoveEmptyTextNodes() replaces the range between
+ * aRangeToReplace with aReplaceString simply. Additionally, removes
+ * empty text nodes in the range.
+ *
+ * @param aRangeToReplace Range to replace text.
+ * @param aReplaceString The new string. Empty string is allowed.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
+ ReplaceTextAndRemoveEmptyTextNodes(
+ HTMLEditor& aHTMLEditor, const EditorDOMRangeInTexts& aRangeToReplace,
+ const nsAString& aReplaceString);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef WSRunObject_h
diff --git a/editor/libeditor/crashtests/1057677.html b/editor/libeditor/crashtests/1057677.html
new file mode 100644
index 0000000000..d0b9497a5a
--- /dev/null
+++ b/editor/libeditor/crashtests/1057677.html
@@ -0,0 +1,9 @@
+<html><body></body><script>
+document.designMode = "on";
+var hrElem = document.createElement("HR");
+var select = window.getSelection();
+document.body.appendChild(hrElem);
+select.collapse(hrElem,0);
+document.execCommand("InsertHTML", false, "<div>foo</div><div>bar</div>");
+</script>
+</html>
diff --git a/editor/libeditor/crashtests/1128787.html b/editor/libeditor/crashtests/1128787.html
new file mode 100644
index 0000000000..fc6bff0972
--- /dev/null
+++ b/editor/libeditor/crashtests/1128787.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Bug 1128787</title>
+</head>
+<body>
+ <input type="button"/>
+ <script>
+ window.onload = function () {
+ document.designMode = "on";
+ }
+ var input = document.getElementsByTagName("input")[0];
+ input.focus();
+ input.type = "text";
+ </script>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1134545.html b/editor/libeditor/crashtests/1134545.html
new file mode 100644
index 0000000000..d09fd6beb3
--- /dev/null
+++ b/editor/libeditor/crashtests/1134545.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<!-- saved from url=(0065)https://bug1134545.bugzilla.mozilla.org/attachment.cgi?id=8566418 -->
+<html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text at end of the <body>.
+ getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+ );
+ const textNode = document.createTextNode(" ");
+ const editingHost = document.querySelector("div[contenteditable]");
+ editingHost.appendChild(textNode);
+ editingHost.setAttribute('contenteditable', "true");
+ textNode.remove();
+ getSelection().selectAllChildren(textNode);
+ document.execCommand("increasefontsize");
+}
+</script>
+</head>
+<body onload="onLoad();">
+<div contenteditable></div>
+
+
+</body></html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/1158452.html b/editor/libeditor/crashtests/1158452.html
new file mode 100644
index 0000000000..56c74abd61
--- /dev/null
+++ b/editor/libeditor/crashtests/1158452.html
@@ -0,0 +1,10 @@
+
+<div>
+<div>
+aaaaaaa
+</script>
+<script type="text/javascript">
+document.designMode = "on"
+window.getSelection().modify("extend", "backward", "line")
+document.execCommand("increasefontsize","",null);
+</script>
diff --git a/editor/libeditor/crashtests/1158651.html b/editor/libeditor/crashtests/1158651.html
new file mode 100644
index 0000000000..27278b5234
--- /dev/null
+++ b/editor/libeditor/crashtests/1158651.html
@@ -0,0 +1,18 @@
+<script>
+onload = function() {
+ var testContainer = document.createElement("span");
+ testContainer.contentEditable = true;
+ document.body.appendChild(testContainer);
+
+ function queryFormatBlock(content)
+ {
+ testContainer.innerHTML = content;
+ while (testContainer.firstChild)
+ testContainer = testContainer.firstChild;
+ window.getSelection().collapse(testContainer, 0);
+ document.queryCommandValue('formatBlock');
+ }
+
+ queryFormatBlock('<ol>hello</ol>');
+};
+</script>
diff --git a/editor/libeditor/crashtests/1244894.xhtml b/editor/libeditor/crashtests/1244894.xhtml
new file mode 100644
index 0000000000..89a24751e9
--- /dev/null
+++ b/editor/libeditor/crashtests/1244894.xhtml
@@ -0,0 +1,21 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+
+function boom()
+{
+ document.designMode = 'on';
+ document.execCommand("indent", false, null);
+ document.execCommand("insertText", false, "a");
+ document.execCommand("forwardDelete", false, null);
+ document.execCommand("justifyfull", false, null);
+}
+
+window.addEventListener("load", boom, false);
+
+</script>
+</head>
+
+<body> <span class="v"></span></body><body><input type="file" /></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/1264921.html b/editor/libeditor/crashtests/1264921.html
new file mode 100644
index 0000000000..b53fcb5fd4
--- /dev/null
+++ b/editor/libeditor/crashtests/1264921.html
@@ -0,0 +1 @@
+<body style=display:table-footer-group onload=d=document;h=d.all[0];h.appendChild(d.createElement("input"));h.appendChild(d.createElement("iframe"));d.designMode="on"> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/1272490.html b/editor/libeditor/crashtests/1272490.html
new file mode 100644
index 0000000000..2a8f1d9f9c
--- /dev/null
+++ b/editor/libeditor/crashtests/1272490.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+window.onload = function () {
+ var childDocument = document.getElementsByTagName("iframe")[0].contentDocument;
+ childDocument.designMode = "on";
+ function onAttrModified(aEvent) {
+ childDocument.removeEventListener("DOMAttrModified", onAttrModified);
+ // Remove the editor from document during executing "insertOrderedList".
+ document.body.innerHTML = "";
+ }
+ childDocument.addEventListener("DOMAttrModified", onAttrModified);
+ childDocument.execCommand("insertOrderedList", false, "1");
+}
+</script>
+</head>
+<body><iframe></iframe></body>
+</html>
diff --git a/editor/libeditor/crashtests/1274050.html b/editor/libeditor/crashtests/1274050.html
new file mode 100644
index 0000000000..5b7a5613ef
--- /dev/null
+++ b/editor/libeditor/crashtests/1274050.html
@@ -0,0 +1,17 @@
+<body>
+<table contenteditable></table>
+</body>
+<script>
+window.onload = function() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <body>.
+ getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+ );
+ document.execCommand("styleWithCSS", false, false);
+ document.designMode = "on";
+ document.execCommand("insertUnorderedList");
+ document.execCommand("justifyFull");
+};
+</script>
diff --git a/editor/libeditor/crashtests/1317704.html b/editor/libeditor/crashtests/1317704.html
new file mode 100644
index 0000000000..2f6d2820c3
--- /dev/null
+++ b/editor/libeditor/crashtests/1317704.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+addEventListener('DOMContentLoaded', () => {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <body>.
+ getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+ );
+ document.documentElement.className = 'lizard';
+ setTimeout(() => {
+ document.execCommand('selectAll');
+ document.designMode = 'on';
+ document.execCommand('removeformat');
+ }, 0);
+});
+</script>
+<style>
+.lizard {
+ -webkit-user-select: all;
+}
+* {
+ position: fixed;
+ display: table-column;
+}
+</style>
+</head>
+<body>
+<span>
+<span contenteditable>
+<span class=lizard></span>
+<span class=lizard></span>
+<span />
+</span>
+</span>
+</span>
+</span>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1317718.html b/editor/libeditor/crashtests/1317718.html
new file mode 100644
index 0000000000..892b8aee31
--- /dev/null
+++ b/editor/libeditor/crashtests/1317718.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<script>
+addEventListener('DOMContentLoaded', function() {
+ let root = document.documentElement;
+ while(root.firstChild) {
+ root.firstChild.remove();
+ }
+ document.designMode = 'on';
+ document.removeChild(document.documentElement);
+ document.execCommand('justifyleft', false, null);
+});
+</script>
+</html>
diff --git a/editor/libeditor/crashtests/1324505.html b/editor/libeditor/crashtests/1324505.html
new file mode 100644
index 0000000000..4872b9f05d
--- /dev/null
+++ b/editor/libeditor/crashtests/1324505.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<script>
+addEventListener("DOMContentLoaded", function(){
+ let root = document.documentElement;
+ while(root.firstChild) {
+ root.firstChild.remove();
+ }
+ let o_0 = document.createElement("body");
+ root.appendChild(o_0);
+ o_0.appendChild(document.createElement("table"));
+ o_0.appendChild(document.createElement("canvas"));
+ let o_1 = document.createElement("canvas");
+ o_0.appendChild(o_1);
+ o_1.setAttribute("contenteditable", "true");
+ setTimeout(function(){
+ document.execCommand("selectAll", false, "Marker felt");
+ document.designMode = 'on';
+ document.execCommand("insertorderedlist", false, null);
+ document.execCommand("insertorderedlist", false, null);
+ }, 0);
+});
+</script>
+</html>
diff --git a/editor/libeditor/crashtests/1343918.html b/editor/libeditor/crashtests/1343918.html
new file mode 100644
index 0000000000..24a3d9041f
--- /dev/null
+++ b/editor/libeditor/crashtests/1343918.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script type="application/javascript">
+ let form = document.createElement('form');
+ let input1 = document.createElement('input');
+ let input2 = document.createElement('input');
+ document.documentElement.appendChild(form);
+ document.documentElement.appendChild(input1);
+ form.appendChild(input2);
+ form.contentEditable = true
+ input1.focus();
+
+ let range = document.createRange();
+ range.selectNode(input2);
+ window.getSelection().addRange(range);
+ document.execCommand("italic", false, null);
+ </script>
+ </head>
+ <body></body>
+</html>
diff --git a/editor/libeditor/crashtests/1344097.html b/editor/libeditor/crashtests/1344097.html
new file mode 100644
index 0000000000..7f76957c5a
--- /dev/null
+++ b/editor/libeditor/crashtests/1344097.html
@@ -0,0 +1,20 @@
+<html>
+ <head>
+ <script></script>
+ <script>
+ document.designMode = 'on';
+ o1 = window.getSelection();
+ o2 = document.createElement('code');
+ o3 = document.createElement('style');
+ o4 = document.createElement('style');
+ document.head.appendChild(o3);
+ o1.modify("move", "right", "character");
+ document.styleSheets[0].insertRule("* { position: absolute; }", 0);
+ document.head.appendChild(o4);
+ window.resizeTo(1,1);
+ document.documentElement.appendChild(o2);
+ o1.selectAllChildren(o2);
+ </script>
+ </head>
+ <body></body>
+</html>
diff --git a/editor/libeditor/crashtests/1345015.html b/editor/libeditor/crashtests/1345015.html
new file mode 100644
index 0000000000..3781469578
--- /dev/null
+++ b/editor/libeditor/crashtests/1345015.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script type="application/javascript">
+ document.designMode = "on";
+ let selection = window.getSelection();
+
+ let foo = document.createElement('foo');
+ let div = document.createElement('div');
+ document.documentElement.appendChild(foo);
+ document.documentElement.appendChild(div);
+ foo.outerHTML = '<foo>';
+
+ let range = document.createRange();
+ range.selectNode(div);
+ selection.addRange(range);
+ range.setStart(foo, 0);
+
+ range = document.createRange();
+ range.selectNode(document.documentElement);
+ selection.addRange(range);
+
+ document.execCommand('insertparagraph', false, null);
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/editor/libeditor/crashtests/1348851.html b/editor/libeditor/crashtests/1348851.html
new file mode 100644
index 0000000000..d618f049ee
--- /dev/null
+++ b/editor/libeditor/crashtests/1348851.html
@@ -0,0 +1,19 @@
+<!DOCTYPE>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+function boom(){
+ document.designMode = "on";
+ document.execCommand("insertlinebreak");
+ document.designMode = "off";
+ document.designMode = "on";
+ document.execCommand("insertunorderedlist");
+}
+addEventListener("DOMContentLoaded", boom);
+</script>
+</head>
+<body style="display:flex;">
+<!--comment-->
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1350772.html b/editor/libeditor/crashtests/1350772.html
new file mode 100644
index 0000000000..e20481527d
--- /dev/null
+++ b/editor/libeditor/crashtests/1350772.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+addEventListener("DOMContentLoaded", () => {
+ try {
+ document.designMode = 'on';
+ document.removeChild(document.documentElement);
+ document.appendChild(document.createElement("p"));
+ document.execCommand("insertParagraph", false, null);
+ } catch(e) {
+ }
+});
+</script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1364133.html b/editor/libeditor/crashtests/1364133.html
new file mode 100644
index 0000000000..a1d06fba98
--- /dev/null
+++ b/editor/libeditor/crashtests/1364133.html
@@ -0,0 +1,42 @@
+<html>
+ <head>
+ <script>
+ var tr = document.createElement('tr');
+ document.documentElement.appendChild(tr);
+
+ var a1 = document.createElement('a');
+ document.documentElement.appendChild(a1);
+ var a2 = document.createElement('a');
+ tr.appendChild(a2);
+
+ var a3 = document.createElement('a');
+ document.documentElement.appendChild(a3);
+
+ var a4 = document.createElement('a');
+ document.documentElement.appendChild(a4);
+
+ var a5 = document.createElement('a');
+ a1.appendChild(a5);
+
+ var input = document.createElement('input');
+ document.documentElement.appendChild(input);
+
+ a3.contentEditable = true;
+ a5.innerText = "xx";
+ a4.outerHTML = "";
+ input.select();
+
+ document.replaceChild(document.documentElement, document.documentElement);
+ window.find("x", false, false, false, false, false, false);
+
+ var range = document.createRange();
+ range.setStart(a1, 1);
+ window.getSelection().addRange(range);
+
+ document.designMode = "on";
+
+ range.selectNode(a2);
+ document.execCommand("forecolor", false, "-moz-default-background-color");
+ </script>
+ </head>
+</html>
diff --git a/editor/libeditor/crashtests/1366176.html b/editor/libeditor/crashtests/1366176.html
new file mode 100644
index 0000000000..cf6841c650
--- /dev/null
+++ b/editor/libeditor/crashtests/1366176.html
@@ -0,0 +1,30 @@
+<html>
+<head>
+ <script>
+
+ function go()
+ {
+ try { o114 = document.createElement('canvas'); } catch(e) {;}
+ try { o103 = (new DOMParser).parseFromString('/', 'text/html'); } catch(e) {;}
+ try { o217 = o103.all[1]; } catch(e) {;}
+ try { o253 = window.getSelection(); } catch(e) {;}
+
+ try { o270 = document.body.appendChild(document.createElement('a')); } catch(e) {;}
+
+ try { o301 = window.getSelection(); } catch(e) {;}
+ try { o314 = window.getSelection(); } catch(e) {;}
+ try { document.body.appendChild(o114); } catch(e) {;}
+ try { document.documentElement.appendChild(o217); } catch(e) {;}
+ try { document.body.appendChild(o270); } catch(e) {;}
+ try { o217.contentEditable = true } catch(e) {;}
+
+ try { o253.modify('move','forward','lineboundary'); } catch(e) {;}
+ try { o314.modify('extend','left','line'); } catch(e) {;}
+ try { o301.modify('extend','right','line'); } catch(e) {;}
+ }
+ </script>
+</head>
+<body onload="go();">
+ <plaintext></plaintext>
+</bdoy>
+</html>
diff --git a/editor/libeditor/crashtests/1375131.html b/editor/libeditor/crashtests/1375131.html
new file mode 100644
index 0000000000..4060cf3d4c
--- /dev/null
+++ b/editor/libeditor/crashtests/1375131.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script type="application/javascript">
+ document.designMode = 'on';
+
+ let div = document.createElement('div');
+ let p = document.createElement('p');
+ document.documentElement.appendChild(div);
+ document.documentElement.appendChild(
+ document.createElement('body'));
+ document.documentElement.appendChild(p);
+ document.execCommand('insertimage', false, 'http://localhost/');
+ document.execCommand('insertparagraph', false, null);
+
+ document.elementFromPoint(0, 0);
+
+ let selection = window.getSelection();
+ selection.modify('extend', 'forward', 'character');
+
+ let range = document.createRange();
+ range.selectNode(p);
+ selection.addRange(range);
+ range.setStart(div, 0);
+
+ range = document.createRange();
+ range.selectNode(p);
+ selection.addRange(range);
+
+ document.execCommand('delete', false, null);
+ </script>
+ </head>
+</html>
diff --git a/editor/libeditor/crashtests/1381541.html b/editor/libeditor/crashtests/1381541.html
new file mode 100644
index 0000000000..8902e3d032
--- /dev/null
+++ b/editor/libeditor/crashtests/1381541.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script type="application/javascript">
+ var editable = document.createElement('div');
+ document.documentElement.appendChild(editable);
+ editable.contentEditable = 'true';
+ var div = document.createElement('div');
+ document.documentElement.appendChild(div);
+ // Flush content
+ div.offsetLeft;
+ document.execCommand('styleWithCSS', false, true);
+
+ var range = new Range();
+ window.getSelection().addRange(range);
+ range.setStart(document.createTextNode(''), 0);
+ document.queryCommandState('backcolor');
+ </script>
+ </head>
+</html>
diff --git a/editor/libeditor/crashtests/1383747.html b/editor/libeditor/crashtests/1383747.html
new file mode 100644
index 0000000000..1c926cc7ea
--- /dev/null
+++ b/editor/libeditor/crashtests/1383747.html
@@ -0,0 +1,15 @@
+<html>
+ <head>
+ <script>
+ try { o1 = window.getSelection() } catch(e) { }
+ try { o2 = document.createElement('map') } catch(e) { };
+ try { o3 = document.createElement('select') } catch(e) { }
+ try { document.documentElement.appendChild(o2) } catch(e) { };
+ try { o2.contentEditable = 'true' } catch(e) { };
+ try { o2.offsetTop } catch(e) { };
+ try { document.replaceChild(document.implementation.createHTMLDocument().documentElement, document.documentElement); } catch(e) { }
+ try { document.documentElement.appendChild(o3) } catch(e) { }
+ try { o1.modify('extend', 'forward', 'word') } catch(e) { }
+ </script>
+ </head>
+</html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/1383755.html b/editor/libeditor/crashtests/1383755.html
new file mode 100644
index 0000000000..3cbfe08d17
--- /dev/null
+++ b/editor/libeditor/crashtests/1383755.html
@@ -0,0 +1,26 @@
+<html>
+ <head>
+ <script type="application/javascript">
+ let table = document.createElement('table');
+ document.documentElement.appendChild(table);
+ let tr = document.createElement('tr');
+ table.appendChild(tr);
+ let input = document.createElement('input');
+ document.documentElement.appendChild(input);
+
+ let img = document.createElement('img');
+ input.appendChild(img);
+ img.contentEditable = 'true'
+ tr.appendChild(img);
+ img.offsetParent;
+
+ // Since table's cell is selected by the following, it will show
+ // object resizer that is anonymous element.
+ window.getSelection().selectAllChildren(tr);
+ // Document.adoptNode will remove anonymous element of object resizer
+ // and it shouldn't cause crash
+ document.implementation.createDocument('', '').adoptNode(table);
+ </script>
+ </head>
+</html>
+
diff --git a/editor/libeditor/crashtests/1383763.html b/editor/libeditor/crashtests/1383763.html
new file mode 100644
index 0000000000..a97d685368
--- /dev/null
+++ b/editor/libeditor/crashtests/1383763.html
@@ -0,0 +1,17 @@
+<xsl:stylesheet id='xsl'>
+<script id='script'>
+try { o1 = document.createElement('table') } catch(e) { }
+try { o3 = document.createElement('area') } catch(e) { }
+try { o4 = document.createElement('script'); } catch(e) { }
+try { o5 = document.getSelection() } catch(e) { }
+try { document.implementation.createDocument('', '', null).adoptNode(o1); } catch(e) { }
+try { o1.appendChild(o3) } catch(e) { }
+try { o5.addRange(new Range()); } catch(e) { }
+try { document.documentElement.appendChild(o4) } catch(e) { }
+try { o4.textContent = 'XX' } catch(e) { }
+try { o7 = o4.firstChild } catch(e) { }
+try { o4.parentNode.insertBefore(o7, o4); } catch(e) { }
+try { o5.modify('extend', 'forward', 'line') } catch(e) { }
+try { o5.selectAllChildren(o3) } catch(e) { }
+try { o7.splitText(1) } catch(e) { }
+</script> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/1384161.html b/editor/libeditor/crashtests/1384161.html
new file mode 100644
index 0000000000..c7d8ccc292
--- /dev/null
+++ b/editor/libeditor/crashtests/1384161.html
@@ -0,0 +1,17 @@
+<html>
+ <head>
+ <script>
+ try { o1 = document.createElement('caption') } catch(e) { }
+ try { o2 = document.createElement('select') } catch(e) { }
+ try { o3 = document.createElement('map') } catch(e) { }
+ try { o4 = window.getSelection() } catch(e) { }
+ try { document.documentElement.appendChild(o1) } catch(e) { }
+ try { o1.style.display = 'contents' } catch(e) { }
+ try { document.prepend(o2, document) } catch(e) { }
+ try { document.designMode = 'on'; } catch(e) { }
+ try { o3.ownerDocument.execCommand('outdent', false, null) } catch(e) { }
+ try { document.designMode = 'off'; } catch(e) { }
+ try { o4.extend(o2, 0) } catch(e) { }
+ </script>
+ </head>
+</html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/1388075.html b/editor/libeditor/crashtests/1388075.html
new file mode 100644
index 0000000000..c539f97d57
--- /dev/null
+++ b/editor/libeditor/crashtests/1388075.html
@@ -0,0 +1,60 @@
+<html>
+<head>
+<script>
+try {
+ var style = document.createElement("style");
+} catch(e) {}
+try {
+ var output = document.createElement("output");
+} catch(e) {}
+try {
+ var input = document.createElement("input");
+} catch(e) {}
+try {
+ var script = document.createElement("script");
+} catch(e) {}
+try {
+ var clonedOutput = output.cloneNode(false);
+} catch(e) {}
+try {
+ document.documentElement.appendChild(style);
+} catch(e) {}
+try {
+ style.outerHTML = '<a contenteditable="true">';
+} catch(e) {}
+// For emulating the traditional behavior, collapse Selection to end of the
+// <body> which must be empty (<style> was added after the <body>).
+getSelection().collapse(document.body, document.body.childNodes.length);
+try {
+ document.documentElement.appendChild(input);
+} catch(e) {}
+try {
+ var text = document.createTextNode(" ");
+} catch(e) {}
+try {
+ script.appendChild(text);
+} catch(e) {}
+try {
+ document.documentElement.appendChild(script);
+} catch(e) {}
+try {
+ var selection = getSelection();
+} catch(e) {}
+try {
+ input.select();
+} catch(e) {}
+try {
+ document.replaceChild(document.documentElement, document.documentElement);
+} catch(e) {}
+try {
+ selection.setBaseAndExtent(text, 0, clonedOutput, 0);
+} catch(e) {}
+try {
+ document.designMode = "on";
+} catch(e) {}
+try {
+ document.execCommand("insertImage", false, "http://localhost/");
+} catch(e) {}
+</script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1393171.html b/editor/libeditor/crashtests/1393171.html
new file mode 100644
index 0000000000..0aca3304e7
--- /dev/null
+++ b/editor/libeditor/crashtests/1393171.html
@@ -0,0 +1,10 @@
+<script>
+window.onload=function(){
+ window.getSelection().addRange(document.createRange());
+ document.getElementById('a').appendChild(document.createElement('option'));
+ window.getSelection().modify('extend','backward','lineboundary');
+}
+</script>
+<div></div>
+<textarea autofocus='true'></textarea>
+<del id='a'>
diff --git a/editor/libeditor/crashtests/1402196.html b/editor/libeditor/crashtests/1402196.html
new file mode 100644
index 0000000000..8e0a7be0fe
--- /dev/null
+++ b/editor/libeditor/crashtests/1402196.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <p> which is the deepest last child of the <body> (<spacer> can contain
+ // <p>).
+ getSelection().collapse(
+ document.querySelector("p"),
+ document.querySelector("p").childNodes.length
+ );
+ document.querySelector("spacer").addEventListener("DOMNodeInserted", () => {
+ document.querySelector("style").appendChild(
+ document.querySelector("table")
+ );
+ document.documentElement.classList.remove("reftest-wait");
+ });
+ document.execCommand("insertOrderedList");
+}
+</script>
+</head>
+<body onload="onLoad()">
+<table></table>
+<style></style>
+<spacer contenteditable>
+<p></p>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1402469.html b/editor/libeditor/crashtests/1402469.html
new file mode 100644
index 0000000000..06583acb8b
--- /dev/null
+++ b/editor/libeditor/crashtests/1402469.html
@@ -0,0 +1,22 @@
+<html>
+<head>
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <body>.
+ getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+ );
+ document.execCommand("insertUnorderedList");
+}
+</script>
+</head>
+<body onload="onLoad()">
+<table>
+ <th contenteditable>
+ <ol contenteditable>
+ </th>
+</table>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1402526.html b/editor/libeditor/crashtests/1402526.html
new file mode 100644
index 0000000000..fd75a30eca
--- /dev/null
+++ b/editor/libeditor/crashtests/1402526.html
@@ -0,0 +1,24 @@
+<script>
+function onLoad() {
+ const shadow = document.querySelector("shadow");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <shadow> which is the deepest last child of the <body>.
+ getSelection().collapse(shadow, shadow.childNodes.length);
+ try {
+ document.querySelector("dd[contenteditable]").addEventListener(
+ "DOMNodeInserted",
+ () => {
+ try {
+ shadow.replaceWith("1");
+ } catch(e) {}
+ }
+ );
+ } catch(e) {}
+ try {
+ document.execCommand("justifyFull");
+ } catch(e) {}
+}
+</script>
+<body onload="onLoad();">
+<dd contenteditable>
+<shadow></body>
diff --git a/editor/libeditor/crashtests/1402904.html b/editor/libeditor/crashtests/1402904.html
new file mode 100644
index 0000000000..544d01208a
--- /dev/null
+++ b/editor/libeditor/crashtests/1402904.html
@@ -0,0 +1,38 @@
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // parent of <embed> (<embed> is not a container, therefore, its parent is the
+ // deepest last child container element of the <body>).
+ getSelection().collapse(
+ document.querySelector("embed").parentElement,
+ document.querySelector("embed").parentElement.childNodes.length
+ ); // Point the <embed>
+ const option = document.querySelector("option");
+ option.addEventListener("click", () => {
+ document.execCommand("forwardDelete");
+ });
+ const li2 = document.getElementById("li2");
+ li2.addEventListener("DOMNodeInserted", () => {
+ option.click();
+ });
+ const select = document.querySelector("select");
+ select.parentElement.setAttribute("onpageshow", "onPageShow()");
+}
+
+function onPageShow() {
+ const li1 = document.getElementById("li1");
+ li1.addEventListener("DOMSubtreeModified", () => {
+ document.execCommand("selectAll");
+ document.execCommand("indent");
+ });
+ li1.appendChild(document.createElement("legend"));
+}
+</script>
+<body onload="onLoad()">
+<select>
+<option></option>
+</select>
+<li id="li1"></li>
+<ul contenteditable="true">
+<li id="li2"></li>
+<embed>a;#2
diff --git a/editor/libeditor/crashtests/1405747.html b/editor/libeditor/crashtests/1405747.html
new file mode 100644
index 0000000000..dae3a130f0
--- /dev/null
+++ b/editor/libeditor/crashtests/1405747.html
@@ -0,0 +1,40 @@
+<script>
+let th;
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <colgroup> which is the last child of the <body>.
+ getSelection().collapse(
+ document.querySelector("colgroup"),
+ document.querySelector("colgroup").childNodes.length
+ );
+ th = document.querySelector("th"); // Cache the target for removing the event handler.
+ try {
+ th.addEventListener("DOMSubtreeModified", onDOMSubtreeModified);
+ } catch(e) {}
+ try {
+ th.align = "";
+ } catch(e) {}
+}
+
+let count = 0;
+function onDOMSubtreeModified() {
+ if (count++ == 1) {
+ // If we didn't stop testing this, this event handler would be called too
+ // many times. It's enough to run twice to reproduce the bug report.
+ th.removeEventListener("DOMSubtreeModified", onDOMSubtreeModified);
+ }
+ try {
+ document.execCommand("selectAll");
+ } catch(e) {}
+ try {
+ document.execCommand("justifyCenter");
+ } catch(e) {}
+ try {
+ document.execCommand("forwardDelete");
+ } catch(e) {}
+}
+</script>
+<body onload="onLoad()">
+<table contenteditable>
+<th></th>
+<colgroup></body>
diff --git a/editor/libeditor/crashtests/1405897.html b/editor/libeditor/crashtests/1405897.html
new file mode 100644
index 0000000000..60118628b5
--- /dev/null
+++ b/editor/libeditor/crashtests/1405897.html
@@ -0,0 +1,23 @@
+<style>
+* { position: absolute; }
+</style>
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <iframe> which is the last child of the <body>. Note that
+ // <iframe> is treated as a container in HTMLEditor.
+ const iframe = document.querySelector("iframe");
+ getSelection().collapse(iframe.firstChild, iframe.firstChild.length);
+ document.querySelector("del").addEventListener("DOMSubtreeModified", () => {
+ document.execCommand("italic");
+ document.execCommand("selectAll");
+ });
+ const anchor = document.querySelector("a[contenteditable]");
+ anchor.replaceChild(iframe, anchor.childNodes[0]);
+}
+</script>
+<body onload="onLoad()">
+<a contenteditable>
+<del>
+<iframe>
+</body> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/1408170.html b/editor/libeditor/crashtests/1408170.html
new file mode 100644
index 0000000000..66119e9da3
--- /dev/null
+++ b/editor/libeditor/crashtests/1408170.html
@@ -0,0 +1,40 @@
+<script>
+function onLoad() {
+ try {
+ document.execCommand("insertUnorderedList");
+ } catch(e) {}
+ try {
+ document.execCommand("delete");
+ } catch(e) {}
+}
+
+function onToggle1() {
+ try {
+ getSelection().collapse(
+ document.querySelector("font"),
+ 1
+ );
+ } catch(e) {}
+}
+
+function onToggle2() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <summary> which is the last child of the <body>.
+ const summary = document.querySelector("summary");
+ getSelection().collapse(summary.firstChild, summary.firstChild.length);
+ try {
+ document.querySelector("label").appendChild(
+ document.querySelector("font")
+ );
+ } catch(e) {}
+}
+</script>
+<body onload="onLoad()">
+<label contenteditable>
+<details ontoggle="onToggle2()" open>
+</details>
+</label>
+<details ontoggle="onToggle1()" open>
+<font dir="rtl">
+<summary>
+</details></font></summary></body>
diff --git a/editor/libeditor/crashtests/1414581.html b/editor/libeditor/crashtests/1414581.html
new file mode 100644
index 0000000000..17cdc2f8c8
--- /dev/null
+++ b/editor/libeditor/crashtests/1414581.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function jsfuzzer() {
+ document.execCommand("outdent", false);
+}
+function eventhandler2() {
+ document.execCommand("styleWithCSS", false, true);
+}
+function eventhandler3() {
+ document.execCommand("delete", false);
+ var element1 = document.getElementById("element1");
+ document.getSelection().setPosition(element1, 0);
+ var element1 = document.getElementById("element2");
+ element2.ownerDocument.execCommand("insertOrderedList", false);
+ var element1 = document.getElementById("element3");
+ element3.addEventListener("DOMSubtreeModified", eventhandler3);
+ document.activeElement.setAttribute("contenteditable", "true");
+}
+</script>
+</head>
+<body onload=jsfuzzer()>
+<i id="element1">
+<audio i src="x"></audio>
+<da id="element2">
+<br id="element3" style="">
+<svg onload="eventhandler3()">
+<animate onbegin="eventhandler2()">
+<body>
+</html>
diff --git a/editor/libeditor/crashtests/1415231.html b/editor/libeditor/crashtests/1415231.html
new file mode 100644
index 0000000000..d8702e4542
--- /dev/null
+++ b/editor/libeditor/crashtests/1415231.html
@@ -0,0 +1,26 @@
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <option> which is in the last child of the <body>.
+ const option = document.querySelector("option");
+ getSelection().collapse(option.firstChild, option.firstChild.length);
+ document.querySelector("ins").addEventListener(
+ "DOMNodeRemoved",
+ onDOMNodeRemoved
+ );
+ getSelection().setPosition(
+ document.querySelector("shadow")
+ );
+ document.execCommand("insertText", false, "1");
+}
+function onDOMNodeRemoved() {
+ document.execCommand("insertHorizontalRule");
+ document.execCommand("justifyCenter");
+}
+</script>
+<body onload="onLoad()">
+<li contenteditable>
+<shadow>
+<ins>
+<option>
+</li></shadow></ins></option></body>
diff --git a/editor/libeditor/crashtests/1423767.html b/editor/libeditor/crashtests/1423767.html
new file mode 100644
index 0000000000..777cf58036
--- /dev/null
+++ b/editor/libeditor/crashtests/1423767.html
@@ -0,0 +1,17 @@
+<script>
+function onLoad() {
+ const label = document.querySelector("label");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <label> which is the last child of the <body>, i.e., at the comment node.
+ getSelection().collapse(label, label.childNodes.length);
+ label.addEventListener("DOMNodeRemoved", () => {
+ document.querySelector("a").innerText = "";
+ });
+ document.execCommand("indent");
+}
+</script>
+<body onload="onLoad()">
+<li contenteditable>
+<a>
+<label></br>
+<!---
diff --git a/editor/libeditor/crashtests/1423776.html b/editor/libeditor/crashtests/1423776.html
new file mode 100644
index 0000000000..4790a79f31
--- /dev/null
+++ b/editor/libeditor/crashtests/1423776.html
@@ -0,0 +1,25 @@
+<script>
+function onLoad() {
+ const svg = document.querySelector("svg");
+ const feConvolveMatrix = document.querySelectorAll("feConvolveMatrix");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the last <svg> which is the deepest last child of the <body>
+ // (i.e., at end of the text node after the last <feConvolveMatrix>).
+ getSelection().collapse(svg.lastChild, svg.lastChild.length);
+ feConvolveMatrix[0].addEventListener("DOMNodeInserted", () => {
+ svg.appendChild(feConvolveMatrix[1]);
+ document.execCommand("insertOrderedList", false);
+ });
+ feConvolveMatrix[0].insertAdjacentHTML(
+ "afterBegin",
+ document.querySelector("table").outerHTML
+ );
+}
+</script>
+<body onload="onLoad()">
+<table></table>
+<b contenteditable>
+<svg>
+<feConvolveMatrix/>
+<feConvolveMatrix/>
+</svg></body>
diff --git a/editor/libeditor/crashtests/1424450.html b/editor/libeditor/crashtests/1424450.html
new file mode 100644
index 0000000000..bd0fd98a42
--- /dev/null
+++ b/editor/libeditor/crashtests/1424450.html
@@ -0,0 +1,51 @@
+<script>
+function onLoad() {
+ const feComponentTransfer = document.querySelector("feComponentTransfer");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <feComponentTransfer> which is the deepest last child of
+ // the <body>.
+ getSelection().collapse(
+ feComponentTransfer.firstChild,
+ feComponentTransfer.firstChild.length
+ );
+ getSelection().setPosition(
+ document.querySelector("pre[contenteditable]"),
+ 1
+ );
+ getSelection().setBaseAndExtent(
+ document.querySelector("fieldset"),
+ 0,
+ document.querySelector("use"),
+ 0
+ );
+ feComponentTransfer.before(
+ document.querySelector("font-face-uri").previousElementSibling
+ );
+
+ document.execCommand("removeFormat");
+ document.execCommand("hiliteColor", false, "-moz-buttondefault");
+ document.execCommand("insertText", false, "");
+}
+function onBegin() {
+ document.querySelector("desc").appendChild(
+ document.querySelector("fieldset")
+ );
+ document.querySelector("span").appendChild(
+ document.querySelector("a[hidden][contenteditable]")
+ );
+}
+</script>
+<body onload="onLoad()">
+<span>
+<pre contenteditable>
+<fieldset></fieldset>
+<iframe srcdoc="H"></iframe>
+<a hidden contenteditable>
+<svg>
+<set onbegin="onBegin()"/>
+<use>
+<desc></desc>
+</use>
+<font-face-uri/>
+<feComponentTransfer>
+</feComponentTransfer></svg></a></pre></body>
diff --git a/editor/libeditor/crashtests/1425091.html b/editor/libeditor/crashtests/1425091.html
new file mode 100644
index 0000000000..be8f051931
--- /dev/null
+++ b/editor/libeditor/crashtests/1425091.html
@@ -0,0 +1,25 @@
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <body>, i.e., end of the text node after the
+ // <meter>.
+ getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+ );
+ document.scrollingElement.addEventListener("DOMNodeInserted", () => {
+ document.execCommand("selectAll");
+ document.execCommand("insertOrderedList");
+ });
+ document.querySelector("style").appendChild(
+ document.createElement("e")
+ );
+}
+</script>
+<body onload="onLoad()">
+<meter contenteditable>
+<dialog>
+<style></style>
+</dialog>
+</meter>
+</body>
diff --git a/editor/libeditor/crashtests/1426709.html b/editor/libeditor/crashtests/1426709.html
new file mode 100644
index 0000000000..0026176e98
--- /dev/null
+++ b/editor/libeditor/crashtests/1426709.html
@@ -0,0 +1,27 @@
+<script>
+function onLoad() {
+ const pre = document.querySelector("pre[contenteditable]");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <pre> which is the deepest last child of the
+ // <body>, i.e., end of the text node after the <input>.
+ getSelection().collapse(pre.lastChild, pre.lastChild.length);
+ document.querySelector("li").setAttribute("contenteditable", "true");
+ pre.addEventListener("DOMNodeRemoved", onDOMNodeRemoved);
+ pre.appendChild(
+ document.querySelector("input")
+ );
+}
+function onDOMNodeRemoved() {
+ document.body.appendChild(
+ document.querySelector("pre[contenteditable]")
+ );
+ document.execCommand("justifyFull");
+ document.execCommand("delete");
+}
+</script>
+<body onload="onLoad()">
+<li>
+A
+<pre contenteditable>
+<input autofocus>
+</pre></li></body>
diff --git a/editor/libeditor/crashtests/1429523.html b/editor/libeditor/crashtests/1429523.html
new file mode 100644
index 0000000000..67b227d20b
--- /dev/null
+++ b/editor/libeditor/crashtests/1429523.html
@@ -0,0 +1,18 @@
+<html class="reftest-wait">
+ <head>
+ <script>
+ document.designMode = "on";
+ Promise.all([
+ new Promise(resolve => addEventListener("load", resolve)),
+ new Promise(resolve => addEventListener("focus", resolve)),
+ ]).then(() => {
+ document.body.firstChild.data = "";
+ document.documentElement.removeAttribute("class");
+ });
+ blur();
+ focus();
+ getSelection().collapse(document.body, 0);
+ </script>
+ </head>
+ <body><!-- comment --></body>
+</html>
diff --git a/editor/libeditor/crashtests/1429523.xhtml b/editor/libeditor/crashtests/1429523.xhtml
new file mode 100644
index 0000000000..da2fe16b30
--- /dev/null
+++ b/editor/libeditor/crashtests/1429523.xhtml
@@ -0,0 +1,18 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+ <head>
+ <script>
+ document.designMode = "on";
+ Promise.all([
+ new Promise(resolve => addEventListener("load", resolve)),
+ new Promise(resolve => addEventListener("focus", resolve)),
+ ]).then(() => {
+ document.body.firstChild.data = "";
+ document.documentElement.removeAttribute("class");
+ });
+ blur();
+ focus();
+ getSelection().collapse(document.body, 0);
+ </script>
+ </head>
+ <body><![CDATA[ CDATA ]]></body>
+</html>
diff --git a/editor/libeditor/crashtests/1441619.html b/editor/libeditor/crashtests/1441619.html
new file mode 100644
index 0000000000..08343feeed
--- /dev/null
+++ b/editor/libeditor/crashtests/1441619.html
@@ -0,0 +1,12 @@
+<html>
+ <head>
+ <script>
+ try { o1 = window.getSelection() } catch (e) {}
+ try { o2 = document.createRange() } catch (e) {}
+ try { document.head.replaceWith('', document.documentElement) } catch (e) {}
+ try { o1.addRange(o2) } catch (e) {}
+ try { document.designMode = 'on' } catch (e) {}
+ try { o1.focusNode.execCommand('justifyleft', false, null) } catch (e) {}
+ </script>
+ </head>
+</html>
diff --git a/editor/libeditor/crashtests/1443664.html b/editor/libeditor/crashtests/1443664.html
new file mode 100644
index 0000000000..b197d3cd49
--- /dev/null
+++ b/editor/libeditor/crashtests/1443664.html
@@ -0,0 +1,15 @@
+<style>
+input:focus { counter-increment: c; }
+</style>
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <body>, i.e., end of the text node after the
+ // <input contenteditable>.
+ getSelection().collapse(document.body, document.body.childNodes.length);
+ document.querySelector("input[type=number][contenteditable]").select();
+}
+</script>
+<body onload="onLoad()">
+<input type="number" contenteditable>
+</body>
diff --git a/editor/libeditor/crashtests/1444630.html b/editor/libeditor/crashtests/1444630.html
new file mode 100644
index 0000000000..3bdc490b36
--- /dev/null
+++ b/editor/libeditor/crashtests/1444630.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+function load()
+{
+ let textarea = document.getElementById("editor");
+ textarea.focus();
+
+ const { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://reftest/AsyncSpellCheckTestHelper.sys.mjs"
+ );
+ maybeOnSpellCheck(textarea, () => {
+ let isc = SpecialPowers.wrap(textarea).editor.getInlineSpellChecker(false);
+ let sc = isc.spellChecker;
+
+ textarea.setAttribute("lang", "en-US");
+ sc.UpdateCurrentDictionary(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+ sc.UninitSpellChecker();
+ });
+}
+</script>
+</head>
+<body onload="load()">
+<textarea id="editor" spellchecker="true">ABC</textarea>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1446451.html b/editor/libeditor/crashtests/1446451.html
new file mode 100644
index 0000000000..56c887dc3b
--- /dev/null
+++ b/editor/libeditor/crashtests/1446451.html
@@ -0,0 +1,13 @@
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <dialog> which is the deepest last child of the <body>.
+ const dialog = document.querySelector("dialog");
+ getSelection().collapse(dialog.lastChild, dialog.lastChild.length);
+ document.execCommand("insertImage", false, "o")
+}
+</script>
+<body onload="onLoad()">
+<meter contenteditable>
+<dialog>
+</dialog></meter></body>
diff --git a/editor/libeditor/crashtests/1464251.html b/editor/libeditor/crashtests/1464251.html
new file mode 100644
index 0000000000..afe7e87561
--- /dev/null
+++ b/editor/libeditor/crashtests/1464251.html
@@ -0,0 +1,25 @@
+<script>
+var count = 0;
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <s> which is the deepest last child (and a container) of
+ // the <body> (i.e., end of the the text node after the last comment node).
+ const s = document.querySelector("s");
+ getSelection().collapse(s.lastChild, s.lastChild.length);
+ document.execCommand("delete");
+}
+
+function onInputOrDOMNodeInserted() {
+ if (++count >= 3) {
+ return;
+ }
+ addEventListener("DOMNodeInserted", onInputOrDOMNodeInserted);
+ document.execCommand("removeFormat");
+ document.execCommand("insertText", false, "1");
+}
+</script>
+<body onload="onLoad()">
+<ol oninput="onInputOrDOMNodeInserted()" contenteditable>
+<!-- x -->
+<s>
+<!-- x -->
diff --git a/editor/libeditor/crashtests/1470926.html b/editor/libeditor/crashtests/1470926.html
new file mode 100644
index 0000000000..9c39710a97
--- /dev/null
+++ b/editor/libeditor/crashtests/1470926.html
@@ -0,0 +1,9 @@
+<script>
+function go() {
+ a.select();
+ a.setAttribute("hidden", "");
+ a.setRangeText("f");
+}
+</script>
+<body onload=go()>
+<textarea id="a">-</textarea>
diff --git a/editor/libeditor/crashtests/1474978.html b/editor/libeditor/crashtests/1474978.html
new file mode 100644
index 0000000000..d7eb01e03b
--- /dev/null
+++ b/editor/libeditor/crashtests/1474978.html
@@ -0,0 +1,21 @@
+<script>
+function onLoad() {
+ const dd = document.querySelector("dd[contenteditable]");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <dd contenteditable> which is the deepest last
+ // child of the <body> (end of the text node after the <template>).
+ getSelection().collapse(dd.lastChild, dd.lastChild.length);
+ getSelection().setPosition(
+ document.querySelector("template")
+ );
+ dd.addEventListener("DOMNodeInserted", () => {
+ document.execCommand("selectAll");
+ document.execCommand("insertText", false, "");
+ });
+ document.execCommand("insertImage", false, "#");
+}
+</script>
+<body onload="onLoad()">
+<dd contenteditable>
+<template></template>
+</dd></body>
diff --git a/editor/libeditor/crashtests/1517028.html b/editor/libeditor/crashtests/1517028.html
new file mode 100644
index 0000000000..cbc3b71c0a
--- /dev/null
+++ b/editor/libeditor/crashtests/1517028.html
@@ -0,0 +1,23 @@
+<!-- COMMENT -->
+abc
+<head>
+<script>
+document.addEventListener('DOMContentLoaded', () => {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <em> which is the deepest last child of the <body> (at the comment node).
+ getSelection().collapse(
+ document.querySelector("em[contenteditable]"),
+ document.querySelector("em[contenteditable]").childNodes.length
+ );
+ document.execCommand("justifyLeft");
+ document.designMode = 'on';
+ document.execCommand("insertParagraph");
+});
+</script>
+</head>
+<h4>
+<em contenteditable>
+<big>
+<button autofocus formnovalidate formtarget="">
+</button>
+<!-- COMMENT --></big></em></h4></body>
diff --git a/editor/libeditor/crashtests/1525481.html b/editor/libeditor/crashtests/1525481.html
new file mode 100644
index 0000000000..d96e1c2d0d
--- /dev/null
+++ b/editor/libeditor/crashtests/1525481.html
@@ -0,0 +1,23 @@
+<script>
+function onLoad() {
+ const button = document.querySelector("button");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <button> which is the deepest last child of the <body>.
+ getSelection().collapse(button, button.childNodes.length);
+ document.execCommand("styleWithCSS", false, true);
+ document.execCommand("delete");
+ document.querySelector("ul[contenteditable]")
+ .addEventListener("DOMNodeRemoved", () => {
+ const range = document.createRange();
+ range.setEndAfter(button);
+ getSelection().addRange(range);
+ getSelection().deleteFromDocument();
+ });
+ document.execCommand("outdent");
+}
+</script>
+<body onload="onLoad()">
+<ul contenteditable style="margin: -1px 0px 1px 6px">
+<dd></dd>
+<dd>
+<button></button>></dd></ul></body>
diff --git a/editor/libeditor/crashtests/1533913.html b/editor/libeditor/crashtests/1533913.html
new file mode 100644
index 0000000000..3009bbb660
--- /dev/null
+++ b/editor/libeditor/crashtests/1533913.html
@@ -0,0 +1,19 @@
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node which is the last child of the <dd contenteditable> which is the
+ // last child of the <body>.
+ const dd = document.querySelector("dd[contenteditable]");
+ getSelection().collapse(dd.lastChild, dd.lastChild.length);
+ document.execCommand("justifyFull");
+ document.execCommand("selectAll");
+ window.top.addEventListener("DOMNodeRemoved", () => {
+ document.execCommand("insertHTML", false, undefined)
+ });
+ document.execCommand("heading", false, "H1")
+}
+</script>
+<body onload="onLoad()">
+<dd contenteditable>A
+<!-- A -->
+</dd></body>
diff --git a/editor/libeditor/crashtests/1534394.html b/editor/libeditor/crashtests/1534394.html
new file mode 100644
index 0000000000..c254aa5f52
--- /dev/null
+++ b/editor/libeditor/crashtests/1534394.html
@@ -0,0 +1,29 @@
+<html>
+<head>
+ <script>
+
+ function start () {
+ const sel = document.getSelection()
+ const range_1 = new Range()
+ const noscript = document.getElementById('id_24')
+ const map = document.getElementById('id_26')
+ sel.selectAllChildren(noscript)
+ const range_2 = range_1.cloneRange()
+ range_2.selectNode(map)
+ const frag = range_2.extractContents()
+ range_2.insertNode(noscript)
+ document.documentElement.contentEditable = true
+ document.execCommand('removeFormat', false,)
+ noscript.contentEditable = false
+ document.execCommand('insertText', false, '�\n')
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+<body>
+<big class="">
+ <map class="" id="id_26">
+ <noscript class="" id="id_24">
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1547897.html b/editor/libeditor/crashtests/1547897.html
new file mode 100644
index 0000000000..9543048e6c
--- /dev/null
+++ b/editor/libeditor/crashtests/1547897.html
@@ -0,0 +1,25 @@
+<html>
+<head>
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // last text node of the <body> (end of the text node after the <p hidden>).
+ getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+ );
+ getSelection().setPosition(
+ document.querySelector("hr"),
+ 0
+ );
+ document.execCommand("delete");
+}
+</script>
+<body onload="onLoad()">
+<p hidden>
+ <canvas contenteditable>
+ <hr contenteditable>
+ 3uW4*</hr></canvas>
+</p>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1547898.html b/editor/libeditor/crashtests/1547898.html
new file mode 100644
index 0000000000..696e429e0b
--- /dev/null
+++ b/editor/libeditor/crashtests/1547898.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <script>
+ function jsfuzzer () {
+ window.find('1')
+ const selection = window.getSelection()
+ selection.extend(document.getElementById('list_item'))
+ document.getElementById('list_item').contentEditable = 'true'
+ document.execCommand('indent', false)
+ }
+ </script>
+</head>
+<body onload=jsfuzzer()>
+<li id="list_item">16</li>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1556799.html b/editor/libeditor/crashtests/1556799.html
new file mode 100644
index 0000000000..7c213b1f27
--- /dev/null
+++ b/editor/libeditor/crashtests/1556799.html
@@ -0,0 +1,18 @@
+<script>
+function onError() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <dl> which is the last deepest child of the <body>.
+ const dl = document.querySelector("dl");
+ getSelection().collapse(dl.lastChild, dl.lastChild.length);
+ getSelection().selectAllChildren(
+ document.querySelector("img")
+ );
+ document.execCommand("insertUnorderedList");
+ document.execCommand("enableObjectResizing");
+}
+</script>
+<dd contenteditable="true">
+<audio src="data:text/html,foo" onerror="onError()">
+<img></img>
+<dl>
+</dl></audio><dd></body>
diff --git a/editor/libeditor/crashtests/1574544.html b/editor/libeditor/crashtests/1574544.html
new file mode 100644
index 0000000000..26b2fee5ff
--- /dev/null
+++ b/editor/libeditor/crashtests/1574544.html
@@ -0,0 +1,17 @@
+<style>
+* { display: table-caption }
+body { display: contents }
+</style>
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <th> which is the deepest last child of the <body>.
+ const th = document.querySelector("th");
+ getSelection().collapse(th.lastChild, th.lastChild.length);
+ document.execCommand("enableInlineTableEditing");
+}
+</script>
+<body onload="onLoad()">
+<table contenteditable>
+<th>
+</th></table></body>
diff --git a/editor/libeditor/crashtests/1578916.html b/editor/libeditor/crashtests/1578916.html
new file mode 100644
index 0000000000..f1d79be804
--- /dev/null
+++ b/editor/libeditor/crashtests/1578916.html
@@ -0,0 +1,28 @@
+<script>
+function onLoad() {
+ const data = document.querySelector("data");
+ const source = document.querySelector("source");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <data> which is the deepest last child (and a container) of the <body>.
+ getSelection().collapse(data, data.childNodes.length);
+ source.appendChild(
+ document.body.firstChild // The invisible text node
+ );
+ getSelection().setBaseAndExtent(
+ data.appendChild(source),
+ 0,
+ source,
+ 1
+ );
+ document.querySelector("audio")
+ .addEventListener("DOMCharacterDataModified", () => {
+ getSelection().removeAllRanges()
+ });
+ document.execCommand("delete");
+}
+</script>
+<body onload="onLoad()">
+<audio>
+<li contenteditable>
+<data>
+<source>
diff --git a/editor/libeditor/crashtests/1579934.html b/editor/libeditor/crashtests/1579934.html
new file mode 100644
index 0000000000..55be305e2f
--- /dev/null
+++ b/editor/libeditor/crashtests/1579934.html
@@ -0,0 +1,39 @@
+<html><script>
+window["_gaUserPrefs"] = { ioo : function() { return true; } }
+</script><head>
+<meta charset="windows-1252"><script>
+function onLoad() {
+ const form = document.querySelector("form");
+ form.addEventListener("DOMCharacterDataModified", () => {
+ document.documentElement.appendChild(form);
+ });
+ document.execCommand("delete");
+}
+
+function onToggle() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node node in the <button> which is the deepest last child of the
+ // <body>.
+ const button = document.querySelector("button");
+ getSelection().collapse(button.lastChild, button.lastChild.length);
+ document.querySelector("form").reset();
+}
+</script>
+</head><body onload="onLoad()">
+<details ontoggle="onToggle()" open>
+<dt contenteditable>
+</dt>
+</details><aside style="
+ position: fixed;
+ top: 0px;
+ right: 0px;
+ font-family: &quot;Lucida Console&quot;, monospace;
+ background-color: rgb(242, 230, 217);
+ padding: 3px;
+ z-index: 10000;
+ text-align: center;
+ max-width: 120px;
+ opacity: 0;
+ transition: opacity 0.5s linear 0s;">1194 x 73</aside></body><form id="b"><keygen>
+<button autofocus>
+</button></form></html>
diff --git a/editor/libeditor/crashtests/1581246.html b/editor/libeditor/crashtests/1581246.html
new file mode 100644
index 0000000000..e9c472c7d9
--- /dev/null
+++ b/editor/libeditor/crashtests/1581246.html
@@ -0,0 +1,21 @@
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <textarea> which is the last child of the <body>.
+ // However, it may be omitted by the parser. Therefore, this test tries to
+ // check the text node, but if it does not exist, uses the <textarea>.
+ const textarea = document.querySelector("textarea");
+ const textareaOrTextNode = textarea.lastChild ? textarea.lastChild : textarea;
+ getSelection().collapse(textareaOrTextNode, textareaOrTextNode.length);
+ document.querySelector("script").appendChild(
+ document.querySelector("li[contenteditable=false]")
+ );
+ document.execCommand("indent");
+ document.execCommand("delete");
+}
+</script>
+<body onload="onLoad()">
+<ul contenteditable>
+<li contenteditable="false">
+<textarea autofocus>
+</textarea>></li></ul></body>
diff --git a/editor/libeditor/crashtests/1596516.html b/editor/libeditor/crashtests/1596516.html
new file mode 100644
index 0000000000..37b9c5889c
--- /dev/null
+++ b/editor/libeditor/crashtests/1596516.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+ <script>
+ function start () {
+ const italic = document.createElementNS('http://www.w3.org/1999/xhtml', 'i')
+ italic.dir = ''
+ document.documentElement.contentEditable = 'true'
+ const selection = document.getSelection()
+ const range = selection.getRangeAt(0)
+ const attribute = italic.attributes.getNamedItem('dir')
+ range.setEnd(attribute, (2361162229 % attribute.childNodes))
+ document.queryCommandState('heading')
+ }
+
+ document.addEventListener('DOMContentLoaded', start)
+ </script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1605741.html b/editor/libeditor/crashtests/1605741.html
new file mode 100644
index 0000000000..7868adcf96
--- /dev/null
+++ b/editor/libeditor/crashtests/1605741.html
@@ -0,0 +1,14 @@
+<script>
+addEventListener("load", () => {
+ const editingHost = document.querySelector("div[contenteditable]");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <div contenteditable> which is the last child of the
+ // <body>.
+ getSelection().collapse(editingHost.lastChild, editingHost.lastChild.length);
+ editingHost.addEventListener("DOMNodeRemoved", () => {
+ getSelection().collapse(null);
+ });
+ document.execCommand("delete");
+});
+</script>
+<div contenteditable>x</link></body>
diff --git a/editor/libeditor/crashtests/1613521.html b/editor/libeditor/crashtests/1613521.html
new file mode 100644
index 0000000000..7d59667559
--- /dev/null
+++ b/editor/libeditor/crashtests/1613521.html
@@ -0,0 +1,16 @@
+<script>
+function go() {
+ window.find("1", true, false)
+ a.setAttribute("contenteditable", "true")
+ document.execCommand("forwardDelete", false)
+ document.execCommand("enableObjectResizing", false)
+}
+</script>
+<dl id="a">
+<table>
+<th>
+GjdR1W3
+<svg>
+<animateMotion onbegin="go()" />
+</tr>
+<details ontoggle="go()" open>
diff --git a/editor/libeditor/crashtests/1618906.html b/editor/libeditor/crashtests/1618906.html
new file mode 100644
index 0000000000..cdb278f018
--- /dev/null
+++ b/editor/libeditor/crashtests/1618906.html
@@ -0,0 +1,17 @@
+<html>
+<head>
+ <script>
+ window.addEventListener('load', () => {
+ const range = new Range()
+ const fragment = range.cloneContents()
+ range.selectNodeContents(document)
+ document.designMode = 'on'
+ document.replaceChild(fragment, document.documentElement)
+ const selection = window.getSelection()
+ selection.addRange(range)
+ document.execCommand('indent', false, null)
+ })
+ </script>
+</head>
+</html>
+<!-- COMMENT -->
diff --git a/editor/libeditor/crashtests/1623166.html b/editor/libeditor/crashtests/1623166.html
new file mode 100644
index 0000000000..75bbb20872
--- /dev/null
+++ b/editor/libeditor/crashtests/1623166.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+<script>
+document.addEventListener('DOMContentLoaded', () => {
+ const code = document.querySelector("code[contenteditable=false]");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // last text node in the <code> which is the last inline container of the
+ // <body>.
+ getSelection().collapse(code.lastChild, code.lastChild.length);
+ code.querySelector("br").contentEditable = true;
+ document.execCommand("indent");
+});
+</script>
+<body>
+<b contenteditable hidden>
+ <script></script>
+ <code contenteditable="false">
+ <br>
+</body>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1623913.html b/editor/libeditor/crashtests/1623913.html
new file mode 100644
index 0000000000..291bd65197
--- /dev/null
+++ b/editor/libeditor/crashtests/1623913.html
@@ -0,0 +1,30 @@
+<html>
+<head>
+ <script>
+ async function start () {
+ const element_0 = document.createElementNS('', 's')
+ const svg = document.getElementById('id_8')
+ const math = document.getElementById('id_20')
+ const selection = window.getSelection()
+ const range = new Range()
+ range.setStartAfter(math)
+ range.insertNode(svg)
+ range.selectNodeContents(element_0)
+ document.documentElement.contentEditable = true
+ range.setStart(svg, (2760506212 % svg.childNodes))
+ selection.addRange(range)
+ document.execCommand('insertOrderedList', false, null)
+ document.execCommand('insertParagraph', false, null)
+ }
+
+ document.addEventListener('DOMContentLoaded', start)
+ </script>
+</head>
+<body>
+<svg id='id_8'>
+ <metadata xml:space='preserve'>
+</svg>
+<math id='id_20'>
+</math>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1624005.html b/editor/libeditor/crashtests/1624005.html
new file mode 100644
index 0000000000..9d277db6ed
--- /dev/null
+++ b/editor/libeditor/crashtests/1624005.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+ <script>
+ window.addEventListener('load', () => {
+ const element = document.createElementNS('', 't')
+ document.designMode = 'on'
+ const range = new Range()
+ range.selectNodeContents(element)
+ range.surroundContents(document.documentElement)
+ document.execCommand('insertHorizontalRule', false, null)
+ })
+ </script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1624007.html b/editor/libeditor/crashtests/1624007.html
new file mode 100644
index 0000000000..06e35e1d50
--- /dev/null
+++ b/editor/libeditor/crashtests/1624007.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+ <script>
+ window.addEventListener('load', () => {
+ const element = document.createElement('t')
+ document.documentElement.appendChild(element)
+ document.documentElement.contentEditable = true
+ const selection = self.getSelection()
+ const range_1 = new Range()
+ range_1.setStart(element, (1715865858 % element.childNodes))
+ document.execCommand('enableObjectResizing', false, true)
+ const range_2 = new Range()
+ selection.addRange(range_2)
+ selection.addRange(range_1)
+ document.execCommand('bold', false, null)
+ })
+ </script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1624011.html b/editor/libeditor/crashtests/1624011.html
new file mode 100644
index 0000000000..98267f848d
--- /dev/null
+++ b/editor/libeditor/crashtests/1624011.html
@@ -0,0 +1,12 @@
+<html>
+<head>
+ <script>
+ window.addEventListener('load', () => {
+ document.designMode = 'on'
+ const selection = document.getSelection()
+ selection.empty()
+ document.queryCommandIndeterm('justifyFull')
+ })
+ </script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1626002.html b/editor/libeditor/crashtests/1626002.html
new file mode 100644
index 0000000000..55099cb648
--- /dev/null
+++ b/editor/libeditor/crashtests/1626002.html
@@ -0,0 +1,27 @@
+<html>
+<head>
+<script>
+addEventListener("load", () => {
+ const anchor = document.createElement("a");
+ const b = document.createElement("b");
+ const c = document.createElement("c");
+ document.documentElement.appendChild(anchor);
+ anchor.appendChild(b);
+ b.setAttribute("contenteditable", "true");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <body> which must be empty because this test appends the new elements after
+ // the <body>.
+ const selection = self.getSelection();
+ selection.collapse(document.body, document.body.childNodes.length);
+ b.appendChild(c);
+ c.outerHTML = '<s contenteditable="false"><b contenteditable="true">';
+ selection.setBaseAndExtent(document, 0, document.documentElement, 0);
+ const range = selection.getRangeAt((260523900 % selection.rangeCount));
+ selection.selectAllChildren(b);
+ range.collapse(false);
+ range.setEndAfter(document.documentElement);
+ range.extractContents();
+});
+</script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1636541.html b/editor/libeditor/crashtests/1636541.html
new file mode 100644
index 0000000000..71a92817d8
--- /dev/null
+++ b/editor/libeditor/crashtests/1636541.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+ <script>
+ window.addEventListener('load', () => {
+ document.documentElement.contentEditable = true
+ document.execCommand('indent', false, null)
+ const selection = document.getSelection()
+ selection.collapse(document.documentElement, (3474956128 % document.documentElement.childNodes))
+ document.execCommand('indent', false, null)
+ document.execCommand('outdent', false, null)
+ })
+ </script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1644903.html b/editor/libeditor/crashtests/1644903.html
new file mode 100644
index 0000000000..175c6afda7
--- /dev/null
+++ b/editor/libeditor/crashtests/1644903.html
@@ -0,0 +1,19 @@
+<script>
+window.onload = () => {
+ document.execCommand("undo");
+}
+function onToggle() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <details> (<p> is closed before the <details>).
+ const details = document.querySelector("details");
+ getSelection().collapse(details.lastChild, details.lastChild.length);
+ const link = document.querySelector("link");
+ document.execCommand("delete");
+ document.querySelector("iframe").contentDocument.adoptNode(link);
+}
+</script>
+<p contenteditable>
+<link item="">
+<details open ontoggle="onToggle()">
+<iframe></iframe>
+</details></body>
diff --git a/editor/libeditor/crashtests/1645983-1.html b/editor/libeditor/crashtests/1645983-1.html
new file mode 100644
index 0000000000..c1bb7219a5
--- /dev/null
+++ b/editor/libeditor/crashtests/1645983-1.html
@@ -0,0 +1,11 @@
+<div contenteditable>&nbsp;a</div>
+<script>
+// For emulating the traditional behavior, collapse Selection to end of the
+// text node in this <script>.
+const script = document.querySelector("script");
+getSelection().collapse(script.lastChild, script.lastChild.length);
+const editingHost = document.querySelector("div[contenteditable]");
+editingHost.insertBefore(document.createTextNode(""), editingHost.firstChild);
+getSelection().collapse(editingHost.firstChild.nextSibling, 2);
+document.execCommand("delete");
+</script></body>
diff --git a/editor/libeditor/crashtests/1645983-2.html b/editor/libeditor/crashtests/1645983-2.html
new file mode 100644
index 0000000000..132c1381f3
--- /dev/null
+++ b/editor/libeditor/crashtests/1645983-2.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<div contenteditable>abc<span></span><span></span></div>
+<script>
+// For emulating the traditional behavior, collapse Selection to end of the
+// text node after this <script>.
+getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+);
+const editingHost = document.querySelector("div[contenteditable]");
+getSelection().collapse(editingHost, 3);
+document.execCommand("insertText", false, " ");
+</script>
+</body>
diff --git a/editor/libeditor/crashtests/1648564.html b/editor/libeditor/crashtests/1648564.html
new file mode 100644
index 0000000000..f82ec3d8e6
--- /dev/null
+++ b/editor/libeditor/crashtests/1648564.html
@@ -0,0 +1,29 @@
+<script>
+window.addEventListener('load', () => {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <body> (at the comment node).
+ getSelection().collapse(document.body, document.body.childNodes.length);
+ const map = document.querySelector("map");
+ const anchor = document.querySelector("a");
+ map.replaceChild(
+ anchor,
+ map.childNodes[(2828994049 % map.childNodes.length)]
+ );
+ anchor.innerHTML = "<o>";
+ getSelection().setBaseAndExtent(
+ document,
+ (2019424593 % document.childNodes.length),
+ document.documentElement,
+ (3503355750 % document.documentElement.childNodes.length)
+ );
+ document.designMode = "on";
+ document.execCommand("forwardDelete");
+ document.execCommand("forwardDelete");
+});
+</script>
+<sub contenteditable>
+<map>
+<address></address>
+<a>
+</a>
+<!-- COMMENT --></body>
diff --git a/editor/libeditor/crashtests/1655508.html b/editor/libeditor/crashtests/1655508.html
new file mode 100644
index 0000000000..e30aba0ca0
--- /dev/null
+++ b/editor/libeditor/crashtests/1655508.html
@@ -0,0 +1,34 @@
+<html>
+<head>
+<script>
+document.addEventListener("DOMContentLoaded", () => {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <body> (end of the text node after the <h3>).
+ getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+ );
+ const textarea = document.querySelector("textarea");
+ const abbr = document.querySelector("abbr");
+ let node;
+ document.addEventListener("DOMAttrModified", event => {
+ node = event.originalTarget.getRootNode({});
+ abbr.insertBefore(textarea, abbr.childNodes[0]);
+ }, false);
+ abbr.contentEditable = false;
+ node.normalize();
+ document.designMode = "on";
+ document.execCommand("insertParagraph");
+});
+</script>
+</head>
+<body>
+<dfn contenteditable>
+ <abbr></abbr>
+</dfn>
+<h3>
+ <textarea autofocus></textarea>
+ <script></script>
+</h3>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1655539.html b/editor/libeditor/crashtests/1655539.html
new file mode 100644
index 0000000000..094c7b2e30
--- /dev/null
+++ b/editor/libeditor/crashtests/1655539.html
@@ -0,0 +1,15 @@
+<script>
+ document.addEventListener('DOMContentLoaded', () => {
+ selection = document.getSelection()
+ element = document.createElementNS('', 'm')
+ document.documentElement.appendChild(element)
+ range = new Range()
+ range.setStartBefore(element)
+ selection.addRange(range)
+ document.documentElement.contentEditable = true
+ document.execCommand('forwardDelete', false, null)
+ })
+</script>
+<iframe class='\'>'>
+</iframe>
+<!-- COMMENT --> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/1655988.html b/editor/libeditor/crashtests/1655988.html
new file mode 100644
index 0000000000..666a5f8cb8
--- /dev/null
+++ b/editor/libeditor/crashtests/1655988.html
@@ -0,0 +1,29 @@
+<html>
+<head>
+<script>
+document.addEventListener("DOMContentLoaded", () => {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <body> (end of the text node after the
+ // <feDistantLight>).
+ getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+ );
+ const feDistantLight = document.querySelector("feDistantLight");
+ const li = document.querySelector("li");
+ li.after('foo');
+ feDistantLight.addEventListener("DOMAttrModified", () => {
+ window.find("foo");
+ document.execCommand("insertImage", false, "#");
+ })
+ feDistantLight.setAttribute("i", "");
+});
+</script>
+</head>
+<body>
+<feDistantLight contenteditable>
+ <li>A</li>
+ <!-- COMMENT -->
+</feDistantLight>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1659717.html b/editor/libeditor/crashtests/1659717.html
new file mode 100644
index 0000000000..a3e9c59699
--- /dev/null
+++ b/editor/libeditor/crashtests/1659717.html
@@ -0,0 +1,14 @@
+<html><script type="text/javascript" id="__gaOptOutExtension">window["_gaUserPrefs"] = { ioo : function() { return true; } }</script><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <script>
+ document.addEventListener('DOMContentLoaded', async () => {
+ const selection = document.getSelection()
+ document.designMode = 'on'
+ selection.collapse(document, 0)
+ document.execCommand('forwardDelete', false, null)
+ })
+ </script>
+</head>
+<body>
+<aside style="position:fixed;top:0px;right:0px;font-family:&quot;Lucida Console&quot;,monospace;background-color:#f2e6d9;padding:3px;z-index:10000;text-align:center;max-width:120px;opacity:0;transition:opacity linear;"></aside></body></html>
+<!-- COMMENT --> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/1663725.html b/editor/libeditor/crashtests/1663725.html
new file mode 100644
index 0000000000..7f4ffd62eb
--- /dev/null
+++ b/editor/libeditor/crashtests/1663725.html
@@ -0,0 +1,18 @@
+<script>
+window.onload = () => {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <body> (at the text node after the <input>).
+ getSelection().collapse(document.body, document.body.childNodes.length);
+ document.execCommand("insertHorizontalRule");
+ getSelection().collapse(
+ document.querySelector("b")
+ );
+ document.execCommand("forwardDelete");
+}
+function onFocusChangeOfInput() {
+ document.getSelection().setPosition(document.querySelector("pre"));
+}
+</script>
+<pre>
+<time contenteditable>a|</t>
+<input onfocus="onFocusChangeOfInput()" autofocus onblur="onFocusChangeOfInput()">
diff --git a/editor/libeditor/crashtests/1666556.html b/editor/libeditor/crashtests/1666556.html
new file mode 100644
index 0000000000..8c8c18574a
--- /dev/null
+++ b/editor/libeditor/crashtests/1666556.html
@@ -0,0 +1,28 @@
+<script>
+function onError() {
+ document.querySelector("details").appendChild(
+ document.querySelector("p")
+ );
+ document.execCommand("indent");
+}
+
+function onLoadOfStyle() {
+ document.execCommand("delete");
+ document.querySelector("details").contentEditable = "true";
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node at end of the <style> (end of the text node after the comment
+ // node).
+ const style = document.querySelector("style");
+ getSelection().collapse(style.lastChild, style.lastChild.length);
+ document.querySelector("input").select();
+}
+</script>
+<video focus="false">
+<source onerror="onError()">
+</video>
+<details open>
+<p>
+<input contenteditable="false">
+<style onload="onLoadOfStyle()">
+<!-- x -->
+</style></p></details></body>
diff --git a/editor/libeditor/crashtests/1677566.html b/editor/libeditor/crashtests/1677566.html
new file mode 100644
index 0000000000..1546bb4601
--- /dev/null
+++ b/editor/libeditor/crashtests/1677566.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+document.addEventListener('DOMContentLoaded', () => {
+ const table = document.querySelector("table");
+ // For emulating traditional behavior, collapse Selection to end of the
+ // text node after the <p> (<table> does not close the <p>).
+ getSelection().collapse(
+ document.body.lastChild,
+ document.body.lastChild.length
+ );
+ const paragraph = document.querySelector("p");
+ document.documentElement.contentEditable = true;
+ getSelection().setBaseAndExtent(document, 0, document.documentElement, 1);
+ paragraph.contentEditable = false;
+ table.insertRow(0);
+ document.execCommand("forwardDelete");
+});
+</script>
+</head>
+<p>
+ <del>
+ <button contenteditable>
+ </button>
+ <table>
+ </table>
+ </del>
+</p>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/1691051.html b/editor/libeditor/crashtests/1691051.html
new file mode 100644
index 0000000000..29aeae551c
--- /dev/null
+++ b/editor/libeditor/crashtests/1691051.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ window.addEventListener('load', () => {
+ document.documentElement.contentEditable = true
+ document.execCommand('selectAll', false, null)
+ document.addEventListener('DOMNodeRemoved', async (e) => {
+ e.currentTarget.writeln()
+ }, {})
+ document.execCommand('insertLineBreak', false, null)
+ })
+ </script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/1699866.html b/editor/libeditor/crashtests/1699866.html
new file mode 100644
index 0000000000..f8d700ec9c
--- /dev/null
+++ b/editor/libeditor/crashtests/1699866.html
@@ -0,0 +1,22 @@
+<script>
+window.onload = () => {
+ const font = document.querySelector("font");
+ // For emulating traditional behavior, collapse Selection to end of the
+ // text node in the <font>.
+ getSelection().collapse(font.lastChild, font.lastChild.length);
+ const meta = document.querySelector("meta");
+ meta.style.setProperty(
+ "text-decoration",
+ "overline underline line-through"
+ );
+ meta.appendChild(font);
+ document.execCommand("selectAll");
+ getSelection().extend(meta, 0);
+ document.execCommand("underline");
+}
+</script>
+<ins contenteditable>
+a
+<meta></meta>
+<font>
+</font></ins></body>
diff --git a/editor/libeditor/crashtests/1701348.html b/editor/libeditor/crashtests/1701348.html
new file mode 100644
index 0000000000..1e8c2fbaa7
--- /dev/null
+++ b/editor/libeditor/crashtests/1701348.html
@@ -0,0 +1,10 @@
+<style>
+.c { resize: both }
+</style>
+<script>
+window.onload = () => {
+ a.className = "a"
+ document.execCommand("delete", false)
+}
+</script>
+<input id="a" autofocus="autofocus" class="c">
diff --git a/editor/libeditor/crashtests/1707630.html b/editor/libeditor/crashtests/1707630.html
new file mode 100644
index 0000000000..e62df20b1b
--- /dev/null
+++ b/editor/libeditor/crashtests/1707630.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ window.addEventListener('load', () => {
+ document = document
+ document.documentElement = document.documentElement
+ document.addEventListener('DOMNodeInserted', (e) => {
+ e.currentTarget.removeChild(e.currentTarget.childNodes[(2731378636 % e.currentTarget.childNodes.length)])
+ }, {})
+ document.documentElement.contentEditable = true
+ document.execCommand('insertParagraph', false, null)
+ })
+ </script>
+</head>
+</html>
diff --git a/editor/libeditor/crashtests/336081-1.xhtml b/editor/libeditor/crashtests/336081-1.xhtml
new file mode 100644
index 0000000000..5a499d9f51
--- /dev/null
+++ b/editor/libeditor/crashtests/336081-1.xhtml
@@ -0,0 +1,52 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+<head>
+<script>
+<![CDATA[
+
+function foop(targetWindow)
+{
+ var targetDocument = targetWindow.document;
+
+ var r1 = targetDocument.createRange();
+ r1.setStart(targetDocument.getElementById("out1"), 0);
+ r1.setEnd (targetDocument.getElementById("out2"), 0);
+ targetWindow.getSelection().addRange(r1);
+
+ var r2 = targetDocument.createRange();
+ r2.setStart(targetDocument.getElementById("in1"), 0);
+ r2.setEnd (targetDocument.getElementById("in2"), 0);
+ targetWindow.getSelection().addRange(r2);
+
+ targetDocument.execCommand('removeformat', false, null);
+ targetDocument.execCommand('outdent', false, null);
+}
+
+function init()
+{
+ setTimeout(function()
+ {
+ var fd = window.frames[0].document;
+ fd.body.appendChild(fd.importNode(document.getElementById('rootish'), true));
+ fd.designMode = 'on';
+ foop(window.frames[0]);
+ document.documentElement.removeAttribute("class");
+ }, 100);
+}
+
+]]>
+</script>
+</head>
+
+<body onload="init()">
+
+<iframe srcdoc="<html></html>" style="width: 95%; height: 500px;"/>
+
+<div id="rootish">
+<div id="out1"/>
+<div id="in1"/>
+<div id="in2"/>
+<div id="out2"/>
+</div>
+
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/403965-1.xhtml b/editor/libeditor/crashtests/403965-1.xhtml
new file mode 100644
index 0000000000..02993914d9
--- /dev/null
+++ b/editor/libeditor/crashtests/403965-1.xhtml
@@ -0,0 +1,7 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<tbody contenteditable="true"/>
+<frameset/>
+<xul:box onbeforecopy="event.explicitOriginalTarget.parentNode.parentNode.removeChild(event.explicitOriginalTarget.parentNode)">
+<xul:box/>
+</xul:box>
+</html> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/428489-1.html b/editor/libeditor/crashtests/428489-1.html
new file mode 100644
index 0000000000..8eec1268b9
--- /dev/null
+++ b/editor/libeditor/crashtests/428489-1.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Crash [@ nsHTMLEditor::GetPositionAndDimensions] when window gets removed during click on contenteditable absolute positioned element</title>
+</head>
+<body>
+<iframe id="content" src="data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%0A%3Cscript%3E%0Awindow.addEventListener%28%27DOMAttrModified%27%2C%20function%28e%29%20%7Bdump%28%27DOMAttrModified\n%27%29%3Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%3B%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/head%3E%0A%3Cbody%3E%0A%3Cdiv%20style%3D%22position%3A%20absolute%3B%22%20contenteditable%3D%22true%22%3EClicking%20on%20this%20should%20not%20crash%20Mozilla%0A%3C/body%3E%0A%3C/html%3E" style="width:1000px;height: 300px;"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/429586-1.html b/editor/libeditor/crashtests/429586-1.html
new file mode 100644
index 0000000000..a32df3b721
--- /dev/null
+++ b/editor/libeditor/crashtests/429586-1.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Bug 429586 - Crash [@ nsEditor::EndUpdateViewBatch] with pasting and domattrmodified removing iframe</title>
+</head>
+<body>
+<iframe id="content" src="data:text/html;charset=utf-8,%3Chtml%3E%0A%3Chead%3E%0A%3C/head%3E%0A%3Cbody%20contenteditable%3D%22true%22%3E%0A%0A%3Cscript%3E%0Afunction%20dokey%28%29%7B%0Adocument.body.focus%28%29%3B%0Adocument.execCommand%28%27insertParagraph%27%2C%20false%2C%20%27%27%29%3B%0A%7D%0AsetTimeout%28dokey%2C200%29%3B%0A%0Adocument.addEventListener%28%27DOMAttrModified%27%2C%20function%28%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/body%3E%0A%3C/html%3E"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/crashtests/431086-1.xhtml b/editor/libeditor/crashtests/431086-1.xhtml
new file mode 100644
index 0000000000..c6c5d8d992
--- /dev/null
+++ b/editor/libeditor/crashtests/431086-1.xhtml
@@ -0,0 +1,22 @@
+<div xmlns="http://www.w3.org/1999/xhtml">
+
+<script type="text/javascript">
+
+function boom()
+{
+ var r = document.documentElement;
+ r.style.position = "absolute";
+ r.contentEditable = "true";
+ r.focus();
+ r.contentEditable = "false";
+ r.focus();
+ r.contentEditable = "true";
+ document.execCommand("subscript", false, null);
+ r.contentEditable = "false";
+}
+
+window.addEventListener("load", boom, false);
+
+</script>
+
+</div>
diff --git a/editor/libeditor/crashtests/475132-1.xhtml b/editor/libeditor/crashtests/475132-1.xhtml
new file mode 100644
index 0000000000..e721c55639
--- /dev/null
+++ b/editor/libeditor/crashtests/475132-1.xhtml
@@ -0,0 +1,21 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script type="text/javascript">
+function onLoad() {
+ document.querySelector("td").contentEditable = "true";
+ getSelection().collapse(
+ document.querySelector("td"),
+ document.querySelector("td").childNodes.length
+ );
+ document.querySelector("td").focus();
+ document.documentElement.contentEditable = "true";
+ document.documentElement.focus();
+ document.execCommand("indent");
+ document.execCommand("insertParagraph");
+}
+</script>
+</head>
+
+<body onload="onLoad();" contenteditable="false"><td></td></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/503709-1.xhtml b/editor/libeditor/crashtests/503709-1.xhtml
new file mode 100644
index 0000000000..867bebf1a9
--- /dev/null
+++ b/editor/libeditor/crashtests/503709-1.xhtml
@@ -0,0 +1,11 @@
+<html contenteditable="true" xmlns="http://www.w3.org/1999/xhtml"><head><script>
+
+function boom()
+{
+ document.execCommand("selectAll", false, "");
+ try { document.execCommand("justifyfull", false, null); } catch(e) { }
+ try { document.execCommand("inserthorizontalrule", false, "false"); } catch(e) { }
+ document.execCommand("delete", false, null);
+}
+
+</script></head>x y z<body onload="boom();"><div/></body></html>
diff --git a/editor/libeditor/crashtests/513375-1.xhtml b/editor/libeditor/crashtests/513375-1.xhtml
new file mode 100644
index 0000000000..4db5a42159
--- /dev/null
+++ b/editor/libeditor/crashtests/513375-1.xhtml
@@ -0,0 +1,19 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head contenteditable="true">
+<script type="text/javascript">
+<![CDATA[
+function onLoad() {
+ getSelection().collapse(document.body, document.body.childNodes.length);
+ const r = document.createRange();
+ r.selectNode(document.body);
+ r.deleteContents();
+ try {
+ document.execCommand("selectAll");
+ } catch(e) {}
+}
+]]>
+</script>
+</head>
+
+<body onload="onLoad();" contenteditable="true"></body>
+</html>
diff --git a/editor/libeditor/crashtests/535632-1.xhtml b/editor/libeditor/crashtests/535632-1.xhtml
new file mode 100644
index 0000000000..92470b8258
--- /dev/null
+++ b/editor/libeditor/crashtests/535632-1.xhtml
@@ -0,0 +1 @@
+<body xmlns="http://www.w3.org/1999/xhtml" style="margin: 200px;" contenteditable="true" onload="document.execCommand('outdent', false, null);" /> \ No newline at end of file
diff --git a/editor/libeditor/crashtests/574558-1.xhtml b/editor/libeditor/crashtests/574558-1.xhtml
new file mode 100644
index 0000000000..4e61ad02e7
--- /dev/null
+++ b/editor/libeditor/crashtests/574558-1.xhtml
@@ -0,0 +1,15 @@
+<html xmlns="http://www.w3.org/1999/xhtml"><head><script>
+<![CDATA[
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // text node in the <textarea> which is the deepest last child of the <body>.
+ const textarea = document.querySelector("textarea");
+ getSelection().collapse(textarea.lastChild, textarea.lastChild.length);
+ document.execCommand("selectAll");
+ document.execCommand("selectAll");
+ document.execCommand("inserthtml", false, "<span><div>");
+ const span = document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ textarea.appendChild(span);
+}
+]]>
+</script></head><div contenteditable="true"></div><body onload="onLoad();"><textarea>f</textarea></body></html>
diff --git a/editor/libeditor/crashtests/580151-1.xhtml b/editor/libeditor/crashtests/580151-1.xhtml
new file mode 100644
index 0000000000..3799411117
--- /dev/null
+++ b/editor/libeditor/crashtests/580151-1.xhtml
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+var t;
+
+function boom()
+{
+ var b = document.createElementNS("http://www.w3.org/1999/xhtml", "body");
+ t = document.createElementNS("http://www.w3.org/1999/xhtml", "textarea");
+ b.appendChild(t);
+ document.removeChild(document.documentElement)
+ document.appendChild(b)
+ document.removeChild(document.documentElement)
+ var ns = document.createElementNS("http://www.w3.org/1999/xhtml", "script");
+ var nt = document.createTextNode("t.appendChild(document.createTextNode(' '));");
+ ns.appendChild(nt);
+ b.appendChild(ns);
+ document.appendChild(b);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/582138-1.xhtml b/editor/libeditor/crashtests/582138-1.xhtml
new file mode 100644
index 0000000000..afcec2eba1
--- /dev/null
+++ b/editor/libeditor/crashtests/582138-1.xhtml
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml"><mtr xmlns="http://www.w3.org/1998/Math/MathML"><td id="cell" xmlns="http://www.w3.org/1999/xhtml"></td></mtr><script>
+function boom()
+{
+ document.getElementById("cell").contentEditable = true;
+ document.getElementById("cell").focus();
+ document.execCommand("inserthtml", false, "x");
+}
+
+window.addEventListener("load", boom, false);
+</script></html>
diff --git a/editor/libeditor/crashtests/633709.xhtml b/editor/libeditor/crashtests/633709.xhtml
new file mode 100644
index 0000000000..75ad518b8f
--- /dev/null
+++ b/editor/libeditor/crashtests/633709.xhtml
@@ -0,0 +1,68 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+
+<body><div contenteditable="true"></div><div><input><div></div></input></div></body>
+
+<script>
+<![CDATA[
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // parent <div> of the <input>. In XHTML document, the <input> may have the
+ // <div> child. Therefore, the deepest last child container element of the
+ // <body> is the parent of the <input>.
+ getSelection().collapse(
+ document.querySelector("input").parentElement,
+ document.querySelector("input").parentElement.childNodes.length
+ );
+ document.querySelector("input").focus();
+
+ try {
+ document.execCommand("stylewithcss", false, "true");
+ } catch(e) {}
+ try {
+ document.execCommand("inserthtml", false, "<x>X</x>");
+ } catch(e) {}
+ try {
+ document.execCommand("underline");
+ } catch(e) {}
+ try {
+ document.execCommand("justifyfull");
+ } catch(e) {}
+ try {
+ document.execCommand("underline");
+ } catch(e) {}
+ try {
+ document.execCommand("insertParagraph");
+ } catch(e) {}
+ try {
+ document.execCommand("delete");
+ } catch(e) {}
+
+ try {
+ document.execCommand("stylewithcss", false, "false");
+ } catch(e) {}
+ try {
+ document.execCommand("inserthtml", false, "<x>X</x>");
+ } catch(e) {}
+ try {
+ document.execCommand("underline");
+ } catch(e) {}
+ try {
+ document.execCommand("justifyfull");
+ } catch(e) {}
+ try {
+ document.execCommand("underline");
+ } catch(e) {}
+ try {
+ document.execCommand("insertParagraph");
+ } catch(e) {}
+ try {
+ document.execCommand("delete");
+ } catch(e) {}
+
+ document.documentElement.removeAttribute("class");
+}
+addEventListener("load", onLoad);
+]]>
+</script>
+
+</html>
diff --git a/editor/libeditor/crashtests/639736-1.xhtml b/editor/libeditor/crashtests/639736-1.xhtml
new file mode 100644
index 0000000000..1aea3040e0
--- /dev/null
+++ b/editor/libeditor/crashtests/639736-1.xhtml
@@ -0,0 +1,19 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+function onLoad() {
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // <td> which is the deepest last child of the <body>.
+ getSelection().collapse(
+ document.querySelector("td"),
+ document.querySelector("td").childNodes.length
+ );
+ try {
+ document.execCommand("removeformat");
+ } catch(e) {}
+ document.adoptNode(document.documentElement);
+}
+</script>
+</head>
+<body onload="onLoad();"><td contenteditable="true"/></body>
+</html>
diff --git a/editor/libeditor/crashtests/713427-2.xhtml b/editor/libeditor/crashtests/713427-2.xhtml
new file mode 100644
index 0000000000..39ac18ed07
--- /dev/null
+++ b/editor/libeditor/crashtests/713427-2.xhtml
@@ -0,0 +1,28 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+<![CDATA[
+
+function boom()
+{
+ while (document.documentElement.firstChild) {
+ document.documentElement.firstChild.remove();
+ }
+
+ var td = document.createElementNS("http://www.w3.org/1999/xhtml", "td");
+ td.setAttributeNS(null, "contenteditable", "true");
+ (document.documentElement).appendChild(td);
+ var head = document.createElementNS("http://www.w3.org/1999/xhtml", "head");
+ (document.documentElement).appendChild(head);
+
+ head.appendChild(td);
+}
+
+window.addEventListener("load", boom);
+
+]]>
+</script>
+</head>
+
+<body></body>
+</html>
diff --git a/editor/libeditor/crashtests/766360.html b/editor/libeditor/crashtests/766360.html
new file mode 100644
index 0000000000..76c30456d6
--- /dev/null
+++ b/editor/libeditor/crashtests/766360.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var r = document.createRange();
+ r.setEnd(document.createTextNode("x"), 0);
+ window.getSelection().addRange(r);
+ document.execCommand("inserthtml", false, "y");
+}
+
+</script>
+</head>
+
+<body contenteditable="true" onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/766387.html b/editor/libeditor/crashtests/766387.html
new file mode 100644
index 0000000000..a819bf7f89
--- /dev/null
+++ b/editor/libeditor/crashtests/766387.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function onLoad() {
+ const editingHost = document.querySelectorAll("div[contenteditable]");
+ // For emulating the traditional behavior, collapse Selection to end of the
+ // last <div contenteditable> which is the deepest last child of the <body>.
+ getSelection().collapse(editingHost[1], editingHost[1].childNodes.length);
+ getSelection().removeAllRanges();
+ const r = document.createRange();
+ r.setStart(editingHost[0], 1);
+ r.setEnd(editingHost[1], 0);
+ getSelection().addRange(r);
+ document.execCommand("insertOrderedList");
+}
+</script>
+</head>
+
+<body onload="onLoad();"><div contenteditable>a</div><div contenteditable></div></body>
+</html>
diff --git a/editor/libeditor/crashtests/766413.html b/editor/libeditor/crashtests/766413.html
new file mode 100644
index 0000000000..1a7092d92a
--- /dev/null
+++ b/editor/libeditor/crashtests/766413.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var root = document.documentElement;
+ while (root.firstChild) {
+ root.firstChild.remove();
+ }
+
+ var space = document.createTextNode(" ");
+ var body = document.createElementNS("http://www.w3.org/1999/xhtml", "body");
+ root.contentEditable = "true";
+ root.focus();
+ document.execCommand("contentReadOnly", false, null);
+ root.appendChild(body);
+ root.contentEditable = "false";
+ root.appendChild(space);
+ root.removeChild(body);
+ root.contentEditable = "true";
+
+ window.getSelection().removeAllRanges();
+ var r1 = document.createRange();
+ r1.setStart(root, 0);
+ r1.setEnd(root, 0);
+ window.getSelection().addRange(r1);
+ looseText = document.createTextNode("c");
+ var r2 = document.createRange();
+ r2.setStart(looseText, 0);
+ r2.setEnd(looseText, 0);
+ window.getSelection().addRange(r2);
+
+ document.execCommand("forwardDelete", false, null);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/766795.html b/editor/libeditor/crashtests/766795.html
new file mode 100644
index 0000000000..b4ade30209
--- /dev/null
+++ b/editor/libeditor/crashtests/766795.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var fragEl = document.createElement("span");
+ fragEl.setAttribute("contenteditable", "true");
+ fragEl.setAttribute("style", "position: absolute;");
+
+ var frag = document.createDocumentFragment();
+ frag.appendChild(fragEl);
+
+ window.getSelection().selectAllChildren(fragEl);
+}
+
+</script>
+</head>
+
+<body contenteditable="true" onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/766845.xhtml b/editor/libeditor/crashtests/766845.xhtml
new file mode 100644
index 0000000000..409e210109
--- /dev/null
+++ b/editor/libeditor/crashtests/766845.xhtml
@@ -0,0 +1,27 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+<![CDATA[
+
+function boom()
+{
+ window.getSelection().removeAllRanges();
+ var r1 = document.createRange();
+ r1.setStart(document.body, 0);
+ r1.setEnd (document.body, 1);
+ window.getSelection().addRange(r1);
+ var r2 = document.createRange();
+ r2.setStart(document.body, 1);
+ r2.setEnd (document.body, 2);
+ window.getSelection().addRange(r2);
+ if (document.queryCommandEnabled("inserthtml"))
+ document.execCommand("inserthtml", false, "1");
+}
+
+]]>
+</script>
+</head>
+
+<body contenteditable="true" onload="boom();"><div></div><div></div></body>
+
+</html>
diff --git a/editor/libeditor/crashtests/767169.html b/editor/libeditor/crashtests/767169.html
new file mode 100644
index 0000000000..a7673bce62
--- /dev/null
+++ b/editor/libeditor/crashtests/767169.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+<script>
+
+// Document must not have a doctype to trigger the bug
+
+function boom()
+{
+ var root = document.documentElement;
+ while (root.firstChild) { root.firstChild.remove(); }
+ root.contentEditable = "true";
+ document.removeChild(root);
+ document.appendChild(root);
+ window.getSelection().collapse(root, 0);
+ window.getSelection().extend(document, 1);
+ document.removeChild(root);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/768748.html b/editor/libeditor/crashtests/768748.html
new file mode 100644
index 0000000000..09206dce3f
--- /dev/null
+++ b/editor/libeditor/crashtests/768748.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html contenteditable="true">
+<head>
+<script>
+
+function boom()
+{
+ var looseText = document.createTextNode("x");
+ window.getSelection().collapse(looseText, 0);
+ document.queryCommandState("insertorderedlist");
+}
+
+</script>
+</head>
+<body onload="setTimeout(boom, 0)"></body>
+</html>
diff --git a/editor/libeditor/crashtests/768765.html b/editor/libeditor/crashtests/768765.html
new file mode 100644
index 0000000000..551e4ec6c3
--- /dev/null
+++ b/editor/libeditor/crashtests/768765.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var root = document.documentElement;
+
+ while (root.firstChild) { root.firstChild.remove(); }
+
+ var body = document.createElementNS("http://www.w3.org/1999/xhtml", "body");
+ var div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ root.contentEditable = "true";
+ root.appendChild(div);
+ root.removeChild(div);
+ root.insertBefore(body, root.firstChild);
+
+ window.getSelection().removeAllRanges();
+ var r0 = document.createRange();
+ r0.setStart(body, 0);
+ r0.setEnd(body, 0);
+ window.getSelection().addRange(r0);
+ var r1 = document.createRange();
+ r1.setStart(div, 0);
+ r1.setEnd(div, 0);
+ window.getSelection().addRange(r1);
+
+ document.execCommand("inserthtml", false, "1");
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/769008-1.html b/editor/libeditor/crashtests/769008-1.html
new file mode 100644
index 0000000000..8ea8a3601d
--- /dev/null
+++ b/editor/libeditor/crashtests/769008-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var x = document.getElementById("x");
+
+ window.getSelection().removeAllRanges();
+
+ var range = document.createRange();
+ range.setStart(x, 0);
+ range.setEnd(x, 0);
+ window.getSelection().addRange(range);
+
+ document.execCommand("delete", false, "null");
+}
+
+</script>
+</head>
+<body contenteditable="true" onload="boom();"><div></div><span id="x"></span></body>
+</html>
diff --git a/editor/libeditor/crashtests/769967.xhtml b/editor/libeditor/crashtests/769967.xhtml
new file mode 100644
index 0000000000..af07571591
--- /dev/null
+++ b/editor/libeditor/crashtests/769967.xhtml
@@ -0,0 +1,16 @@
+<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true" style="user-select: all;"><sub>x</sub><script>
+function boom()
+{
+ window.getSelection().removeAllRanges();
+ var r = document.createRange();
+ r.setStart(document.documentElement, 0);
+ r.setEnd(document.documentElement, 0);
+ window.getSelection().addRange(r);
+
+ document.execCommand("subscript", false, null);
+ document.execCommand("insertText", false, "y");
+}
+
+window.addEventListener("load", boom, false);
+
+</script></html>
diff --git a/editor/libeditor/crashtests/771749.html b/editor/libeditor/crashtests/771749.html
new file mode 100644
index 0000000000..9237364f2d
--- /dev/null
+++ b/editor/libeditor/crashtests/771749.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var root = document.documentElement;
+ root.contentEditable = "true";
+ document.removeChild(root);
+ document.appendChild(root);
+ document.execCommand("insertunorderedlist", false, null);
+ document.execCommand("inserthtml", false, "<span></span>");
+ document.execCommand("outdent", false, null);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/772282.html b/editor/libeditor/crashtests/772282.html
new file mode 100644
index 0000000000..bba3d6bd67
--- /dev/null
+++ b/editor/libeditor/crashtests/772282.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var root = document.documentElement;
+ while(root.firstChild) { root.firstChild.remove(); }
+ var body = document.createElementNS("http://www.w3.org/1999/xhtml", "body");
+ body.setAttributeNS(null, "contenteditable", "true");
+ var img = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
+ body.appendChild(img);
+ root.appendChild(body);
+ document.removeChild(root);
+ document.appendChild(root);
+ document.execCommand("insertText", false, "5");
+ document.execCommand("selectAll", false, null);
+ document.execCommand("insertParagraph", false, null);
+ document.execCommand("increasefontsize", false, null);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/776323.html b/editor/libeditor/crashtests/776323.html
new file mode 100644
index 0000000000..9fc2776c37
--- /dev/null
+++ b/editor/libeditor/crashtests/776323.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html contenteditable="true">
+<head>
+<script>
+
+function boom()
+{
+ document.execCommand("inserthtml", false, "b");
+ var myrange = document.createRange();
+ myrange.selectNodeContents(document.getElementsByTagName("img")[0]);
+ window.getSelection().addRange(myrange);
+ document.execCommand("strikethrough", false, null);
+}
+
+</script>
+</head>
+<body onload="boom();"><img></body>
+</html>
diff --git a/editor/libeditor/crashtests/793866.html b/editor/libeditor/crashtests/793866.html
new file mode 100644
index 0000000000..4984474dbc
--- /dev/null
+++ b/editor/libeditor/crashtests/793866.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var b = document.body;
+ b.contentEditable = "true";
+ document.execCommand("contentReadOnly", false, null);
+ b.focus();
+ b.contentEditable = "false";
+ document.documentElement.contentEditable = "true";
+ document.createDocumentFragment().appendChild(b);
+ document.documentElement.focus();
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/editor/libeditor/crashtests/848644.html b/editor/libeditor/crashtests/848644.html
new file mode 100644
index 0000000000..95570fabe3
--- /dev/null
+++ b/editor/libeditor/crashtests/848644.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<iframe src="data:text/html;charset=utf-8;base64,PGh0bWw+CjxoZWFkPgoJPHNjcmlwdD4KCQoJCXZhciBuID0gMDsKCQkKICAgICAgICB2YXIgU3ByYXkgPSBbXTsgICAgICAgICAgICAKICAgICAgICB2YXIgblNwcmF5ID0gMHgxMDA7ICAgICAgICAKCiAgICAKICAgIGZ1bmN0aW9uIHB0clRvU3RyaW5nKHB0cikKICAgIHsKICAgICAgICB2YXIgdGVtcCA9IHB0ci50b1N0cmluZygxNik7CiAgICAgICAgd2hpbGUgKHRlbXAubGVuZ3RoIDwgOCkKICAgICAgICAgICAgdGVtcCA9ICIwIiArIHRlbXA7CiAgICAgICAgYWQgPSAiJXUiOwogICAgICAgIGZvciAodmFyIGk9MDsgaTw0OyBpKyspCiAgICAgICAgICAgIGFkID0gYWQgKyB0ZW1wLmNoYXJBdCg0K2kpOwogICAgICAgIGFkICs9ICIldSI7CiAgICAgICAgZm9yICh2YXIgaT0wOyBpPDQ7IGkrKykKICAgICAgICAgICAgYWQgPSBhZCArIHRlbXAuY2hhckF0KGkpOwogICAgICAgIHJldHVybiBhZDsKICAgIH0KCiAgICAgICBmdW5jdGlvbiBzcHJheU1lbW9yeSgpCiAgICB7CiAgICAgICAgdmFyIHBhZ2Vfc2l6ZSA9IDB4MTAwMDAwIC0gMHgxMDAwOwogICAgICAgIHZhciBibG9jayA9ICIiOwoKICAgICAgICB3aGlsZSAoYmxvY2subGVuZ3RoICogMiA8IDB4MEM0KzB4MjAyMCkKICAgICAgICAgICAgYmxvY2sgKz0gcGFjaygweDkwOTA5MDkwKTsKICAgICAgICAKICAgICAgICB3aGlsZSAoYmxvY2subGVuZ3RoICogMiA8IHBhZ2Vfc2l6ZSkKICAgICAgICAgICAgYmxvY2sgKz0gIlx1OTA5MFx1OTA5MCI7CiAgICAgICAgICAgIAogICAgICAgIHZhciBzcHIgPSBibG9jay5zdWJzdHJpbmcoMCwgcGFnZV9zaXplIC8gMik7CiAgICAgICAgCiAgICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCBuU3ByYXk7IGkrKykKICAgICAgICAgICAgU3ByYXlbaV0gPSBbc3ByXS5qb2luKCIiKTsKICAgIH0KICAgICAgICAKCQlmdW5jdGlvbiBwYWNrKGEpCgkJewoJCQlyZXR1cm4gU3RyaW5nLmZyb21DaGFyQ29kZShhICYgMHhGRkZGKSArIFN0cmluZy5mcm9tQ2hhckNvZGUoYSA+PiAxNik7CgkJfQoJCQoJCXZhciAkID0gZnVuY3Rpb24oaWQpIHsgcmV0dXJuIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlkKTsgfQoJCQoJCXZhciBhbGxvYyA9IGZ1bmN0aW9uKHN6LCBwYXR0ZXJuPSdcdTQxNDEnKQoJCXsKCQkJc3ogLT0gMjsKCQkJaWYgKHN6IDwgMCkKCQkJCXJldHVybiBudWxsOwoJCQkKCQkJdmFyIHN0ciA9IHBhdHRlcm47CgkJCXdoaWxlIChzdHIubGVuZ3RoICogMiA8IHN6KQoJCQkJc3RyICs9IHBhdHRlcm47CgkJCgkJCXJldHVybiBzdHIuc3Vic3RyaW5nKDAsIHN6LzIpOwoJCX0KCQkKCQlmdW5jdGlvbiBhcHBlbmRJbnB1dChuLCB3aXRoSWRzPWZhbHNlKSAKCQl7CgkJCXZhciBib2R5ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiYm9keSIpCgkJCXZhciBibG9jayA9IHBhY2soMHgyMDIwMjAyMCk7CgkJCXdoaWxlIChibG9jay5sZW5ndGggKiAyIDwgMHg1NCkKCQkJCWJsb2NrICs9IHBhY2soMHgyMDIwMjAyMCk7CgkJCWJsb2NrICs9IHBhY2soMHgyMDIwMjAyMCk7CgkJCXdoaWxlIChibG9jay5sZW5ndGggKiAyIDwgMHhDNCkKCQkJCWJsb2NrICs9IHBhY2soMHgyMDIwMjAyMCk7CgkJCWJsb2NrICs9IHBhY2soMHgyMDIwMjAyMCk7CgkJCXdoaWxlIChibG9jay5sZW5ndGggKiAyIDwgMHgyRDQgLSAyKQoJCQkJYmxvY2sgKz0gcGFjaygweDIwMjAyMDIwKTsKCQkgICAgdmFyIGIgPSBibG9jay5zdWJzdHJpbmcoMCwgMHgyRDIpOwoJCQlmb3IgKHZhciBpID0wOyBpPG47IGkrKykgCgkJCXsKCQkJCXZhciBpbnB1dCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2lucHV0JykKCQkJCWlmICh3aXRoSWRzID09IHRydWUpCgkJCQkJaW5wdXQuc2V0QXR0cmlidXRlKCdpZCcsICdpbnB1dCcgKyBpKTsKCQkJCWlucHV0LnZhbHVlID0gYgoJCQkJZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChpbnB1dCkKCQkJfQoJCX0KCQkKCQlmdW5jdGlvbiBjcmFmdEhlYXAobikKCQl7CgkJCWFwcGVuZElucHV0KG4sIHRydWUpOwoJCQlmb3IgKHZhciBpID0gMDsgaSA8IG47IGkgKz0gMikKCQkJCWRvY3VtZW50LmJvZHkucmVtb3ZlQ2hpbGQoJCgnaW5wdXQnICsgaSkpOwoJCX0KCQkKCQlmdW5jdGlvbiBzdGFydEZ1bigpCgkJewogICAgICAgICAgICAgICAgICAgICAgICBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7CgkJCWNyYWZ0SGVhcCgweDEwKTsKCQkJZG9jdW1lbnQuZGVzaWduTW9kZSA9ICJvbiI7CgkJCWRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5zdHlsZS5oZWlnaHQgPSAiMjAwcHgiOwoJCQkkKCdpZDEnKS5vZmZzZXRUb3A7CgkJfQoJCQoJCQoJCWZ1bmN0aW9uIGNhbGNNZUJhYmUoKQoJCXsKCQkJbisrOwoJCQlkb2N1bWVudC5leGVjQ29tbWFuZCgiaW5zZXJ0T3JkZXJlZExpc3QiLGZhbHNlLHRydWUpOwoJCQlkb2N1bWVudC53cml0ZSgnQUFBQUFBQUFBQUFBJyk7CQkKICAgICAgICAgICAgc3ByYXlNZW1vcnkoKTsKCQkJYXBwZW5kSW5wdXQoMHgxMDApOwoJCQkKCQkJZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LnN0eWxlLmhlaWdodCA9ICIzMDBweCI7CgkJCWlmIChuID09IDIpCgkJCXsKICAgICAgICAgICAgICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKCJyZXNpemUiLCBjYWxjTWVCYWJlLCBmYWxzZSk7CgkJCQloaXN0b3J5LmdvKC0xKTsKCQkJfQoKCQl9CgkJd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoInJlc2l6ZSIsIGNhbGNNZUJhYmUsIGZhbHNlKTsKCTwvc2NyaXB0Pgo8L2hlYWQ+Cgk8Ym9keT4KCQk8ZGl2IGlkPSdpZDEnPiAgICA8L2Rpdj4KCQk8c2NyaXB0PnN0YXJ0RnVuKCk7PC9zY3JpcHQ+Cgk8L2JvZHk+CjwvaHRtbD4K" style="width: 100px; height: 100px"></iframe>
diff --git a/editor/libeditor/crashtests/crashtests.list b/editor/libeditor/crashtests/crashtests.list
new file mode 100644
index 0000000000..bc9b7a3f2d
--- /dev/null
+++ b/editor/libeditor/crashtests/crashtests.list
@@ -0,0 +1,117 @@
+load 336081-1.xhtml
+load 403965-1.xhtml
+load 428489-1.html
+load 429586-1.html
+load 431086-1.xhtml
+load 475132-1.xhtml
+load 503709-1.xhtml
+load 513375-1.xhtml
+load 535632-1.xhtml
+load 574558-1.xhtml
+load 580151-1.xhtml
+load 582138-1.xhtml
+load 633709.xhtml
+load 639736-1.xhtml
+load 713427-2.xhtml
+load 766360.html
+load 766387.html
+load 766413.html
+load 766795.html
+load 766845.xhtml
+load 767169.html
+load 768748.html
+load 768765.html
+load 769008-1.html
+load 769967.xhtml
+needs-focus load 771749.html
+load 772282.html
+load 776323.html
+needs-focus load 793866.html
+load 848644.html
+load 1057677.html
+needs-focus load 1128787.html
+load 1134545.html
+load 1158452.html
+load 1158651.html
+load 1244894.xhtml
+load 1264921.html
+load 1272490.html
+load 1274050.html
+load 1317704.html
+load 1317718.html
+load 1324505.html
+needs-focus load 1343918.html
+load 1344097.html
+load 1345015.html
+load 1348851.html
+load 1350772.html
+load 1364133.html
+load 1366176.html
+load 1375131.html
+load 1381541.html
+load 1383747.html
+load 1383755.html
+load 1383763.html
+load 1384161.html
+load 1388075.html
+load 1393171.html
+needs-focus load 1402196.html
+load 1402469.html
+load 1402526.html
+pref(dom.document.exec_command.nested_calls_allowed,true) asserts(1) load 1402904.html # assertion is that mutation event listener caused by execCommand calls another execCommand
+pref(dom.document.exec_command.nested_calls_allowed,true) asserts(1) load 1405747.html # assertion is that mutation event listener caused by execCommand calls another execCommand
+load 1405897.html
+load 1408170.html
+asserts(0-1) load 1414581.html
+load 1415231.html
+load 1423767.html
+needs-focus load 1423776.html
+skip-if(ThreadSanitizer) needs-focus load 1424450.html # bug 1718775, permafail on tsan
+load 1425091.html
+load 1426709.html
+needs-focus load 1429523.html
+needs-focus load 1429523.xhtml
+load 1441619.html
+load 1443664.html
+skip-if(Android) needs-focus load 1444630.html
+load 1446451.html
+pref(dom.document.exec_command.nested_calls_allowed,true) load 1464251.html
+pref(layout.accessiblecaret.enabled,true) load 1470926.html
+pref(dom.document.exec_command.nested_calls_allowed,true) asserts(2) load 1474978.html # assertion is that mutation event listener caused by execCommand calls another execCommand
+load 1517028.html
+load 1525481.html
+load 1533913.html
+load 1534394.html
+load 1547897.html
+load 1547898.html
+load 1556799.html
+load 1579934.html
+load 1574544.html
+load 1578916.html
+load 1581246.html
+load 1596516.html
+load 1605741.html
+load 1613521.html
+load 1618906.html
+load 1623166.html
+load 1623913.html
+load 1624005.html # throws
+load 1624007.html
+load 1624011.html
+load 1626002.html
+load 1636541.html
+load 1644903.html
+load 1645983-1.html
+load 1645983-2.html
+load 1648564.html
+load 1655539.html
+load 1659717.html
+load 1663725.html # throws
+load 1655508.html
+load 1655988.html
+pref(dom.document.exec_command.nested_calls_allowed,true) load 1666556.html
+load 1677566.html
+load 1691051.html
+load 1699866.html
+load 1701348.html
+load 1707630.html
diff --git a/editor/libeditor/moz.build b/editor/libeditor/moz.build
new file mode 100644
index 0000000000..a28c2c7100
--- /dev/null
+++ b/editor/libeditor/moz.build
@@ -0,0 +1,98 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += [
+ "tests/browserscope/mochitest.toml",
+ "tests/mochitest.toml",
+]
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.toml"]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
+
+EXPORTS.mozilla += [
+ "EditAction.h",
+ "EditorBase.h",
+ "EditorCommands.h",
+ "EditorController.h",
+ "EditorDOMPoint.h",
+ "EditorForwards.h",
+ "EditTransactionBase.h",
+ "HTMLEditor.h",
+ "HTMLEditorController.h",
+ "ManualNAC.h",
+ "PendingStyles.h",
+ "SelectionState.h",
+ "TextEditor.h",
+]
+
+UNIFIED_SOURCES += [
+ "AutoRangeArray.cpp",
+ "ChangeAttributeTransaction.cpp",
+ "ChangeStyleTransaction.cpp",
+ "CompositionTransaction.cpp",
+ "CSSEditUtils.cpp",
+ "DeleteContentTransactionBase.cpp",
+ "DeleteMultipleRangesTransaction.cpp",
+ "DeleteNodeTransaction.cpp",
+ "DeleteRangeTransaction.cpp",
+ "DeleteTextTransaction.cpp",
+ "EditAggregateTransaction.cpp",
+ "EditorBase.cpp",
+ "EditorCommands.cpp",
+ "EditorController.cpp",
+ "EditorEventListener.cpp",
+ "EditorUtils.cpp",
+ "EditTransactionBase.cpp",
+ "HTMLAbsPositionEditor.cpp",
+ "HTMLAnonymousNodeEditor.cpp",
+ "HTMLEditHelpers.cpp",
+ "HTMLEditor.cpp",
+ "HTMLEditorCommands.cpp",
+ "HTMLEditorController.cpp",
+ "HTMLEditorDataTransfer.cpp",
+ "HTMLEditorDeleteHandler.cpp",
+ "HTMLEditorDocumentCommands.cpp",
+ "HTMLEditorEventListener.cpp",
+ "HTMLEditorObjectResizer.cpp",
+ "HTMLEditorState.cpp",
+ "HTMLEditSubActionHandler.cpp",
+ "HTMLEditUtils.cpp",
+ "HTMLInlineTableEditor.cpp",
+ "HTMLStyleEditor.cpp",
+ "HTMLTableEditor.cpp",
+ "InsertNodeTransaction.cpp",
+ "InsertTextTransaction.cpp",
+ "InternetCiter.cpp",
+ "JoinNodesTransaction.cpp",
+ "MoveNodeTransaction.cpp",
+ "PendingStyles.cpp",
+ "PlaceholderTransaction.cpp",
+ "ReplaceTextTransaction.cpp",
+ "SelectionState.cpp",
+ "SplitNodeTransaction.cpp",
+ "TextEditor.cpp",
+ "TextEditorDataTransfer.cpp",
+ "TextEditSubActionHandler.cpp",
+ "WSRunObject.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/dom/html",
+ "/extensions/spellcheck/src",
+ "/layout/generic",
+ "/layout/style",
+ "/layout/tables",
+ "/layout/xul",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+with Files("tests/*1151186*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
diff --git a/editor/libeditor/tests/browser.toml b/editor/libeditor/tests/browser.toml
new file mode 100644
index 0000000000..ac18b6eefc
--- /dev/null
+++ b/editor/libeditor/tests/browser.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+
+["browser_bug527935.js"]
+support-files = [
+ "bug527935.html",
+ "bug527935_2.html"
+]
+
+["browser_content_command_insert_text.js"]
diff --git a/editor/libeditor/tests/browser_bug527935.js b/editor/libeditor/tests/browser_bug527935.js
new file mode 100644
index 0000000000..cece783491
--- /dev/null
+++ b/editor/libeditor/tests/browser_bug527935.js
@@ -0,0 +1,78 @@
+add_task(async function () {
+ await new Promise(resolve => waitForFocus(resolve, window));
+
+ const kPageURL =
+ "http://example.org/browser/editor/libeditor/tests/bug527935.html";
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: kPageURL,
+ },
+ async function (aBrowser) {
+ var popupShown = false;
+ function listener() {
+ popupShown = true;
+ }
+ SpecialPowers.addAutoCompletePopupEventListener(
+ window,
+ "popupshowing",
+ listener
+ );
+
+ await SpecialPowers.spawn(aBrowser, [], async function () {
+ var window = content.window.wrappedJSObject;
+ var document = window.document;
+ var formTarget = document.getElementById("formTarget");
+ var initValue = document.getElementById("initValue");
+
+ window.loadPromise = new Promise(resolve => {
+ formTarget.onload = resolve;
+ });
+
+ initValue.focus();
+ initValue.value = "foo";
+ });
+
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await SpecialPowers.spawn(aBrowser, [], async function () {
+ var window = content.window.wrappedJSObject;
+ var document = window.document;
+
+ await window.loadPromise;
+
+ var newInput = document.createElement("input");
+ newInput.setAttribute("name", "test");
+ document.body.appendChild(newInput);
+
+ var event = new window.KeyboardEvent("keypress", {
+ bubbles: true,
+ cancelable: true,
+ view: null,
+ keyCode: 0,
+ charCode: "f".charCodeAt(0),
+ });
+ newInput.value = "";
+ newInput.focus();
+ newInput.dispatchEvent(event);
+ });
+
+ await new Promise(resolve => hitEventLoop(resolve, 100));
+
+ ok(!popupShown, "Popup must not be opened");
+ SpecialPowers.removeAutoCompletePopupEventListener(
+ window,
+ "popupshowing",
+ listener
+ );
+ }
+ );
+});
+
+function hitEventLoop(func, times) {
+ if (times > 0) {
+ setTimeout(hitEventLoop, 0, func, times - 1);
+ } else {
+ setTimeout(func, 0);
+ }
+}
diff --git a/editor/libeditor/tests/browser_content_command_insert_text.js b/editor/libeditor/tests/browser_content_command_insert_text.js
new file mode 100644
index 0000000000..073b6f830e
--- /dev/null
+++ b/editor/libeditor/tests/browser_content_command_insert_text.js
@@ -0,0 +1,271 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+const gCUITestUtils = new CustomizableUITestUtils(window);
+const gDOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+
+add_task(async function test_setup() {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+function promiseResettingSearchBarAndFocus() {
+ const waitForFocusInSearchBar = BrowserTestUtils.waitForEvent(
+ BrowserSearch.searchBar.textbox,
+ "focus"
+ );
+ BrowserSearch.searchBar.textbox.focus();
+ BrowserSearch.searchBar.textbox.value = "";
+ return Promise.all([
+ waitForFocusInSearchBar,
+ TestUtils.waitForCondition(
+ () =>
+ gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED &&
+ gDOMWindowUtils.inputContextOrigin ===
+ Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_MAIN
+ ),
+ ]);
+}
+
+function promiseIMEStateEnabledByRemote() {
+ return TestUtils.waitForCondition(
+ () =>
+ gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED &&
+ gDOMWindowUtils.inputContextOrigin ===
+ Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_CONTENT
+ );
+}
+
+function promiseContentTick(browser) {
+ return SpecialPowers.spawn(browser, [], async () => {
+ await new Promise(r => {
+ content.requestAnimationFrame(() => {
+ content.requestAnimationFrame(r);
+ });
+ });
+ });
+}
+
+add_task(async function test_text_editor_in_chrome() {
+ await promiseResettingSearchBarAndFocus();
+
+ let events = [];
+ function logEvent(event) {
+ events.push(event);
+ }
+ BrowserSearch.searchBar.textbox.addEventListener("beforeinput", logEvent);
+ gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ");
+
+ is(
+ BrowserSearch.searchBar.textbox.value,
+ "XYZ",
+ "The string should be inserted into the focused search bar"
+ );
+ is(
+ events.length,
+ 1,
+ "One beforeinput event should be fired in the searchbar"
+ );
+ is(events[0]?.inputType, "insertText", 'inputType should be "insertText"');
+ is(events[0]?.data, "XYZ", 'inputType should be "XYZ"');
+ is(events[0]?.cancelable, true, "beforeinput event should be cancelable");
+ BrowserSearch.searchBar.textbox.removeEventListener("beforeinput", logEvent);
+
+ BrowserSearch.searchBar.textbox.blur();
+});
+
+add_task(async function test_text_editor_in_content() {
+ for (const test of [
+ {
+ tag: "input",
+ target: "input",
+ nonTarget: "input + input",
+ page: 'data:text/html,<input value="abc"><input value="def">',
+ },
+ {
+ tag: "textarea",
+ target: "textarea",
+ nonTarget: "textarea + textarea",
+ page: "data:text/html,<textarea>abc</textarea><textarea>def</textarea>",
+ },
+ ]) {
+ // Once, move focus to chrome's searchbar.
+ await promiseResettingSearchBarAndFocus();
+
+ await BrowserTestUtils.withNewTab(test.page, async browser => {
+ await SpecialPowers.spawn(browser, [test], async function (aTest) {
+ content.window.focus();
+ await ContentTaskUtils.waitForCondition(() =>
+ content.document.hasFocus()
+ );
+ const target = content.document.querySelector(aTest.target);
+ target.focus();
+ target.selectionStart = target.selectionEnd = 2;
+ content.document.documentElement.scrollTop; // Flush pending things
+ });
+
+ await promiseIMEStateEnabledByRemote();
+ const waitForBeforeInputEvent = SpecialPowers.spawn(
+ browser,
+ [test],
+ async function (aTest) {
+ await new Promise(resolve => {
+ content.document.querySelector(aTest.target).addEventListener(
+ "beforeinput",
+ event => {
+ is(
+ event.inputType,
+ "insertText",
+ `The inputType of beforeinput event fired on <${aTest.target}> should be "insertText"`
+ );
+ is(
+ event.data,
+ "XYZ",
+ `The data of beforeinput event fired on <${aTest.target}> should be "XYZ"`
+ );
+ is(
+ event.cancelable,
+ true,
+ `The beforeinput event fired on <${aTest.target}> should be cancelable`
+ );
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ }
+ );
+ const waitForInputEvent = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "input"
+ );
+ await promiseContentTick(browser); // Ensure "input" event listener in the remote process
+ gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ");
+ await waitForBeforeInputEvent;
+ await waitForInputEvent;
+
+ await SpecialPowers.spawn(browser, [test], async function (aTest) {
+ is(
+ content.document.querySelector(aTest.target).value,
+ "abXYZc",
+ `The string should be inserted into the focused <${aTest.target}> element`
+ );
+ is(
+ content.document.querySelector(aTest.nonTarget).value,
+ "def",
+ `The string should not be inserted into the non-focused <${aTest.nonTarget}> element`
+ );
+ });
+ });
+
+ is(
+ BrowserSearch.searchBar.textbox.value,
+ "",
+ "The string should not be inserted into the previously focused search bar"
+ );
+ }
+});
+
+add_task(async function test_html_editor_in_content() {
+ for (const test of [
+ {
+ mode: "contenteditable",
+ target: "div",
+ page: "data:text/html,<div contenteditable>abc</div>",
+ },
+ {
+ mode: "designMode",
+ target: "div",
+ page: "data:text/html,<div>abc</div>",
+ },
+ ]) {
+ // Once, move focus to chrome's searchbar.
+ await promiseResettingSearchBarAndFocus();
+
+ await BrowserTestUtils.withNewTab(test.page, async browser => {
+ await SpecialPowers.spawn(browser, [test], async function (aTest) {
+ content.window.focus();
+ await ContentTaskUtils.waitForCondition(() =>
+ content.document.hasFocus()
+ );
+ const target = content.document.querySelector(aTest.target);
+ if (aTest.mode == "designMode") {
+ content.document.designMode = "on";
+ content.window.focus();
+ } else {
+ target.focus();
+ }
+ content.window.getSelection().collapse(target.firstChild, 2);
+ content.document.documentElement.scrollTop; // Flush pending things
+ });
+
+ await promiseIMEStateEnabledByRemote();
+ const waitForBeforeInputEvent = SpecialPowers.spawn(
+ browser,
+ [test],
+ async function (aTest) {
+ await new Promise(resolve => {
+ const eventTarget =
+ aTest.mode === "designMode"
+ ? content.document
+ : content.document.querySelector(aTest.target);
+ eventTarget.addEventListener(
+ "beforeinput",
+ event => {
+ is(
+ event.inputType,
+ "insertText",
+ `The inputType of beforeinput event fired on ${aTest.mode} editor should be "insertText"`
+ );
+ is(
+ event.data,
+ "XYZ",
+ `The data of beforeinput event fired on ${aTest.mode} editor should be "XYZ"`
+ );
+ is(
+ event.cancelable,
+ true,
+ `The beforeinput event fired on ${aTest.mode} editor should be cancelable`
+ );
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ }
+ );
+ const waitForInputEvent = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "input"
+ );
+ await promiseContentTick(browser); // Ensure "input" event listener in the remote process
+ gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ");
+ await waitForBeforeInputEvent;
+ await waitForInputEvent;
+
+ await SpecialPowers.spawn(browser, [test], async function (aTest) {
+ is(
+ content.document.querySelector(aTest.target).innerHTML,
+ "abXYZc",
+ `The string should be inserted into the focused ${aTest.mode} editor`
+ );
+ });
+ });
+
+ is(
+ BrowserSearch.searchBar.textbox.value,
+ "",
+ "The string should not be inserted into the previously focused search bar"
+ );
+ }
+});
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/LICENSE b/editor/libeditor/tests/browserscope/lib/richtext/LICENSE
new file mode 100644
index 0000000000..57bc88a15a
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/README b/editor/libeditor/tests/browserscope/lib/richtext/README
new file mode 100644
index 0000000000..a3bc3110f4
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/README
@@ -0,0 +1,58 @@
+README FOR BROWSERSCOPE
+-----------------------
+
+Hey there - thanks for downloading the code. This file has instructions
+for getting setup so that you can run the codebase locally.
+
+This project is built on Google App Engine using the
+Django web application framework and written in Python.
+
+To get started, you'll need to first download the App Engine SDK at:
+http://code.google.com/appengine/downloads.html
+
+For local development, just startup the server:
+./pathto/google_appengine/dev_appserver.py --port=8080 browserscope
+
+You should then be able to access the local application at:
+http://localhost:8080/
+
+Note: the first time you hit the homepage it may take a little
+while - that's because it's trying to read out median times for all
+of the tests from a nonexistent datastore and write to memcache.
+Just be a lil patient.
+
+You can run the unit tests at:
+ http://localhost:8080/test
+
+
+CONTRIBUTING
+------------------
+
+Most likely you are interested in adding new tests or creating
+a new test category. If you are interested in adding tests to an existing
+"category" you may want to get in touch with the maintainer for that
+branch of the tree. We are really looking forward to receiving your
+code in patch format. Currently the category maintainers are:
+Network: Steve Souders <souders@gmail.com>
+Reflow: Lindsey Simon <elsigh@gmail.com>
+Security: Adam Barth <adam@adambarth.com> and Collin Jackson <collin@collinjackson.com>
+
+
+To create a completely new test category:
+ * Copy one of the existing directories in categories/
+ * Edit your test_set.py, handlers.py
+ * Add your files in templates/ and static/
+ * Update urls.py and settings.CATEGORIES
+ * Follow the examples of other tests re:
+ * beaconing using/testdriver_base
+ * your GetScoreAndDisplayValue method
+ * your GetRowScoreAndDisplayValue method
+
+References:
+ * App Engine Docs - http://code.google.com/appengine/docs/python/overview.html
+ * App Engine Group - http://groups.google.com/group/google-appengine
+ * Python Docs - http://www.python.org/doc/
+ * Django - http://www.djangoproject.com/
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla b/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla
new file mode 100644
index 0000000000..5d304943f7
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla
@@ -0,0 +1,17 @@
+The BrowserScope project provides a set of cross-browser HTML editor tests,
+which we import in our test suite in order to run them as part of our
+continuous integration system.
+
+We pull tests occasionally from their Subversion repository using the pull
+script which can be found in this directory. We also record the revision ID
+which we've used in the current_revision file inside this directory.
+
+Using the pull script is quite easy, just switch to this directory, and say:
+
+sh update_from_upstream
+
+There are tests which we're currently failing on, and there will probably be
+more of those in the future. We should maintain a list of the failing tests
+manually in currentStatus.js (which can also be found in this directory), to
+make sure that the suite passes entirely, with failing tests marked as todo
+items.
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js b/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js
new file mode 100644
index 0000000000..4b645d9db4
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js
@@ -0,0 +1,43 @@
+/**
+ * This file lists the tests in the BrowserScope suite which we are currently
+ * failing. We mark them as todo items to keep track of them.
+ */
+
+var knownFailures = {
+ // Dummy result items. There is one for each category.
+ 'apply' : {
+ '0-undefined' : true
+ },
+ 'unapply' : {
+ '0-undefined' : true
+ },
+ 'change' : {
+ '0-undefined' : true
+ },
+ 'query' : {
+ '0-undefined' : true
+ },
+ 'a' : {
+ 'createbookmark-0' : true,
+ 'decreasefontsize-0' : true,
+ 'fontsize-1' : true,
+ 'subscript-1' : true,
+ 'superscript-1' : true,
+ },
+ 'u': {
+ 'removeformat-1' : true,
+ 'removeformat-2' : true,
+ 'strikethrough-2' : true,
+ 'subscript-1' : true,
+ 'superscript-1' : true,
+ 'unbookmark-0' : true,
+ },
+ 'q': {
+ 'fontsize-1' : true,
+ 'fontsize-2' : true,
+ },
+};
+
+function isKnownFailure(type, test, param) {
+ return (type in knownFailures) && knownFailures[type][test + "-" + param];
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/current_revision b/editor/libeditor/tests/browserscope/lib/richtext/current_revision
new file mode 100644
index 0000000000..1e25699145
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/current_revision
@@ -0,0 +1 @@
+775
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html b/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html
new file mode 100644
index 0000000000..a294f0b56b
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <script>
+ function load(){
+ window.document.designMode = "On";
+ }
+ </script>
+</head>
+<body contentEditable="true" onload="load()">
+</body>
+</html> \ No newline at end of file
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js b/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js
new file mode 100644
index 0000000000..edd23f86b9
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js
@@ -0,0 +1,1069 @@
+var goog$global = this, goog$isString = function(val) {
+ return typeof val == "string"
+};
+Math.floor(Math.random() * 2147483648).toString(36);
+var goog$now = Date.now || function() {
+ return(new Date).getTime()
+}, goog$inherits = function(childCtor, parentCtor) {
+ function tempCtor() {
+ }
+ tempCtor.prototype = parentCtor.prototype;
+ childCtor.superClass_ = parentCtor.prototype;
+ childCtor.prototype = new tempCtor
+};var goog$array$peek = function(array) {
+ return array[array.length - 1]
+}, goog$array$indexOf = function(arr, obj, opt_fromIndex) {
+ if(arr.indexOf)return arr.indexOf(obj, opt_fromIndex);
+ if(Array.indexOf)return Array.indexOf(arr, obj, opt_fromIndex);
+ for(var fromIndex = opt_fromIndex == null ? 0 : opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex, i = fromIndex;i < arr.length;i++)if(i in arr && arr[i] === obj)return i;
+ return-1
+}, goog$array$map = function(arr, f, opt_obj) {
+ if(arr.map)return arr.map(f, opt_obj);
+ if(Array.map)return Array.map(arr, f, opt_obj);
+ for(var l = arr.length, res = [], resLength = 0, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2)res[resLength++] = f.call(opt_obj, arr2[i], i, arr);
+ return res
+}, goog$array$some = function(arr, f, opt_obj) {
+ if(arr.some)return arr.some(f, opt_obj);
+ if(Array.some)return Array.some(arr, f, opt_obj);
+ for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && f.call(opt_obj, arr2[i], i, arr))return true;
+ return false
+}, goog$array$every = function(arr, f, opt_obj) {
+ if(arr.every)return arr.every(f, opt_obj);
+ if(Array.every)return Array.every(arr, f, opt_obj);
+ for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && !f.call(opt_obj, arr2[i], i, arr))return false;
+ return true
+}, goog$array$find = function(arr, f, opt_obj) {
+ var i;
+ JSCompiler_inline_label_goog$array$findIndex_12: {
+ for(var JSCompiler_inline_l = arr.length, JSCompiler_inline_arr2 = goog$isString(arr) ? arr.split("") : arr, JSCompiler_inline_i = 0;JSCompiler_inline_i < JSCompiler_inline_l;JSCompiler_inline_i++)if(JSCompiler_inline_i in JSCompiler_inline_arr2 && f.call(opt_obj, JSCompiler_inline_arr2[JSCompiler_inline_i], JSCompiler_inline_i, arr)) {
+ i = JSCompiler_inline_i;
+ break JSCompiler_inline_label_goog$array$findIndex_12
+ }i = -1
+ }return i < 0 ? null : goog$isString(arr) ? arr.charAt(i) : arr[i]
+};var goog$string$trim = function(str) {
+ return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, "")
+}, goog$string$htmlEscape = function(str, opt_isLikelyToContainHtmlChars) {
+ if(opt_isLikelyToContainHtmlChars)return str.replace(goog$string$amperRe_, "&amp;").replace(goog$string$ltRe_, "&lt;").replace(goog$string$gtRe_, "&gt;").replace(goog$string$quotRe_, "&quot;");
+ else {
+ if(!goog$string$allRe_.test(str))return str;
+ if(str.includes("&"))str = str.replace(goog$string$amperRe_, "&amp;");
+ if(str.includes("<"))str = str.replace(goog$string$ltRe_, "&lt;");
+ if(str.includes(">"))str = str.replace(goog$string$gtRe_, "&gt;");
+ if(str.includes('"'))str = str.replace(goog$string$quotRe_, "&quot;");
+ return str
+ }
+}, goog$string$amperRe_ = /&/g, goog$string$ltRe_ = /</g, goog$string$gtRe_ = />/g, goog$string$quotRe_ = /\"/g, goog$string$allRe_ = /[&<>\"]/, goog$string$contains = function(s, ss) {
+ return s.includes(ss)
+}, goog$string$compareVersions = function(version1, version2) {
+ for(var order = 0, v1Subs = goog$string$trim(String(version1)).split("."), v2Subs = goog$string$trim(String(version2)).split("."), subCount = Math.max(v1Subs.length, v2Subs.length), subIdx = 0;order == 0 && subIdx < subCount;subIdx++) {
+ var v1Sub = v1Subs[subIdx] || "", v2Sub = v2Subs[subIdx] || "", v1CompParser = new RegExp("(\\d*)(\\D*)", "g"), v2CompParser = new RegExp("(\\d*)(\\D*)", "g");
+ do {
+ var v1Comp = v1CompParser.exec(v1Sub) || ["", "", ""], v2Comp = v2CompParser.exec(v2Sub) || ["", "", ""];
+ if(v1Comp[0].length == 0 && v2Comp[0].length == 0)break;
+ var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10), v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10);
+ order = goog$string$compareElements_(v1CompNum, v2CompNum) || goog$string$compareElements_(v1Comp[2].length == 0, v2Comp[2].length == 0) || goog$string$compareElements_(v1Comp[2], v2Comp[2])
+ }while(order == 0)
+ }return order
+}, goog$string$compareElements_ = function(left, right) {
+ if(left < right)return-1;
+ else if(left > right)return 1;
+ return 0
+};
+goog$now();var goog$userAgent$detectedOpera_, goog$userAgent$detectedIe_, goog$userAgent$detectedWebkit_, goog$userAgent$detectedMobile_, goog$userAgent$detectedGecko_, goog$userAgent$detectedCamino_, goog$userAgent$detectedMac_, goog$userAgent$detectedWindows_, goog$userAgent$detectedLinux_, goog$userAgent$detectedX11_, goog$userAgent$getUserAgentString = function() {
+ return goog$global.navigator ? goog$global.navigator.userAgent : null
+}, goog$userAgent$getNavigator = function() {
+ return goog$global.navigator
+};
+goog$userAgent$detectedCamino_ = goog$userAgent$detectedGecko_ = goog$userAgent$detectedMobile_ = goog$userAgent$detectedWebkit_ = goog$userAgent$detectedIe_ = goog$userAgent$detectedOpera_ = false;
+var JSCompiler_inline_ua_15;
+if(JSCompiler_inline_ua_15 = goog$userAgent$getUserAgentString()) {
+ var JSCompiler_inline_navigator$$1_16 = goog$userAgent$getNavigator();
+ goog$userAgent$detectedOpera_ = JSCompiler_inline_ua_15.indexOf("Opera") == 0;
+ goog$userAgent$detectedIe_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.includes("MSIE");
+ goog$userAgent$detectedMobile_ = (goog$userAgent$detectedWebkit_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.includes("WebKit")) && JSCompiler_inline_ua_15.includes("Mobile");
+ goog$userAgent$detectedCamino_ = (goog$userAgent$detectedGecko_ = !goog$userAgent$detectedOpera_ && !goog$userAgent$detectedWebkit_ && JSCompiler_inline_navigator$$1_16.product == "Gecko") && JSCompiler_inline_navigator$$1_16.vendor == "Camino"
+}var goog$userAgent$OPERA = goog$userAgent$detectedOpera_, goog$userAgent$IE = goog$userAgent$detectedIe_, goog$userAgent$GECKO = goog$userAgent$detectedGecko_, goog$userAgent$WEBKIT = goog$userAgent$detectedWebkit_, goog$userAgent$MOBILE = goog$userAgent$detectedMobile_, goog$userAgent$PLATFORM, JSCompiler_inline_navigator$$2_19 = goog$userAgent$getNavigator();
+goog$userAgent$PLATFORM = JSCompiler_inline_navigator$$2_19 && JSCompiler_inline_navigator$$2_19.platform || "";
+goog$userAgent$detectedMac_ = goog$string$contains(goog$userAgent$PLATFORM, "Mac");
+goog$userAgent$detectedWindows_ = goog$string$contains(goog$userAgent$PLATFORM, "Win");
+goog$userAgent$detectedLinux_ = goog$string$contains(goog$userAgent$PLATFORM, "Linux");
+goog$userAgent$detectedX11_ = !!goog$userAgent$getNavigator() && goog$string$contains(goog$userAgent$getNavigator().appVersion || "", "X11");
+var goog$userAgent$VERSION, JSCompiler_inline_version$$6_26 = "", JSCompiler_inline_re$$2_27;
+if(goog$userAgent$OPERA && goog$global.opera) {
+ var JSCompiler_inline_operaVersion_28 = goog$global.opera.version;
+ JSCompiler_inline_version$$6_26 = typeof JSCompiler_inline_operaVersion_28 == "function" ? JSCompiler_inline_operaVersion_28() : JSCompiler_inline_operaVersion_28
+}else {
+ if(goog$userAgent$GECKO)JSCompiler_inline_re$$2_27 = /rv\:([^\);]+)(\)|;)/;
+ else if(goog$userAgent$IE)JSCompiler_inline_re$$2_27 = /MSIE\s+([^\);]+)(\)|;)/;
+ else if(goog$userAgent$WEBKIT)JSCompiler_inline_re$$2_27 = /WebKit\/(\S+)/;
+ if(JSCompiler_inline_re$$2_27) {
+ var JSCompiler_inline_arr$$41_29 = JSCompiler_inline_re$$2_27.exec(goog$userAgent$getUserAgentString());
+ JSCompiler_inline_version$$6_26 = JSCompiler_inline_arr$$41_29 ? JSCompiler_inline_arr$$41_29[1] : ""
+ }
+}goog$userAgent$VERSION = JSCompiler_inline_version$$6_26;
+var goog$userAgent$isVersionCache_ = {}, goog$userAgent$isVersion = function(version) {
+ return goog$userAgent$isVersionCache_[version] || (goog$userAgent$isVersionCache_[version] = goog$string$compareVersions(goog$userAgent$VERSION, version) >= 0)
+};var goog$dom$getWindow = function(opt_doc) {
+ return opt_doc ? goog$dom$getWindow_(opt_doc) : window
+}, goog$dom$getWindow_ = function(doc) {
+ if(doc.parentWindow)return doc.parentWindow;
+ if(goog$userAgent$WEBKIT && !goog$userAgent$isVersion("500") && !goog$userAgent$MOBILE) {
+ var scriptElement = doc.createElement("script");
+ scriptElement.innerHTML = "document.parentWindow=window";
+ var parentElement = doc.documentElement;
+ parentElement.appendChild(scriptElement);
+ parentElement.removeChild(scriptElement);
+ return doc.parentWindow
+ }return doc.defaultView
+}, goog$dom$appendChild = function(parent, child) {
+ parent.appendChild(child)
+}, goog$dom$BAD_CONTAINS_WEBKIT_ = goog$userAgent$WEBKIT && goog$userAgent$isVersion("522"), goog$dom$contains = function(parent, descendant) {
+ if(typeof parent.contains != "undefined" && !goog$dom$BAD_CONTAINS_WEBKIT_ && descendant.nodeType == 1)return parent == descendant || parent.contains(descendant);
+ if(typeof parent.compareDocumentPosition != "undefined")return parent == descendant || Boolean(parent.compareDocumentPosition(descendant) & 16);
+ for(;descendant && parent != descendant;)descendant = descendant.parentNode;
+ return descendant == parent
+}, goog$dom$compareNodeOrder = function(node1, node2) {
+ if(node1 == node2)return 0;
+ if(node1.compareDocumentPosition)return node1.compareDocumentPosition(node2) & 2 ? 1 : -1;
+ if("sourceIndex" in node1 || node1.parentNode && "sourceIndex" in node1.parentNode) {
+ var isElement1 = node1.nodeType == 1, isElement2 = node2.nodeType == 1;
+ if(isElement1 && isElement2)return node1.sourceIndex - node2.sourceIndex;
+ else {
+ var parent1 = node1.parentNode, parent2 = node2.parentNode;
+ if(parent1 == parent2)return goog$dom$compareSiblingOrder_(node1, node2);
+ if(!isElement1 && goog$dom$contains(parent1, node2))return-1 * goog$dom$compareParentsDescendantNodeIe_(node1, node2);
+ if(!isElement2 && goog$dom$contains(parent2, node1))return goog$dom$compareParentsDescendantNodeIe_(node2, node1);
+ return(isElement1 ? node1.sourceIndex : parent1.sourceIndex) - (isElement2 ? node2.sourceIndex : parent2.sourceIndex)
+ }
+ }var doc = goog$dom$getOwnerDocument(node1), range1, range2;
+ range1 = doc.createRange();
+ range1.selectNode(node1);
+ range1.collapse(true);
+ range2 = doc.createRange();
+ range2.selectNode(node2);
+ range2.collapse(true);
+ return range1.compareBoundaryPoints(goog$global.Range.START_TO_END, range2)
+}, goog$dom$compareParentsDescendantNodeIe_ = function(textNode, node) {
+ var parent = textNode.parentNode;
+ if(parent == node)return-1;
+ for(var sibling = node;sibling.parentNode != parent;)sibling = sibling.parentNode;
+ return goog$dom$compareSiblingOrder_(sibling, textNode)
+}, goog$dom$compareSiblingOrder_ = function(node1, node2) {
+ for(var s = node2;s = s.previousSibling;)if(s == node1)return-1;
+ return 1
+}, goog$dom$findCommonAncestor = function() {
+ var i, count = arguments.length;
+ if(count) {
+ if(count == 1)return arguments[0]
+ }else return null;
+ var paths = [], minLength = Infinity;
+ for(i = 0;i < count;i++) {
+ for(var ancestors = [], node = arguments[i];node;) {
+ ancestors.unshift(node);
+ node = node.parentNode
+ }paths.push(ancestors);
+ minLength = Math.min(minLength, ancestors.length)
+ }var output = null;
+ for(i = 0;i < minLength;i++) {
+ for(var first = paths[0][i], j = 1;j < count;j++)if(first != paths[j][i])return output;
+ output = first
+ }return output
+}, goog$dom$getOwnerDocument = function(node) {
+ // Added 'editorDoc' as hack for browsers that don't support node.ownerDocument
+ return node.nodeType == 9 ? node : node.ownerDocument || node.document || editorDoc
+}, goog$dom$DomHelper = function(opt_document) {
+ this.document_ = opt_document || goog$global.document || document
+};
+goog$dom$DomHelper.prototype.getDocument = function() {
+ return this.document_
+};
+goog$dom$DomHelper.prototype.createElement = function(name) {
+ return this.document_.createElement(name)
+};
+goog$dom$DomHelper.prototype.getWindow = function() {
+ return goog$dom$getWindow_(this.document_)
+};
+goog$dom$DomHelper.prototype.appendChild = goog$dom$appendChild;
+goog$dom$DomHelper.prototype.contains = goog$dom$contains;var goog$Disposable = function() {
+};if("StopIteration" in goog$global)var goog$iter$StopIteration = goog$global.StopIteration;
+else goog$iter$StopIteration = Error("StopIteration");
+var goog$iter$Iterator = function() {
+};
+goog$iter$Iterator.prototype.next = function() {
+ throw goog$iter$StopIteration;
+};
+goog$iter$Iterator.prototype.__iterator__ = function() {
+ return this
+};var goog$debug$exposeException = function(err, opt_fn) {
+ try {
+ var e, JSCompiler_inline_href_34;
+ JSCompiler_inline_label_goog$getObjectByName_61: {
+ for(var JSCompiler_inline_parts = "window.location.href".split("."), JSCompiler_inline_cur = goog$global, JSCompiler_inline_part;JSCompiler_inline_part = JSCompiler_inline_parts.shift();)if(JSCompiler_inline_cur[JSCompiler_inline_part])JSCompiler_inline_cur = JSCompiler_inline_cur[JSCompiler_inline_part];
+ else {
+ JSCompiler_inline_href_34 = null;
+ break JSCompiler_inline_label_goog$getObjectByName_61
+ }JSCompiler_inline_href_34 = JSCompiler_inline_cur
+ }e = typeof err == "string" ? {message:err, name:"Unknown error", lineNumber:"Not available", fileName:JSCompiler_inline_href_34, stack:"Not available"} : !err.lineNumber || !err.fileName || !err.stack ? {message:err.message, name:err.name, lineNumber:err.lineNumber || err.line || "Not available", fileName:err.fileName || err.filename || err.sourceURL || JSCompiler_inline_href_34, stack:err.stack || "Not available"} : err;
+ var error = "Message: " + goog$string$htmlEscape(e.message) + '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' + e.fileName + "</a>\nLine: " + e.lineNumber + "\n\nBrowser stack:\n" + goog$string$htmlEscape(e.stack + "-> ") + "[end]\n\nJS stack traversal:\n" + goog$string$htmlEscape(goog$debug$getStacktrace(opt_fn) + "-> ");
+ return error
+ }catch(e2) {
+ return"Exception trying to expose exception! You win, we lose. " + e2
+ }
+}, goog$debug$getStacktrace = function(opt_fn) {
+ return goog$debug$getStacktraceHelper_(opt_fn || arguments.callee.caller, [])
+}, goog$debug$getStacktraceHelper_ = function(fn, visited) {
+ var sb = [], JSCompiler_inline_result_36;
+ JSCompiler_inline_label_goog$array$contains_41:JSCompiler_inline_result_36 = visited.contains ? visited.contains(fn) : goog$array$indexOf(visited, fn) > -1;
+ if(JSCompiler_inline_result_36)sb.push("[...circular reference...]");
+ else if(fn && visited.length < 50) {
+ sb.push(goog$debug$getFunctionName(fn) + "(");
+ for(var args = fn.arguments, i = 0;i < args.length;i++) {
+ i > 0 && sb.push(", ");
+ var argDesc, arg = args[i];
+ switch(typeof arg) {
+ case "object":
+ argDesc = arg ? "object" : "null";
+ break;
+ case "string":
+ argDesc = arg;
+ break;
+ case "number":
+ argDesc = String(arg);
+ break;
+ case "boolean":
+ argDesc = arg ? "true" : "false";
+ break;
+ case "function":
+ argDesc = (argDesc = goog$debug$getFunctionName(arg)) ? argDesc : "[fn]";
+ break;
+ case "undefined":
+ ;
+ default:
+ argDesc = typeof arg;
+ break
+ }
+ if(argDesc.length > 40)argDesc = argDesc.substr(0, 40) + "...";
+ sb.push(argDesc)
+ }visited.push(fn);
+ sb.push(")\n");
+ try {
+ sb.push(goog$debug$getStacktraceHelper_(fn.caller, visited))
+ }catch(e) {
+ sb.push("[exception trying to get caller]\n")
+ }
+ }else fn ? sb.push("[...long stack...]") : sb.push("[end]");
+ return sb.join("")
+}, goog$debug$getFunctionName = function(fn) {
+ var functionSource = String(fn);
+ if(!goog$debug$fnNameCache_[functionSource]) {
+ var matches = /function ([^\(]+)/.exec(functionSource);
+ if(matches) {
+ var method = matches[1];
+ goog$debug$fnNameCache_[functionSource] = method
+ }else goog$debug$fnNameCache_[functionSource] = "[Anonymous]"
+ }return goog$debug$fnNameCache_[functionSource]
+}, goog$debug$fnNameCache_ = {};var goog$debug$LogRecord = function(level, msg, loggerName, opt_time, opt_sequenceNumber) {
+ this.sequenceNumber_ = typeof opt_sequenceNumber == "number" ? opt_sequenceNumber : goog$debug$LogRecord$nextSequenceNumber_++;
+ this.time_ = opt_time || goog$now();
+ this.level_ = level;
+ this.msg_ = msg;
+ this.loggerName_ = loggerName
+};
+goog$debug$LogRecord.prototype.exception_ = null;
+goog$debug$LogRecord.prototype.exceptionText_ = null;
+var goog$debug$LogRecord$nextSequenceNumber_ = 0;
+goog$debug$LogRecord.prototype.setException = function(exception) {
+ this.exception_ = exception
+};
+goog$debug$LogRecord.prototype.setExceptionText = function(text) {
+ this.exceptionText_ = text
+};
+goog$debug$LogRecord.prototype.setLevel = function(level) {
+ this.level_ = level
+};var goog$debug$Logger = function(name) {
+ this.name_ = name;
+ this.parent_ = null;
+ this.children_ = {};
+ this.handlers_ = []
+};
+goog$debug$Logger.prototype.level_ = null;
+var goog$debug$Logger$Level = function(name, value) {
+ this.name = name;
+ this.value = value
+};
+goog$debug$Logger$Level.prototype.toString = function() {
+ return this.name
+};
+new goog$debug$Logger$Level("OFF", Infinity);
+new goog$debug$Logger$Level("SHOUT", 1200);
+var goog$debug$Logger$Level$SEVERE = new goog$debug$Logger$Level("SEVERE", 1000), goog$debug$Logger$Level$WARNING = new goog$debug$Logger$Level("WARNING", 900);
+new goog$debug$Logger$Level("INFO", 800);
+var goog$debug$Logger$Level$CONFIG = new goog$debug$Logger$Level("CONFIG", 700);
+new goog$debug$Logger$Level("FINE", 500);
+new goog$debug$Logger$Level("FINER", 400);
+new goog$debug$Logger$Level("FINEST", 300);
+new goog$debug$Logger$Level("ALL", 0);
+goog$debug$Logger.prototype.setLevel = function(level) {
+ this.level_ = level
+};
+goog$debug$Logger.prototype.isLoggable = function(level) {
+ if(this.level_)return level.value >= this.level_.value;
+ if(this.parent_)return this.parent_.isLoggable(level);
+ return false
+};
+goog$debug$Logger.prototype.log = function(level, msg, opt_exception) {
+ this.isLoggable(level) && this.logRecord(this.getLogRecord(level, msg, opt_exception))
+};
+goog$debug$Logger.prototype.getLogRecord = function(level, msg, opt_exception) {
+ var logRecord = new goog$debug$LogRecord(level, String(msg), this.name_);
+ if(opt_exception) {
+ logRecord.setException(opt_exception);
+ logRecord.setExceptionText(goog$debug$exposeException(opt_exception, arguments.callee.caller))
+ }return logRecord
+};
+goog$debug$Logger.prototype.severe = function(msg, opt_exception) {
+ this.log(goog$debug$Logger$Level$SEVERE, msg, opt_exception)
+};
+goog$debug$Logger.prototype.warning = function(msg, opt_exception) {
+ this.log(goog$debug$Logger$Level$WARNING, msg, opt_exception)
+};
+goog$debug$Logger.prototype.logRecord = function(logRecord) {
+ if(this.isLoggable(logRecord.level_))for(var target = this;target;) {
+ target.callPublish_(logRecord);
+ target = target.parent_
+ }
+};
+goog$debug$Logger.prototype.callPublish_ = function(logRecord) {
+ for(var i = 0;i < this.handlers_.length;i++)this.handlers_[i](logRecord)
+};
+goog$debug$Logger.prototype.setParent_ = function(parent) {
+ this.parent_ = parent
+};
+goog$debug$Logger.prototype.addChild_ = function(name, logger) {
+ this.children_[name] = logger
+};
+var goog$debug$LogManager$loggers_ = {}, goog$debug$LogManager$rootLogger_ = null, goog$debug$LogManager$getLogger = function(name) {
+ if(!goog$debug$LogManager$rootLogger_) {
+ goog$debug$LogManager$rootLogger_ = new goog$debug$Logger("");
+ goog$debug$LogManager$loggers_[""] = goog$debug$LogManager$rootLogger_;
+ goog$debug$LogManager$rootLogger_.setLevel(goog$debug$Logger$Level$CONFIG)
+ }return name in goog$debug$LogManager$loggers_ ? goog$debug$LogManager$loggers_[name] : goog$debug$LogManager$createLogger_(name)
+}, goog$debug$LogManager$createLogger_ = function(name) {
+ var logger = new goog$debug$Logger(name), parts = name.split("."), leafName = parts[parts.length - 1];
+ parts.length = parts.length - 1;
+ var parentName = parts.join("."), parentLogger = goog$debug$LogManager$getLogger(parentName);
+ parentLogger.addChild_(leafName, logger);
+ logger.setParent_(parentLogger);
+ return goog$debug$LogManager$loggers_[name] = logger
+};var goog$dom$SavedRange = function() {
+ goog$Disposable.call(this)
+};
+goog$inherits(goog$dom$SavedRange, goog$Disposable);
+goog$debug$LogManager$getLogger("goog.dom.SavedRange");var goog$dom$TagIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_tagType, opt_depth) {
+ this.reversed = !!opt_reversed;
+ opt_node && this.setPosition(opt_node, opt_tagType);
+ this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0;
+ if(this.reversed)this.depth *= -1;
+ this.constrained = !opt_unconstrained
+};
+goog$inherits(goog$dom$TagIterator, goog$iter$Iterator);
+goog$dom$TagIterator.prototype.node = null;
+goog$dom$TagIterator.prototype.tagType = null;
+goog$dom$TagIterator.prototype.started_ = false;
+goog$dom$TagIterator.prototype.setPosition = function(node, opt_tagType, opt_depth) {
+ if(this.node = node)this.tagType = typeof opt_tagType == "number" ? opt_tagType : this.node.nodeType != 1 ? 0 : this.reversed ? -1 : 1;
+ if(typeof opt_depth == "number")this.depth = opt_depth
+};
+goog$dom$TagIterator.prototype.next = function() {
+ var node;
+ if(this.started_) {
+ if(!this.node || this.constrained && this.depth == 0)throw goog$iter$StopIteration;node = this.node;
+ var startType = this.reversed ? -1 : 1;
+ if(this.tagType == startType) {
+ var child = this.reversed ? node.lastChild : node.firstChild;
+ child ? this.setPosition(child) : this.setPosition(node, startType * -1)
+ }else {
+ var sibling = this.reversed ? node.previousSibling : node.nextSibling;
+ sibling ? this.setPosition(sibling) : this.setPosition(node.parentNode, startType * -1)
+ }this.depth += this.tagType * (this.reversed ? -1 : 1)
+ }else this.started_ = true;
+ node = this.node;
+ if(!this.node)throw goog$iter$StopIteration;return node
+};
+goog$dom$TagIterator.prototype.isStartTag = function() {
+ return this.tagType == 1
+};var goog$dom$AbstractRange = function() {
+};
+goog$dom$AbstractRange.prototype.getTextRanges = function() {
+ for(var output = [], i = 0, len = this.getTextRangeCount();i < len;i++)output.push(this.getTextRange(i));
+ return output
+};
+goog$dom$AbstractRange.prototype.getAnchorNode = function() {
+ return this.isReversed() ? this.getEndNode() : this.getStartNode()
+};
+goog$dom$AbstractRange.prototype.getAnchorOffset = function() {
+ return this.isReversed() ? this.getEndOffset() : this.getStartOffset()
+};
+goog$dom$AbstractRange.prototype.getFocusNode = function() {
+ return this.isReversed() ? this.getStartNode() : this.getEndNode()
+};
+goog$dom$AbstractRange.prototype.getFocusOffset = function() {
+ return this.isReversed() ? this.getStartOffset() : this.getEndOffset()
+};
+goog$dom$AbstractRange.prototype.isReversed = function() {
+ return false
+};
+goog$dom$AbstractRange.prototype.getDocument = function() {
+ return goog$dom$getOwnerDocument(goog$userAgent$IE ? this.getContainer() : this.getStartNode())
+};
+goog$dom$AbstractRange.prototype.getWindow = function() {
+ return goog$dom$getWindow(this.getDocument())
+};
+goog$dom$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+ return this.containsRange(goog$dom$TextRange$createFromNodeContents(node, undefined), opt_allowPartial)
+};
+var goog$dom$RangeIterator = function(node, opt_reverse) {
+ goog$dom$TagIterator.call(this, node, opt_reverse, true)
+};
+goog$inherits(goog$dom$RangeIterator, goog$dom$TagIterator);var goog$dom$AbstractMultiRange = function() {
+};
+goog$inherits(goog$dom$AbstractMultiRange, goog$dom$AbstractRange);
+goog$dom$AbstractMultiRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+ var ranges = this.getTextRanges(), otherRanges = otherRange.getTextRanges(), fn = opt_allowPartial ? goog$array$some : goog$array$every;
+ return fn(otherRanges, function(otherRange) {
+ return goog$array$some(ranges, function(range) {
+ return range.containsRange(otherRange, opt_allowPartial)
+ })
+ })
+};var goog$dom$TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) {
+ var goNext;
+ if(startNode) {
+ this.startNode_ = startNode;
+ this.startOffset_ = startOffset;
+ this.endNode_ = endNode;
+ this.endOffset_ = endOffset;
+ if(startNode.nodeType == 1 && startNode.tagName != "BR") {
+ var startChildren = startNode.childNodes, candidate = startChildren[startOffset];
+ if(candidate) {
+ this.startNode_ = candidate;
+ this.startOffset_ = 0
+ }else {
+ if(startChildren.length)this.startNode_ = goog$array$peek(startChildren);
+ goNext = true
+ }
+ }if(endNode.nodeType == 1)if(this.endNode_ = endNode.childNodes[endOffset])this.endOffset_ = 0;
+ else this.endNode_ = endNode
+ }goog$dom$RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse);
+ if(goNext)try {
+ this.next()
+ }catch(e) {
+ if(e != goog$iter$StopIteration)throw e;
+ }
+};
+goog$inherits(goog$dom$TextRangeIterator, goog$dom$RangeIterator);
+goog$dom$TextRangeIterator.prototype.startNode_ = null;
+goog$dom$TextRangeIterator.prototype.endNode_ = null;
+goog$dom$TextRangeIterator.prototype.startOffset_ = 0;
+goog$dom$TextRangeIterator.prototype.endOffset_ = 0;
+goog$dom$TextRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog$dom$TextRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog$dom$TextRangeIterator.prototype.isLast = function() {
+ return this.started_ && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag())
+};
+goog$dom$TextRangeIterator.prototype.next = function() {
+ if(this.isLast())throw goog$iter$StopIteration;return goog$dom$TextRangeIterator.superClass_.next.call(this)
+};var goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_, goog$userAgent$jscript$DETECTED_VERSION_, JSCompiler_inline_hasScriptEngine_44 = "ScriptEngine" in goog$global;
+goog$userAgent$jscript$DETECTED_VERSION_ = (goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_ = JSCompiler_inline_hasScriptEngine_44 && goog$global.ScriptEngine() == "JScript") ? goog$global.ScriptEngineMajorVersion() + "." + goog$global.ScriptEngineMinorVersion() + "." + goog$global.ScriptEngineBuildVersion() : "0";var goog$dom$browserrange$AbstractRange = function() {
+};
+goog$dom$browserrange$AbstractRange.prototype.containsRange = function(range, opt_allowPartial) {
+ return this.containsBrowserRange(range.range_, opt_allowPartial)
+};
+goog$dom$browserrange$AbstractRange.prototype.containsBrowserRange = function(range, opt_allowPartial) {
+ try {
+ return opt_allowPartial ? this.compareBrowserRangeEndpoints(range, 0, 1) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 0) <= 0 : this.compareBrowserRangeEndpoints(range, 0, 0) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 1) <= 0
+ }catch(e) {
+ if(!goog$userAgent$IE)throw e;return false
+ }
+};
+goog$dom$browserrange$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+ return this.containsRange(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_allowPartial)
+};
+goog$dom$browserrange$AbstractRange.prototype.__iterator__ = function() {
+ return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};var goog$dom$browserrange$W3cRange = function(range) {
+ this.range_ = range
+};
+goog$inherits(goog$dom$browserrange$W3cRange, goog$dom$browserrange$AbstractRange);
+var goog$dom$browserrange$W3cRange$getBrowserRangeForNode = function(node) {
+ var nodeRange = goog$dom$getOwnerDocument(node).createRange();
+ if(node.nodeType == 3) {
+ nodeRange.setStart(node, 0);
+ nodeRange.setEnd(node, node.length)
+ }else {
+ for(var tempNode, leaf = node;tempNode = leaf.firstChild;)leaf = tempNode;
+ nodeRange.setStart(leaf, 0);
+ for(leaf = node;tempNode = leaf.lastChild;)leaf = tempNode;
+ nodeRange.setEnd(leaf, leaf.nodeType == 1 ? leaf.childNodes.length : leaf.length)
+ }return nodeRange
+}, goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) {
+ var nodeRange = goog$dom$getOwnerDocument(startNode).createRange();
+ nodeRange.setStart(startNode, startOffset);
+ nodeRange.setEnd(endNode, endOffset);
+ return nodeRange
+};
+goog$dom$browserrange$W3cRange.prototype.getContainer = function() {
+ return this.range_.commonAncestorContainer
+};
+goog$dom$browserrange$W3cRange.prototype.getStartNode = function() {
+ return this.range_.startContainer
+};
+goog$dom$browserrange$W3cRange.prototype.getStartOffset = function() {
+ return this.range_.startOffset
+};
+goog$dom$browserrange$W3cRange.prototype.getEndNode = function() {
+ return this.range_.endContainer
+};
+goog$dom$browserrange$W3cRange.prototype.getEndOffset = function() {
+ return this.range_.endOffset
+};
+goog$dom$browserrange$W3cRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.START_TO_END : thisEndpoint == 1 ? goog$global.Range.END_TO_START : goog$global.Range.END_TO_END, range)
+};
+goog$dom$browserrange$W3cRange.prototype.isCollapsed = function() {
+ return this.range_.collapsed
+};
+goog$dom$browserrange$W3cRange.prototype.select = function(reverse) {
+ var win = goog$dom$getWindow(goog$dom$getOwnerDocument(this.getStartNode()));
+ this.selectInternal(win.getSelection(), reverse)
+};
+goog$dom$browserrange$W3cRange.prototype.selectInternal = function(selection) {
+ selection.addRange(this.range_)
+};
+goog$dom$browserrange$W3cRange.prototype.collapse = function(toStart) {
+ this.range_.collapse(toStart)
+};var goog$dom$browserrange$GeckoRange = function(range) {
+ goog$dom$browserrange$W3cRange.call(this, range)
+};
+goog$inherits(goog$dom$browserrange$GeckoRange, goog$dom$browserrange$W3cRange);
+goog$dom$browserrange$GeckoRange.prototype.selectInternal = function(selection, reversed) {
+ var anchorNode = reversed ? this.getEndNode() : this.getStartNode(), anchorOffset = reversed ? this.getEndOffset() : this.getStartOffset(), focusNode = reversed ? this.getStartNode() : this.getEndNode(), focusOffset = reversed ? this.getStartOffset() : this.getEndOffset();
+ selection.collapse(anchorNode, anchorOffset);
+ if(anchorNode != focusNode || anchorOffset != focusOffset)selection.extend(focusNode, focusOffset)
+};var goog$dom$browserrange$IeRange = function(range, doc) {
+ this.range_ = range;
+ this.doc_ = doc
+};
+goog$inherits(goog$dom$browserrange$IeRange, goog$dom$browserrange$AbstractRange);
+var goog$dom$browserrange$IeRange$logger_ = goog$debug$LogManager$getLogger("goog.dom.browserrange.IeRange"), goog$dom$browserrange$IeRange$getBrowserRangeForNode_ = function(node) {
+ var nodeRange = goog$dom$getOwnerDocument(node).body.createTextRange();
+ if(node.nodeType == 1)nodeRange.moveToElementText(node);
+ else {
+ for(var offset = 0, sibling = node;sibling = sibling.previousSibling;) {
+ var nodeType = sibling.nodeType;
+ if(nodeType == 3)offset += sibling.length;
+ else if(nodeType == 1) {
+ nodeRange.moveToElementText(sibling);
+ break
+ }
+ }sibling || nodeRange.moveToElementText(node.parentNode);
+ nodeRange.collapse(!sibling);
+ offset && nodeRange.move("character", offset);
+ nodeRange.moveEnd("character", node.length)
+ }return nodeRange
+}, goog$dom$browserrange$IeRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) {
+ var child, collapse = false;
+ if(startNode.nodeType == 1) {
+ startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have startOffset > startNode child count");
+ child = startNode.childNodes[startOffset];
+ collapse = !child;
+ startNode = child || startNode;
+ startOffset = 0
+ }var leftRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(startNode);
+ startOffset && leftRange.move("character", startOffset);
+ collapse && leftRange.collapse(false);
+ collapse = false;
+ if(endNode.nodeType == 1) {
+ startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have endOffset > endNode child count");
+ endNode = (child = endNode.childNodes[endOffset]) || endNode;
+ if(endNode.tagName == "BR")endOffset = 1;
+ else {
+ endOffset = 0;
+ collapse = !child
+ }
+ }var rightRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(endNode);
+ rightRange.collapse(!collapse);
+ endOffset && rightRange.moveEnd("character", endOffset);
+ leftRange.setEndPoint("EndToEnd", rightRange);
+ return leftRange
+}, goog$dom$browserrange$IeRange$createFromNodeContents = function(node) {
+ var range = new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(node), goog$dom$getOwnerDocument(node));
+ range.parentNode_ = node;
+ return range
+};
+goog$dom$browserrange$IeRange.prototype.parentNode_ = null;
+goog$dom$browserrange$IeRange.prototype.startNode_ = null;
+goog$dom$browserrange$IeRange.prototype.endNode_ = null;
+goog$dom$browserrange$IeRange.prototype.clearCachedValues_ = function() {
+ this.parentNode_ = this.startNode_ = this.endNode_ = null
+};
+goog$dom$browserrange$IeRange.prototype.getContainer = function() {
+ if(!this.parentNode_) {
+ for(var selectText = this.range_.text, i = 1;selectText.charAt(selectText.length - i) == " ";i++)this.range_.moveEnd("character", -1);
+ for(var parent = this.range_.parentElement(), htmlText = this.range_.htmlText.replace(/(\r\n|\r|\n)+/g, " ");htmlText.length > parent.outerHTML.replace(/(\r\n|\r|\n)+/g, " ").length;)parent = parent.parentNode;
+ for(;parent.childNodes.length == 1 && parent.innerText == (parent.firstChild.nodeType == 3 ? parent.firstChild.nodeValue : parent.firstChild.innerText);) {
+ if(parent.firstChild.tagName == "IMG")break;
+ parent = parent.firstChild
+ }if(selectText.length == 0)parent = this.findDeepestContainer_(parent);
+ this.parentNode_ = parent
+ }return this.parentNode_
+};
+goog$dom$browserrange$IeRange.prototype.findDeepestContainer_ = function(node) {
+ for(var childNodes = node.childNodes, i = 0, len = childNodes.length;i < len;i++) {
+ var child = childNodes[i];
+ if(child.nodeType == 1)if(this.range_.inRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(child)))return this.findDeepestContainer_(child)
+ }return node
+};
+goog$dom$browserrange$IeRange.prototype.getStartNode = function() {
+ return this.startNode_ || (this.startNode_ = this.getEndpointNode_(1))
+};
+goog$dom$browserrange$IeRange.prototype.getStartOffset = function() {
+ return this.getOffset_(1)
+};
+goog$dom$browserrange$IeRange.prototype.getEndNode = function() {
+ return this.endNode_ || (this.endNode_ = this.getEndpointNode_(0))
+};
+goog$dom$browserrange$IeRange.prototype.getEndOffset = function() {
+ return this.getOffset_(0)
+};
+goog$dom$browserrange$IeRange.prototype.containsRange = function(range, opt_allowPartial) {
+ return this.containsBrowserRange(range.range_, opt_allowPartial)
+};
+goog$dom$browserrange$IeRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ return this.range_.compareEndPoints((thisEndpoint == 1 ? "Start" : "End") + "To" + (otherEndpoint == 1 ? "Start" : "End"), range)
+};
+goog$dom$browserrange$IeRange.prototype.getEndpointNode_ = function(endpoint, opt_node) {
+ var node = opt_node || this.getContainer();
+ if(!node || !node.firstChild) {
+ if(endpoint == 0 && node.previousSibling && node.previousSibling.tagName == "BR" && this.getOffset_(endpoint, node) == 0)node = node.previousSibling;
+ return node.tagName == "BR" ? node.parentNode : node
+ }for(var child = endpoint == 1 ? node.firstChild : node.lastChild;child;) {
+ if(this.containsNode(child, true))return this.getEndpointNode_(endpoint, child);
+ child = endpoint == 1 ? child.nextSibling : child.previousSibling
+ }return node
+};
+goog$dom$browserrange$IeRange.prototype.getOffset_ = function(endpoint, opt_container) {
+ var container = opt_container || (endpoint == 1 ? this.getStartNode() : this.getEndNode());
+ if(container.nodeType == 1) {
+ for(var children = container.childNodes, len = children.length, i = endpoint == 1 ? 0 : len - 1;i >= 0 && i < len;) {
+ var child = children[i];
+ if(this.containsNode(child, true)) {
+ endpoint == 0 && child.previousSibling && child.previousSibling.tagName == "BR" && this.getOffset_(endpoint, child) == 0 && i--;
+ break
+ }i += endpoint == 1 ? 1 : -1
+ }return i == -1 ? 0 : i
+ }else {
+ var range = this.range_.duplicate(), nodeRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(container);
+ range.setEndPoint(endpoint == 1 ? "EndToEnd" : "StartToStart", nodeRange);
+ var rangeLength = range.text.length;
+ return endpoint == 0 ? rangeLength : container.length - rangeLength
+ }
+};
+goog$dom$browserrange$IeRange.prototype.isCollapsed = function() {
+ return this.range_.text == ""
+};
+goog$dom$browserrange$IeRange.prototype.select = function() {
+ this.range_.select()
+};
+goog$dom$browserrange$IeRange.prototype.collapse = function(toStart) {
+ this.range_.collapse(toStart);
+ if(toStart)this.endNode_ = this.startNode_;
+ else this.startNode_ = this.endNode_
+};var goog$dom$browserrange$WebKitRange = function(range) {
+ goog$dom$browserrange$W3cRange.call(this, range)
+};
+goog$inherits(goog$dom$browserrange$WebKitRange, goog$dom$browserrange$W3cRange);
+goog$dom$browserrange$WebKitRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ if(goog$userAgent$isVersion("528"))return goog$dom$browserrange$WebKitRange.superClass_.compareBrowserRangeEndpoints.call(this, range, thisEndpoint, otherEndpoint);
+ return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.END_TO_START : thisEndpoint == 1 ? goog$global.Range.START_TO_END : goog$global.Range.END_TO_END, range)
+};
+goog$dom$browserrange$WebKitRange.prototype.selectInternal = function(selection, reversed) {
+ selection.removeAllRanges();
+ reversed ? selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(), this.getStartNode(), this.getStartOffset()) : selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};var goog$dom$browserrange$createRangeFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return goog$userAgent$IE ? new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset), goog$dom$getOwnerDocument(startNode)) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset,
+ endNode, endOffset)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset))
+};var goog$dom$TextRange = function() {
+};
+goog$inherits(goog$dom$TextRange, goog$dom$AbstractRange);
+var goog$dom$TextRange$createFromBrowserRangeWrapper_ = function(browserRange, opt_isReversed) {
+ var range = new goog$dom$TextRange;
+ range.browserRangeWrapper_ = browserRange;
+ range.isReversed_ = !!opt_isReversed;
+ return range
+}, goog$dom$TextRange$createFromNodeContents = function(node, opt_isReversed) {
+ return goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_isReversed)
+}, goog$dom$TextRange$createFromNodes = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+ var range = new goog$dom$TextRange;
+ range.isReversed_ = goog$dom$Range$isReversed(anchorNode, anchorOffset, focusNode, focusOffset);
+ if(anchorNode.tagName == "BR") {
+ var parent = anchorNode.parentNode;
+ anchorOffset = goog$array$indexOf(parent.childNodes, anchorNode);
+ anchorNode = parent
+ }if(focusNode.tagName == "BR") {
+ parent = focusNode.parentNode;
+ focusOffset = goog$array$indexOf(parent.childNodes, focusNode);
+ focusNode = parent
+ }if(range.isReversed_) {
+ range.startNode_ = focusNode;
+ range.startOffset_ = focusOffset;
+ range.endNode_ = anchorNode;
+ range.endOffset_ = anchorOffset
+ }else {
+ range.startNode_ = anchorNode;
+ range.startOffset_ = anchorOffset;
+ range.endNode_ = focusNode;
+ range.endOffset_ = focusOffset
+ }return range
+};
+goog$dom$TextRange.prototype.browserRangeWrapper_ = null;
+goog$dom$TextRange.prototype.startNode_ = null;
+goog$dom$TextRange.prototype.startOffset_ = null;
+goog$dom$TextRange.prototype.endNode_ = null;
+goog$dom$TextRange.prototype.endOffset_ = null;
+goog$dom$TextRange.prototype.isReversed_ = false;
+goog$dom$TextRange.prototype.getType = function() {
+ return"text"
+};
+goog$dom$TextRange.prototype.getBrowserRangeObject = function() {
+ return this.getBrowserRangeWrapper_().range_
+};
+goog$dom$TextRange.prototype.clearCachedValues_ = function() {
+ this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null
+};
+goog$dom$TextRange.prototype.getTextRangeCount = function() {
+ return 1
+};
+goog$dom$TextRange.prototype.getTextRange = function() {
+ return this
+};
+goog$dom$TextRange.prototype.getBrowserRangeWrapper_ = function() {
+ return this.browserRangeWrapper_ || (this.browserRangeWrapper_ = goog$dom$browserrange$createRangeFromNodes(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()))
+};
+goog$dom$TextRange.prototype.getContainer = function() {
+ return this.getBrowserRangeWrapper_().getContainer()
+};
+goog$dom$TextRange.prototype.getStartNode = function() {
+ return this.startNode_ || (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode())
+};
+goog$dom$TextRange.prototype.getStartOffset = function() {
+ return this.startOffset_ != null ? this.startOffset_ : (this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset())
+};
+goog$dom$TextRange.prototype.getEndNode = function() {
+ return this.endNode_ || (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode())
+};
+goog$dom$TextRange.prototype.getEndOffset = function() {
+ return this.endOffset_ != null ? this.endOffset_ : (this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset())
+};
+goog$dom$TextRange.prototype.isReversed = function() {
+ return this.isReversed_
+};
+goog$dom$TextRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+ var otherRangeType = otherRange.getType();
+ if(otherRangeType == "text")return this.getBrowserRangeWrapper_().containsRange(otherRange.getBrowserRangeWrapper_(), opt_allowPartial);
+ else if(otherRangeType == "control") {
+ var elements = otherRange.getElements(), fn = opt_allowPartial ? goog$array$some : goog$array$every;
+ return fn(elements, function(el) {
+ return this.containsNode(el, opt_allowPartial)
+ }, this)
+ }
+};
+goog$dom$TextRange.prototype.isCollapsed = function() {
+ return this.getBrowserRangeWrapper_().isCollapsed()
+};
+goog$dom$TextRange.prototype.__iterator__ = function() {
+ return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};
+goog$dom$TextRange.prototype.select = function() {
+ this.getBrowserRangeWrapper_().select(this.isReversed_)
+};
+goog$dom$TextRange.prototype.saveUsingDom = function() {
+ return new goog$dom$DomSavedTextRange_(this)
+};
+goog$dom$TextRange.prototype.collapse = function(toAnchor) {
+ var toStart = this.isReversed() ? !toAnchor : toAnchor;
+ this.browserRangeWrapper_ && this.browserRangeWrapper_.collapse(toStart);
+ if(toStart) {
+ this.endNode_ = this.startNode_;
+ this.endOffset_ = this.startOffset_
+ }else {
+ this.startNode_ = this.endNode_;
+ this.startOffset_ = this.endOffset_
+ }this.isReversed_ = false
+};
+var goog$dom$DomSavedTextRange_ = function(range) {
+ this.anchorNode_ = range.getAnchorNode();
+ this.anchorOffset_ = range.getAnchorOffset();
+ this.focusNode_ = range.getFocusNode();
+ this.focusOffset_ = range.getFocusOffset()
+};
+goog$inherits(goog$dom$DomSavedTextRange_, goog$dom$SavedRange);var goog$dom$ControlRange = function() {
+};
+goog$inherits(goog$dom$ControlRange, goog$dom$AbstractMultiRange);
+goog$dom$ControlRange.prototype.range_ = null;
+goog$dom$ControlRange.prototype.elements_ = null;
+goog$dom$ControlRange.prototype.sortedElements_ = null;
+goog$dom$ControlRange.prototype.clearCachedValues_ = function() {
+ this.sortedElements_ = this.elements_ = null
+};
+goog$dom$ControlRange.prototype.getType = function() {
+ return"control"
+};
+goog$dom$ControlRange.prototype.getBrowserRangeObject = function() {
+ return this.range_ || document.body.createControlRange()
+};
+goog$dom$ControlRange.prototype.getTextRangeCount = function() {
+ return this.range_ ? this.range_.length : 0
+};
+goog$dom$ControlRange.prototype.getTextRange = function(i) {
+ return goog$dom$TextRange$createFromNodeContents(this.range_.item(i))
+};
+goog$dom$ControlRange.prototype.getContainer = function() {
+ return goog$dom$findCommonAncestor.apply(null, this.getElements())
+};
+goog$dom$ControlRange.prototype.getStartNode = function() {
+ return this.getSortedElements()[0]
+};
+goog$dom$ControlRange.prototype.getStartOffset = function() {
+ return 0
+};
+goog$dom$ControlRange.prototype.getEndNode = function() {
+ var sorted = this.getSortedElements(), startsLast = goog$array$peek(sorted);
+ return goog$array$find(sorted, function(el) {
+ return goog$dom$contains(el, startsLast)
+ })
+};
+goog$dom$ControlRange.prototype.getEndOffset = function() {
+ return this.getEndNode().childNodes.length
+};
+goog$dom$ControlRange.prototype.getElements = function() {
+ if(!this.elements_) {
+ this.elements_ = [];
+ if(this.range_)for(var i = 0;i < this.range_.length;i++)this.elements_.push(this.range_.item(i))
+ }return this.elements_
+};
+goog$dom$ControlRange.prototype.getSortedElements = function() {
+ if(!this.sortedElements_) {
+ this.sortedElements_ = this.getElements().concat();
+ this.sortedElements_.sort(function(a, b) {
+ return a.sourceIndex - b.sourceIndex
+ })
+ }return this.sortedElements_
+};
+goog$dom$ControlRange.prototype.isCollapsed = function() {
+ return!this.range_ || !this.range_.length
+};
+goog$dom$ControlRange.prototype.__iterator__ = function() {
+ return new goog$dom$ControlRangeIterator(this)
+};
+goog$dom$ControlRange.prototype.select = function() {
+ this.range_ && this.range_.select()
+};
+goog$dom$ControlRange.prototype.saveUsingDom = function() {
+ return new goog$dom$DomSavedControlRange_(this)
+};
+goog$dom$ControlRange.prototype.collapse = function() {
+ this.range_ = null;
+ this.clearCachedValues_()
+};
+var goog$dom$DomSavedControlRange_ = function(range) {
+ this.elements_ = range.getElements()
+};
+goog$inherits(goog$dom$DomSavedControlRange_, goog$dom$SavedRange);
+var goog$dom$ControlRangeIterator = function(range) {
+ if(range) {
+ this.elements_ = range.getSortedElements();
+ this.startNode_ = this.elements_.shift();
+ this.endNode_ = goog$array$peek(this.elements_) || this.startNode_
+ }goog$dom$RangeIterator.call(this, this.startNode_, false)
+};
+goog$inherits(goog$dom$ControlRangeIterator, goog$dom$RangeIterator);
+goog$dom$ControlRangeIterator.prototype.startNode_ = null;
+goog$dom$ControlRangeIterator.prototype.endNode_ = null;
+goog$dom$ControlRangeIterator.prototype.elements_ = null;
+goog$dom$ControlRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog$dom$ControlRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog$dom$ControlRangeIterator.prototype.isLast = function() {
+ return!this.depth && !this.elements_.length
+};
+goog$dom$ControlRangeIterator.prototype.next = function() {
+ if(this.isLast())throw goog$iter$StopIteration;else if(!this.depth) {
+ var el = this.elements_.shift();
+ this.setPosition(el, 1, 1);
+ return el
+ }return goog$dom$ControlRangeIterator.superClass_.next.call(this)
+};var goog$dom$MultiRange = function() {
+ this.browserRanges_ = [];
+ this.ranges_ = [];
+ this.container_ = this.sortedRanges_ = null
+};
+goog$inherits(goog$dom$MultiRange, goog$dom$AbstractMultiRange);
+goog$dom$MultiRange.prototype.logger_ = goog$debug$LogManager$getLogger("goog.dom.MultiRange");
+goog$dom$MultiRange.prototype.clearCachedValues_ = function() {
+ this.ranges_ = [];
+ this.container_ = this.sortedRanges_ = null
+};
+goog$dom$MultiRange.prototype.getType = function() {
+ return"mutli"
+};
+goog$dom$MultiRange.prototype.getBrowserRangeObject = function() {
+ this.browserRanges_.length > 1 && this.logger_.warning("getBrowserRangeObject called on MultiRange with more than 1 range");
+ return this.browserRanges_[0]
+};
+goog$dom$MultiRange.prototype.getTextRangeCount = function() {
+ return this.browserRanges_.length
+};
+goog$dom$MultiRange.prototype.getTextRange = function(i) {
+ this.ranges_[i] || (this.ranges_[i] = goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? new goog$dom$browserrange$IeRange(this.browserRanges_[i], goog$dom$getOwnerDocument(this.browserRanges_[i].parentElement())) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(this.browserRanges_[i]) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(this.browserRanges_[i]) : new goog$dom$browserrange$W3cRange(this.browserRanges_[i]), undefined));
+ return this.ranges_[i]
+};
+goog$dom$MultiRange.prototype.getContainer = function() {
+ if(!this.container_) {
+ for(var nodes = [], i = 0, len = this.getTextRangeCount();i < len;i++)nodes.push(this.getTextRange(i).getContainer());
+ this.container_ = goog$dom$findCommonAncestor.apply(null, nodes)
+ }return this.container_
+};
+goog$dom$MultiRange.prototype.getSortedRanges = function() {
+ if(!this.sortedRanges_) {
+ this.sortedRanges_ = this.getTextRanges();
+ this.sortedRanges_.sort(function(a, b) {
+ var aStartNode = a.getStartNode(), aStartOffset = a.getStartOffset(), bStartNode = b.getStartNode(), bStartOffset = b.getStartOffset();
+ if(aStartNode == bStartNode && aStartOffset == bStartOffset)return 0;
+ return goog$dom$Range$isReversed(aStartNode, aStartOffset, bStartNode, bStartOffset) ? 1 : -1
+ })
+ }return this.sortedRanges_
+};
+goog$dom$MultiRange.prototype.getStartNode = function() {
+ return this.getSortedRanges()[0].getStartNode()
+};
+goog$dom$MultiRange.prototype.getStartOffset = function() {
+ return this.getSortedRanges()[0].getStartOffset()
+};
+goog$dom$MultiRange.prototype.getEndNode = function() {
+ return goog$array$peek(this.getSortedRanges()).getEndNode()
+};
+goog$dom$MultiRange.prototype.getEndOffset = function() {
+ return goog$array$peek(this.getSortedRanges()).getEndOffset()
+};
+goog$dom$MultiRange.prototype.isCollapsed = function() {
+ return this.browserRanges_.length == 0 || this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed()
+};
+goog$dom$MultiRange.prototype.__iterator__ = function() {
+ return new goog$dom$MultiRangeIterator(this)
+};
+goog$dom$MultiRange.prototype.select = function() {
+ var selection;
+ JSCompiler_inline_label_goog$dom$AbstractRange$getBrowserSelectionForWindow_50: {
+ var JSCompiler_inline_win = this.getWindow();
+ if(JSCompiler_inline_win.getSelection)selection = JSCompiler_inline_win.getSelection();
+ else {
+ var JSCompiler_inline_doc = JSCompiler_inline_win.document;
+ selection = JSCompiler_inline_doc.selection || JSCompiler_inline_doc.getSelection && JSCompiler_inline_doc.getSelection()
+ }
+ }selection.removeAllRanges();
+ for(var i = 0, len = this.getTextRangeCount();i < len;i++)selection.addRange(this.getTextRange(i).getBrowserRangeObject())
+};
+goog$dom$MultiRange.prototype.saveUsingDom = function() {
+ return new goog$dom$DomSavedMultiRange_(this)
+};
+goog$dom$MultiRange.prototype.collapse = function(toAnchor) {
+ if(!this.isCollapsed()) {
+ var range = toAnchor ? this.getTextRange(0) : this.getTextRange(this.getTextRangeCount() - 1);
+ this.clearCachedValues_();
+ range.collapse(toAnchor);
+ this.ranges_ = [range];
+ this.sortedRanges_ = [range];
+ this.browserRanges_ = [range.getBrowserRangeObject()]
+ }
+};
+var goog$dom$DomSavedMultiRange_ = function(range) {
+ this.savedRanges_ = goog$array$map(range.getTextRanges(), function(range) {
+ return range.saveUsingDom()
+ })
+};
+goog$inherits(goog$dom$DomSavedMultiRange_, goog$dom$SavedRange);
+var goog$dom$MultiRangeIterator = function(range) {
+ if(range) {
+ this.ranges_ = range.getSortedRanges();
+ if(this.ranges_.length) {
+ this.startNode_ = this.ranges_[0].getStartNode();
+ this.endNode_ = goog$array$peek(this.ranges_).getEndNode()
+ }
+ }goog$dom$RangeIterator.call(this, this.startNode_, false)
+};
+goog$inherits(goog$dom$MultiRangeIterator, goog$dom$RangeIterator);
+goog$dom$MultiRangeIterator.prototype.startNode_ = null;
+goog$dom$MultiRangeIterator.prototype.endNode_ = null;
+goog$dom$MultiRangeIterator.prototype.ranges_ = null;
+goog$dom$MultiRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog$dom$MultiRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog$dom$MultiRangeIterator.prototype.isLast = function() {
+ return this.ranges_.length == 1 && this.ranges_[0].isLast()
+};
+goog$dom$MultiRangeIterator.prototype.next = function() {
+ do try {
+ this.ranges_[0].next();
+ break
+ }catch(ex) {
+ if(ex != goog$iter$StopIteration)throw ex;this.ranges_.shift()
+ }while(this.ranges_.length);
+ if(this.ranges_.length) {
+ var range = this.ranges_[0];
+ this.setPosition(range.node, range.tagType, range.depth);
+ return range.node
+ }else throw goog$iter$StopIteration;
+};var goog$dom$Range$createCaret = function(node, offset) {
+ return goog$dom$TextRange$createFromNodes(node, offset, node, offset)
+}, goog$dom$Range$createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return goog$dom$TextRange$createFromNodes(startNode, startOffset, endNode, endOffset)
+}, goog$dom$Range$isReversed = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+ if(anchorNode == focusNode)return focusOffset < anchorOffset;
+ var child;
+ if(anchorNode.nodeType == 1 && anchorOffset)if(child = anchorNode.childNodes[anchorOffset]) {
+ anchorNode = child;
+ anchorOffset = 0
+ }else if(goog$dom$contains(anchorNode, focusNode))return true;
+ if(focusNode.nodeType == 1 && focusOffset)if(child = focusNode.childNodes[focusOffset]) {
+ focusNode = child;
+ focusOffset = 0
+ }else if(goog$dom$contains(focusNode, anchorNode))return false;
+ return(goog$dom$compareNodeOrder(anchorNode, focusNode) || anchorOffset - focusOffset) > 0
+};window.createCaret = goog$dom$Range$createCaret;
+window.createFromNodes = goog$dom$Range$createFromNodes;
+try {
+ goog$dom$Range$createCaret(document.body, 0).select()
+}catch(e$$13) {
+};
+
+/**************************************************
+ Trace:
+ 56.427 Start Handling request
+ 0 56.427 Start Building cUnit
+ 1 56.428 Done 1 ms Building cUnit
+ 0 56.428 Start Checking memcacheg
+ 0 56.428 Start Connecting to memcacheg
+ 8 56.436 Done 8 ms Connecting to memcacheg
+ 1 56.437 Done 9 ms Checking memcacheg
+ 0 56.437 Done 10 ms Handling request
+**************************************************/
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html b/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html
new file mode 100644
index 0000000000..140f1a2045
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html
@@ -0,0 +1,1081 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Rich Text Tests</title>
+ <script src="js/range.js"></script>
+ <script>
+ /**
+ * Color class allows cross-browser comparison of values, which can
+ * be returned from queryCommandValue in several formats:
+ * 0xff00ff
+ * rgb(255, 0, 0)
+ * Number containing the hex value
+ */
+ function Color(value) {
+ this.compare = function(other) {
+ if (!this.valid || !other.valid) {
+ return false;
+ }
+ return this.red == other.red && this.green == other.green && this.blue == other.blue;
+ }
+ this.parse = function(value) {
+ var hexMatch = String(value).match(/#([0-9a-f]{6})/i);
+ if (hexMatch) {
+ this.red = parseInt(hexMatch[1].substring(0, 2), 16);
+ this.green = parseInt(hexMatch[1].substring(2, 4), 16);
+ this.blue = parseInt(hexMatch[1].substring(4, 6), 16);
+ return true;
+ }
+ var rgbMatch = String(value).match(/rgb\(([0-9]{1,3}),\s*([0-9]{1,3}),\s*([0-9]{1,3})\)/i);
+ if (rgbMatch) {
+ this.red = Number(rgbMatch[1]);
+ this.green = Number(rgbMatch[2]);
+ this.blue = Number(rgbMatch[3]);
+ return true;
+ }
+ if (Number(value)) {
+ this.red = value & 0xFF;
+ this.green = (value & 0xFF00) >> 8;
+ this.blue = (value & 0xFF0000) >> 16;
+ return true;
+ }
+ return false;
+ }
+ this.toString = function() {
+ return this.red + ',' + this.green + ',' + this.blue;
+ }
+ this.valid = this.parse(value);
+ }
+
+ /**
+ * Utility class for converting font sizes to the size
+ * attribute in a font tag. Currently only converts px because
+ * only the sizes and px ever come from queryCommandValue.
+ */
+ function Size(value) {
+ var pxMatch = String(value).match(/([0-9]+)px/);
+ if (pxMatch) {
+ var px = Number(pxMatch[1]);
+ if (px <= 10) {
+ this.size = 1;
+ } else if (px <= 13) {
+ this.size = 2;
+ } else if (px <= 16) {
+ this.size = 3;
+ } else if (px <= 18) {
+ this.size = 4;
+ } else if (px <= 24) {
+ this.size = 5;
+ } else if (px <= 32) {
+ this.size = 6;
+ } else if (px <= 47) {
+ this.size = 7;
+ } else {
+ this.size = NaN;
+ }
+ } else if (Number(value)) {
+ this.size = Number(value);
+ } else {
+ switch (value) {
+ case 'x-small':
+ this.size = 1;
+ break;
+ case 'small':
+ this.size = 2;
+ break;
+ case 'medium':
+ this.size = 3;
+ break;
+ case 'large':
+ this.size = 4;
+ break;
+ case 'x-large':
+ this.size = 5;
+ break;
+ case 'xx-large':
+ this.size = 6;
+ break;
+ case 'xxx-large':
+ case '-webkit-xxx-large':
+ this.size = 7;
+ break;
+ default:
+ this.size = null;
+ }
+ }
+ this.compare = function(other) {
+ return this.size == other.size;
+ }
+ this.toString = function() {
+ return String(this.size);
+ }
+ }
+
+ var IMAGE_URI = '/tests/editor/libeditor/tests/green.png';
+
+ var APPLY_TESTS = {
+ 'backcolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'background-color'},
+ 'bold' : {
+ opt_arg: null,
+ styleWithCSS: 'font-weight'},
+ 'createbookmark' : {
+ opt_arg: 'bookmark_name'},
+ 'createlink' : {
+ opt_arg: 'http://www.openweb.org'},
+ 'decreasefontsize' : {
+ opt_arg: null},
+ 'fontname' : {
+ opt_arg: 'Arial',
+ styleWithCSS: 'font-family'},
+ 'fontsize' : {
+ opt_arg: 4,
+ styleWithCSS: 'font-size'},
+ 'forecolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'color'},
+ 'formatblock' : {
+ opt_arg: 'h1',
+ wholeline: true},
+ 'hilitecolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'background-color'},
+ 'indent' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'margin'},
+ 'inserthorizontalrule' : {
+ opt_arg: null,
+ collapse: true},
+ 'inserthtml': {
+ opt_arg: '<br>',
+ collapse: true},
+ 'insertimage': {
+ opt_arg: IMAGE_URI,
+ collapse: true},
+ 'insertorderedlist' : {
+ opt_arg: null,
+ wholeline: true},
+ 'insertunorderedlist' : {
+ opt_arg: null,
+ wholeline: true},
+ 'italic' : {
+ opt_arg: null,
+ styleWithCSS: 'font-style'},
+ 'justifycenter' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyfull' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyleft' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyright' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'strikethrough' : {
+ opt_arg: null,
+ styleWithCSS: 'text-decoration'},
+ 'subscript' : {
+ opt_arg: null,
+ styleWithCSS: 'vertical-align'},
+ 'superscript' : {
+ opt_arg: null,
+ styleWithCSS: 'vertical-align'},
+ 'underline' : {
+ opt_arg: null,
+ styleWithCSS: 'text-decoration'}};
+
+ var UNAPPLY_TESTS = {
+ 'bold' : {
+ tags: [
+ ['<b>', '</b>'],
+ ['<STRONG>', '</STRONG>'],
+ ['<span style="font-weight: bold;">', '</span>']]},
+ 'italic' : {
+ tags: [
+ ['<i>', '</i>'],
+ ['<EM>', '</EM>'],
+ ['<span style="font-style: italic;">', '</span>']]},
+ 'outdent' : {
+ unapply: 'indent',
+ block: true,
+ tags: [
+ ['<blockquote>', '</blockquote>'],
+ ['<blockquote class="webkit-indent-blockquote" style="margin: 0 0 0 40px; border: none; padding: 0px;">', '</blockquote>'],
+ ['<ul><li>', '</li></ul>'],
+ ['<ol><li>', '</li></ol>'],
+ ['<div style="margin-left: 40px;">', '</div>']]},
+ 'removeformat' : {
+ unapply: '*',
+ block: true,
+ tags: [
+ ['<b>', '</b>'],
+ ['<a href="http://www.foo.com">', '</a>'],
+ ['<table><tr><td>', '</td></tr></table>']]},
+ 'strikethrough' : {
+ tags: [
+ ['<strike>', '</strike>'],
+ ['<s>', '</s>'],
+ ['<del>', '</del>'],
+ ['<span style="text-decoration: line-through;">', '</span>']]},
+ 'subscript' : {
+ tags: [
+ ['<sub>', '</sub>'],
+ ['<span style="vertical-align: sub;">', '</span>']]},
+ 'superscript' : {
+ tags: [
+ ['<sup>', '</sup>'],
+ ['<span style="vertical-align: super;">', '</span>']]},
+ 'unbookmark' : {
+ unapply: 'createbookmark',
+ tags: [
+ ['<a name="bookmark">', '</a>']]},
+ 'underline' : {
+ tags: [
+ ['<u>', '</u>'],
+ ['<span style="text-decoration: underline;">', '</span>']]},
+ 'unlink' : {
+ unapply: 'createbookmark',
+ tags: [
+ ['<a href="http://www.foo.com">', '</a>']]}};
+
+ var QUERY_TESTS = {
+ 'backcolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'bold' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<b>foo bar baz</b>', expected: true},
+ {html: '<STRONG>foo bar baz</STRONG>', expected: true},
+ {html: '<span style="font-weight:bold">foo bar baz</span>', expected: true},
+ {html: '<b style="font-weight:normal">foo bar baz</b>', expected: false},
+ {html: '<b><span style="font-weight:normal;">foo bar baz</span>', expected: false}
+ ]
+ },
+ 'fontname' : {
+ type: 'value',
+ tests: [
+ {html: '<font face="Arial">foo bar baz</font>', expected: 'Arial'},
+ {html: '<span style="font-family:Arial">foo bar baz</span>', expected: 'Arial'},
+ {html: '<font face="Arial" style="font-family:Courier">foo bar baz</font>', expected: 'Courier'},
+ {html: '<font face="Courier"><font face="Arial">foo bar baz</font></font>', expected: 'Arial'},
+ {html: '<span style="font-family:Courier"><font face="Arial">foo bar baz</font></span>', expected: 'Arial'}
+ ]
+ },
+ 'fontsize' : {
+ type: 'value',
+ tests: [
+ {html: '<font size=4>foo bar baz</font>', expected: new Size(4)},
+ // IE adds +1 to font size from font-size style attributes.
+ // This is hard to correct for since it does NOT add +1 to size attribute from font tag.
+ {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', expected: new Size(4)},
+ {html: '<font size=1 style="font-size:x-large;">foo bar baz</font>', expected: new Size(5)}
+ ]
+ },
+ 'forecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<font color="#ff0000">foo bar baz</font>', expected: new Color('#ff0000')},
+ {html: '<span style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'hilitecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'insertorderedlist' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<ol><li>foo bar baz</li></ol>', expected: true},
+ {html: '<ul><li>foo bar baz</li></ul>', expected: false}
+ ]
+ },
+ 'insertunorderedlist' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<ol><li>foo bar baz</li></ol>', expected: false},
+ {html: '<ul><li>foo bar baz</li></ul>', expected: true}
+ ]
+ },
+ 'italic' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<i>foo bar baz</i>', expected: true},
+ {html: '<EM>foo bar baz</EM>', expected: true},
+ {html: '<span style="font-style:italic">foo bar baz</span>', expected: true},
+ {html: '<i><span style="font-style:normal">foo bar baz</span></i>', expected: false}
+ ]
+ },
+ 'justifycenter' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="center">foo bar baz</div>', expected: true},
+ {html: '<p align="center">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: center;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyfull' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="justify">foo bar baz</div>', expected: true},
+ {html: '<p align="justify">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: justify;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyleft' : {
+ type: 'state',
+ tests: [
+ {html: '<div align="left">foo bar baz</div>', expected: true},
+ {html: '<p align="left">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: left;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyright' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="right">foo bar baz</div>', expected: true},
+ {html: '<p align="right">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: right;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'strikethrough' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<strike>foo bar baz</strike>', expected: true},
+ {html: '<strike style="text-decoration: none">foo bar baz</strike>', expected: false},
+ {html: '<s>foo bar baz</s>', expected: true},
+ {html: '<del>foo bar baz</del>', expected: true},
+ {html: '<span style="text-decoration:line-through">foo bar baz</span>', expected: true}
+ ]
+ },
+ 'subscript' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<sub>foo bar baz</sub>', expected: true}
+ ]
+ },
+ 'superscript' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<sup>foo bar baz</sup>', expected: true}
+ ]
+ },
+ 'underline' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<u>foo bar baz</u>', expected: true},
+ {html: '<a href="http://www.foo.com">foo bar baz</a>', expected: true},
+ {html: '<span style="text-decoration:underline">foo bar baz</span>', expected: true},
+ {html: '<u style="text-decoration:none">foo bar baz</u>', expected: false},
+ {html: '<a style="text-decoration:none" href="http://www.foo.com">foo bar baz</a>', expected: false}
+ ]
+ }
+ };
+
+ var CHANGE_TESTS = {
+ 'backcolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#0000ff'},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#0000ff'}
+ ]
+ },
+ 'fontname' : {
+ type: 'value',
+ tests: [
+ {html: '<font face="Arial">foo bar baz</font>', opt_arg: 'Courier'},
+ {html: '<span style="font-family:Arial">foo bar baz</span>', opt_arg: 'Courier'},
+ {html: '<font face="Arial" style="font-family:Verdana">foo bar baz</font>', opt_arg: 'Courier'},
+ {html: '<font face="Verdana"><font face="Arial">foo bar baz</font></font>', opt_arg: 'Courier'},
+ {html: '<span style="font-family:Verdana"><font face="Arial">foo bar baz</font></span>', opt_arg: 'Courier'}
+ ]
+ },
+ 'fontsize' : {
+ type: 'value',
+ tests: [
+ {html: '<font size=4>foo bar baz</font>', opt_arg: 1},
+ {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', opt_arg: 1},
+ {html: '<font size=1 style="font-size:x-small;">foo bar baz</font>', opt_arg: 5}
+ ]
+ },
+ 'forecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<font color="#ff0000">foo bar baz</font>', opt_arg: '#00ff00'},
+ {html: '<span style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'},
+ {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+ ]
+ },
+ 'hilitecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#00ff00'},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+ ]
+ }
+ };
+
+ /** The document of the editable iframe */
+ var editorDoc = null;
+ /** Dummy text to apply and unapply formatting to */
+ var TEST_CONTENT = 'foo bar baz';
+ /**
+ * Word in dummy text that should change. Formatting is sometimes applied
+ * to a single word instead of the entire text node because sometimes a
+ * style might get applied to the body node instead of wrapped around
+ * the text, and that's not what's being tested.
+ */
+ var TEST_WORD = 'bar';
+ /** Constant for indicating an action is unsupported (threw exception) */
+ var UNSUPPORTED = 'UNSUPPORTED';
+ /** <br> and <p> are acceptable HTML to be left over from block elements */
+ var BLOCK_REMOVE_TAGS = [/\s*<br>\s*/i, /\s*<p>\s*/i];
+ /** Array used to accumulate test results */
+ // Tack on the actual display tests with bogus data
+ // otherwise the beacon will fail.
+ var results = ['apply=0', 'unapply=0', 'change=0', 'query=0'];
+
+ /**
+ *
+ */
+ function resetIframe(newHtml) {
+ // These attributes can get set on the iframe by some errant execCommands
+ editorDoc.body.setAttribute('style', '');
+ editorDoc.body.setAttribute('bgcolor', '');
+ editorDoc.body.innerHTML = newHtml;
+ }
+
+ /**
+ * Finds the text node in the given node containing the given word.
+ * Returns null if not found.
+ */
+ function findTextNode(word, node) {
+ if (node.nodeType == 3) {
+ // Text node, check value.
+ if (node.data.includes(word)) {
+ return node;
+ }
+ } else if (node.nodeType == 1) {
+ // Element node, check children.
+ for (var i = 0; i < node.childNodes.length; i++) {
+ var result = findTextNode(word, node.childNodes[i]);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the selection to be collapsed at the start of the word,
+ * or the start of the editor if no word is passed in.
+ */
+ function selectStart(word) {
+ var textNode = findTextNode(word || '', editorDoc.body);
+ var startOffset = 0;
+ if (word) {
+ startOffset = textNode.data.indexOf(word);
+ }
+ var range = createCaret(textNode, startOffset);
+ range.select();
+ }
+
+ /**
+ * Selects the given word in the editor iframe.
+ */
+ function selectWord(word) {
+ var textNode = findTextNode(word, editorDoc.body);
+ if (!textNode) {
+ return;
+ }
+ var start = textNode.data.indexOf(word);
+ var range = createFromNodes(textNode, start, textNode, start + word.length);
+ range.select();
+ }
+
+ /**
+ * Gets the HTML before the text, so that we know how the browser
+ * applied a style
+ */
+ function getSurroundingTags(text) {
+ var html = editorDoc.body.innerHTML;
+ var tagStart = html.indexOf('<');
+ var index = editorDoc.body.innerHTML.indexOf(text);
+ if (tagStart == -1 || index == -1) {
+ return '';
+ }
+ return editorDoc.body.innerHTML.substring(tagStart, index);
+ }
+
+ /**
+ * Does the test for an apply execCommand.
+ */
+ function doApplyTest(command, styleWithCSS) {
+ try {
+ // Set styleWithCSS
+ try {
+ editorDoc.execCommand('styleWithCSS', false, styleWithCSS);
+ } catch (ex) {
+ // Ignore errors
+ }
+ resetIframe(TEST_CONTENT);
+ if (APPLY_TESTS[command].collapse) {
+ selectStart(TEST_WORD);
+ } else {
+ selectWord(TEST_WORD);
+ }
+ try {
+ editorDoc.execCommand(command, false, APPLY_TESTS[command].opt_arg);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ return getSurroundingTags(APPLY_TESTS[command].wholeline? TEST_CONTENT : TEST_WORD);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Outputs the result of the apply command to a table.
+ * @return {boolean} success
+ */
+ function outputApplyResult(command, result, styleWithCSS) {
+ // The apply command "succeeded" if HTML was generated.
+ var success = (result != UNSUPPORTED) && result;
+ // Except for styleWithCSS commands, which only succeed if the
+ // expected style was applied.
+ if (styleWithCSS) {
+ success = result && result.toLowerCase().includes(APPLY_TESTS[command].styleWithCSS);
+ }
+ results.push('a-' + command + '-' + (styleWithCSS ? 1 : 0) + '=' + (success ? '1' : '0'));
+
+ // Each command is displayed as a table row with 3 columns
+ var tr = document.createElement('TR');
+ tr.className = success ? 'success' : 'fail';
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: styleWithCSS
+ var td = document.createElement('TD');
+ td.innerHTML = styleWithCSS ? 'true' : 'false';
+ tr.appendChild(td);
+
+ // Column 3: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 4: generated HTML (for passing commands)
+ td = document.createElement('TD');
+ // Escape the HTML in the result for printing.
+ result = result.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+ td.innerHTML = result;
+ tr.appendChild(td);
+ var table = document.getElementById('apply_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ /**
+ * Does the test for an unapply execCommand.
+ */
+ function doUnapplyTest(command, index) {
+ try {
+ var wordStart = TEST_CONTENT.indexOf(TEST_WORD);
+ resetIframe(
+ TEST_CONTENT.substring(0, wordStart) +
+ UNAPPLY_TESTS[command].tags[index][0] +
+ TEST_WORD +
+ UNAPPLY_TESTS[command].tags[index][1] +
+ TEST_CONTENT.substring(wordStart + TEST_WORD.length));
+ selectWord(TEST_WORD);
+ try {
+ editorDoc.execCommand(command, false, UNAPPLY_TESTS[command].opt_arg || null);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ return getSurroundingTags(TEST_WORD);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Check if the given unapply execCommand succeeded. It succeeded if
+ * the following conditions are true:
+ * - The execCommand did not throw an exception
+ * - One of the following:
+ * - The html was removed after the execCommand
+ * - The html was block and the html was replaced with <p> or <br>
+ */
+ function unapplyCommandSucceeded(command, result) {
+ if (result != UNSUPPORTED) {
+ if (!result) {
+ return true;
+ } else if (UNAPPLY_TESTS[command].block) {
+ for (var i = 0; i < BLOCK_REMOVE_TAGS.length; i++) {
+ if (result.match(BLOCK_REMOVE_TAGS[i])) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Outputs the result of the unapply command to a table.
+ * @return {boolean} success
+ */
+ function outputUnapplyResult(command, result, index) {
+ // The apply command "succeeded" if HTML was removed.
+ var success = unapplyCommandSucceeded(command, result);
+ results.push('u-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Each command is displayed as a table row with 5 columns
+ var tr = document.createElement('TR');
+ tr.className = success ? 'success' : 'fail';
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: command name being unapplied
+ var td = document.createElement('TD');
+ td.innerHTML = UNAPPLY_TESTS[command].unapply || command;
+ tr.appendChild(td);
+
+ // Column 3: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 4: html being removed
+ td = document.createElement('TD');
+ // Escape the html for printing.
+ var htmlToRemove = UNAPPLY_TESTS[command].tags[index][0].replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+ td.innerHTML = htmlToRemove;
+ tr.appendChild(td);
+
+ // Column 5: resulting html
+ td = document.createElement('TD');
+ // Escape the HTML in the result for printing.
+ result = result.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+ td.innerHTML = success ? '&nbsp;' : result;
+ tr.appendChild(td);
+ var table = document.getElementById('unapply_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ /**
+ * Does a queryCommandState or queryCommandValue test for an execCommand.
+ */
+ function doQueryTest(command, index) {
+ try {
+ resetIframe(QUERY_TESTS[command].tests[index].html);
+ selectWord(TEST_WORD);
+ // Dummy val that won't match any expected vals, including false.
+ var result = UNSUPPORTED;
+ if (QUERY_TESTS[command].type == 'state') {
+ try {
+ result = editorDoc.queryCommandState(command);
+ } catch (ex) {
+ result = UNSUPPORTED;
+ }
+ } else {
+ try {
+ // A return value of false indicates the command is not supported.
+ result = editorDoc.queryCommandValue(command) || UNSUPPORTED;
+ } catch (ex) {
+ result = UNSUPPORTED;
+ }
+ }
+ return result;
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Check if the given queryCommandState or queryCommandValue succeeded.
+ */
+ function queryCommandSucceeded(command, index, result) {
+ var expected = QUERY_TESTS[command].tests[index].expected;
+ if (expected instanceof Color) {
+ return expected.compare(new Color(result));
+ } else if (expected instanceof Size) {
+ return expected.compare(new Size(result));
+ } else {
+ return (result == expected);
+ }
+ }
+
+ /**
+ * @return {boolean} success
+ */
+ function outputQueryResult(command, index, result) {
+ // Create table row for results.
+ var tr = document.createElement('TR');
+ var success = queryCommandSucceeded(command, index, result);
+ tr.className = success ? 'success' : 'fail';
+ results.push('q-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 3: test HTML
+ td = document.createElement('TD');
+ var testHtml = QUERY_TESTS[command].tests[index].html.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+ td.innerHTML = testHtml.substring(0, testHtml.indexOf(TEST_CONTENT));
+ tr.appendChild(td);
+
+ // Column 4: Expected result
+ td = document.createElement('TD');
+ td.innerHTML = QUERY_TESTS[command].tests[index].expected;
+ tr.appendChild(td);
+
+ // Column 5: Actual result
+ td = document.createElement('TD');
+ td.innerHTML = result;
+ tr.appendChild(td);
+
+ // Append result to the state or value table, depending on what
+ // type of command this is.
+ var table = document.getElementById(
+ QUERY_TESTS[command].type == 'state' ? 'querystate_output' : 'queryvalue_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ function doChangeTest(command, index) {
+ try {
+ resetIframe(CHANGE_TESTS[command].tests[index].html);
+ selectWord(TEST_CONTENT);
+ try {
+ editorDoc.execCommand(command, false, CHANGE_TESTS[command].tests[index].opt_arg);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ function checkChangeSuccess(command, index) {
+ var textNode = findTextNode(TEST_CONTENT, editorDoc.body);
+ if (!textNode) {
+ // The text has been removed from the document, or split up for no reason.
+ return false;
+ }
+ var expected = null, attributeName = null, styleName = null;
+ switch (command) {
+ case 'backcolor':
+ case 'hilitecolor':
+ expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+ styleName = 'backgroundColor';
+ break;
+ case 'fontname':
+ expected = CHANGE_TESTS[command].tests[index].opt_arg;
+ attributeName = 'face';
+ styleName = 'fontFamily';
+ break;
+ case 'fontsize':
+ expected = new Size(CHANGE_TESTS[command].tests[index].opt_arg);
+ attributeName = 'size';
+ styleName = 'fontSize';
+ break;
+ case 'forecolor':
+ expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+ attributeName = 'color';
+ styleName = 'color';
+ }
+ var foundExpected = false;
+
+ // Loop through all the parent nodes that format the text node,
+ // checking that there is exactly one font attribute or
+ // style, and that it's set correctly.
+ var currentNode = textNode.parentNode;
+ while(currentNode && currentNode.nodeName != 'BODY') {
+ // Check font attribute.
+ if (attributeName && currentNode.nodeName == 'FONT' && currentNode.getAttribute(attributeName)) {
+ var foundAttribute = false;
+ switch(command) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ foundAttribute = new Color(currentNode.getAttribute(attributeName)).compare(expected);
+ break;
+ case 'fontsize':
+ foundAttribute = new Size(currentNode.getAttribute(attributeName)).compare(expected);
+ break;
+ case 'fontname':
+ foundAttribute = (currentNode.getAttribute(attributeName).toLowerCase() == expected.toLowerCase());
+ }
+ if (foundAttribute && foundExpected) {
+ // This is the correct attribute, but the style has been applied
+ // twice. This makes it hard for other browsers to remove the
+ // style.
+ return false;
+ } else if (!foundAttribute) {
+ // This node has an incorrect font attribute.
+ return false;
+ }
+ // The expected font attribute was found.
+ foundExpected = true;
+ }
+ // Check node style.
+ if (currentNode.style[styleName]) {
+ var foundStyle = false;
+ switch(command) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ foundStyle = new Color(currentNode.style[styleName]).compare(expected);
+ break;
+ case 'fontsize':
+ foundStyle = new Size(currentNode.style[styleName]).compare(expected);
+ break;
+ case 'fontname':
+ foundStyle = (currentNode.style[styleName].toLowerCase() == expected.toLowerCase());
+ }
+ if (foundStyle && foundExpected) {
+ // This is the correct style, but the style has been
+ // applied twice. This makes it hard for other browsers to
+ // remove the style.
+ return false;
+ } else if (!foundStyle) {
+ // This node has an incorrect font style.
+ return false;
+ }
+ foundExpected = true;
+ }
+ currentNode = currentNode.parentNode;
+ }
+ return foundExpected;
+ }
+
+ /**
+ * @return {boolean} success
+ */
+ function outputChangeResult(command, index) {
+ // Each command is displayed as a table row with 4 columns
+ var tr = document.createElement('TR');
+ var success = checkChangeSuccess(command, index);
+ tr.className = success ? 'success' : 'fail';
+ results.push('c-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: status
+ td = document.createElement('TD');
+ td.innerHTML = (success == null) ? '?' : (success == true ? 'PASS' : 'FAIL');
+ tr.appendChild(td);
+
+ // Column 3: opt_arg
+ td = document.createElement('TD');
+ td.innerHTML = CHANGE_TESTS[command].tests[index].opt_arg;
+ tr.appendChild(td);
+
+ // Column 4: original html
+ td = document.createElement('TD');
+ td.innerHTML = CHANGE_TESTS[command].tests[index].html.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');;
+ tr.appendChild(td);
+
+ // Column 5: resulting html
+ td = document.createElement('TD');
+ td.innerHTML = editorDoc.body.innerHTML.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');;
+ tr.appendChild(td);
+
+ var table = document.getElementById('change_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ function runTests() {
+ // Wrap initialization code in a try/catch so we can fail gracefully
+ // on older browsers.
+ try {
+ editorDoc = document.getElementById('editor').contentWindow.document;
+ // Default styleWithCSS to false, since it's not supported by IE.
+ try {
+ editorDoc.execCommand('styleWithCSS', false, false);
+ } catch (ex) {
+ // Not supported by IE.
+ }
+ } catch (ex) {}
+
+ // Apply tests
+ var apply_score = 0;
+ var apply_count = 0;
+ var unapply_score= 0;
+ var unapply_count = 0;
+ var change_score = 0;
+ var change_count = 0;
+ var query_score = 0;
+ var query_count = 0;
+ for (var command in APPLY_TESTS) {
+ try {
+ var result = doApplyTest(command, false);
+ var success = outputApplyResult(command, result, false);
+ apply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ apply_count++;
+ if (APPLY_TESTS[command].styleWithCSS) {
+ try {
+ var result = doApplyTest(command, true);
+ var success = outputApplyResult(command, result, true);
+ apply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ apply_count++;
+ }
+ }
+
+ // Unapply tests
+ for (var command in UNAPPLY_TESTS) {
+ for (var i = 0; i < UNAPPLY_TESTS[command].tags.length; i++) {
+ try {
+ var result = doUnapplyTest(command, i);
+ var success = outputUnapplyResult(command, result, i);
+ unapply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ unapply_count++;
+ }
+ }
+
+ // Query tests
+ for (var command in QUERY_TESTS) {
+ for (var i = 0; i < QUERY_TESTS[command].tests.length; i++) {
+ try {
+ var result = doQueryTest(command, i);
+ var success = outputQueryResult(command, i, result);
+ query_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ query_count++;
+ }
+ }
+
+ // Change tests
+ for (var command in CHANGE_TESTS) {
+ for (var i = 0; i < CHANGE_TESTS[command].tests.length; i++) {
+ try {
+ doChangeTest(command, i);
+ var success = outputChangeResult(command, i);
+ change_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ change_count++;
+ }
+ }
+
+ // Beacon all test results.
+ // and construct a shorter version for the results page.
+ try {
+ document.getElementById('apply-score').innerHTML =
+ apply_score + '/' + apply_count;
+ document.getElementById('unapply-score').innerHTML =
+ unapply_score + '/' + unapply_count;
+ document.getElementById('query-score').innerHTML =
+ query_score + '/' + query_count;
+ document.getElementById('change-score').innerHTML =
+ change_score + '/' + change_count;
+ } catch (ex) {}
+ var continueParams = [
+ 'apply=' + apply_score,
+ 'unapply=' + unapply_score,
+ 'query=' + query_score,
+ 'change=' + change_score
+ ];
+ parent.sendScore(results, continueParams);
+ }
+ </script>
+ <style>
+ .success {
+ background-color: #93c47d;
+ }
+ .fail {
+ background-color: #ea9999;
+ }
+ .score {
+ color: #666;
+ }
+ </style>
+</head>
+<body onload="runTests()">
+ <h1>Apply Formatting <span id="apply-score" class="score"></span></h1>
+ <table id="apply"><tbody id="apply_output"><tr><th>Command</th><th>styleWithCSS</th><th>Status</th><th>Output</th></tr></tbody></table>
+ <h1>Unapply Formatting <span id="unapply-score" class="score"></span></h1>
+ <table id="unapply">
+ <thead><tr><th>Command</th><th>Command unapplied</th><th>Status</th><th>HTML Attempted to Unapply</th><th>Resulting HTML</th></tr></thead>
+ <tbody id="unapply_output"></tbody></table>
+ <h1>Query Formatting State <span id="query-score" class="score"></span></h1>
+ <table id="querystate">
+ <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+ <tbody id="querystate_output"></tbody></table>
+ <h1>Query Formatting Value </h1>
+ <table id="queryvalue">
+ <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+ <tbody id="queryvalue_output"></tbody></table>
+ <h1>Change Formatting <span id="change-score" class="score"></span></h1>
+ <table id="change">
+ <thead><tr><th>Command</th><th>Status</th><th>Argument</th><th>Original HTML</th><th>Resulting HTML</th></tr></thead>
+ <tbody id="change_output"></tbody></table>
+ <iframe name="editor" id="editor" src="editable.html"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream b/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream
new file mode 100644
index 0000000000..2071454a85
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+set -x
+
+if test -d richtext; then
+ rm -drf richtext;
+fi
+
+svn checkout http://browserscope.googlecode.com/svn/trunk/categories/richtext/static richtext | tail -1 | sed 's/[^0-9]//g' > current_revision
+
+find richtext -type d -name .svn -exec rm -drf \{\} \; 2> /dev/null
+
+hg add current_revision richtext
+
+hg stat .
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE b/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE
new file mode 100644
index 0000000000..57bc88a15a
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/README b/editor/libeditor/tests/browserscope/lib/richtext2/README
new file mode 100644
index 0000000000..a3bc3110f4
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/README
@@ -0,0 +1,58 @@
+README FOR BROWSERSCOPE
+-----------------------
+
+Hey there - thanks for downloading the code. This file has instructions
+for getting setup so that you can run the codebase locally.
+
+This project is built on Google App Engine using the
+Django web application framework and written in Python.
+
+To get started, you'll need to first download the App Engine SDK at:
+http://code.google.com/appengine/downloads.html
+
+For local development, just startup the server:
+./pathto/google_appengine/dev_appserver.py --port=8080 browserscope
+
+You should then be able to access the local application at:
+http://localhost:8080/
+
+Note: the first time you hit the homepage it may take a little
+while - that's because it's trying to read out median times for all
+of the tests from a nonexistent datastore and write to memcache.
+Just be a lil patient.
+
+You can run the unit tests at:
+ http://localhost:8080/test
+
+
+CONTRIBUTING
+------------------
+
+Most likely you are interested in adding new tests or creating
+a new test category. If you are interested in adding tests to an existing
+"category" you may want to get in touch with the maintainer for that
+branch of the tree. We are really looking forward to receiving your
+code in patch format. Currently the category maintainers are:
+Network: Steve Souders <souders@gmail.com>
+Reflow: Lindsey Simon <elsigh@gmail.com>
+Security: Adam Barth <adam@adambarth.com> and Collin Jackson <collin@collinjackson.com>
+
+
+To create a completely new test category:
+ * Copy one of the existing directories in categories/
+ * Edit your test_set.py, handlers.py
+ * Add your files in templates/ and static/
+ * Update urls.py and settings.CATEGORIES
+ * Follow the examples of other tests re:
+ * beaconing using/testdriver_base
+ * your GetScoreAndDisplayValue method
+ * your GetRowScoreAndDisplayValue method
+
+References:
+ * App Engine Docs - http://code.google.com/appengine/docs/python/overview.html
+ * App Engine Group - http://groups.google.com/group/google-appengine
+ * Python Docs - http://www.python.org/doc/
+ * Django - http://www.djangoproject.com/
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla b/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla
new file mode 100644
index 0000000000..7d730874c6
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla
@@ -0,0 +1,27 @@
+The BrowserScope project provides a set of cross-browser HTML editor tests,
+which we import in our test suite in order to run them as part of our
+continuous integration system.
+
+We pull tests occasionally from their Subversion repository using the pull
+script which can be found in this directory. We also record the revision ID
+which we've used in the current_revision file inside this directory.
+
+Using the pull script is quite easy, just switch to this directory, and say:
+
+sh update_from_upstream
+
+There are tests which we're currently failing on, and there will probably be
+more of those in the future. We should maintain a list of the failing tests
+manually in currentStatus.js (which can also be found in this directory), to
+make sure that the suite passes entirely, with failing tests marked as todo
+items.
+
+The current status of the test suite needs to be updated whenever an editor
+bug gets fixed, which makes us pass one of the tests. When that happens,
+you should set the UPDATE_TEST_RESULTS constant to true in test_richtext2.html,
+run the test suite, paste the result JSON string in a JSON beautifier (such
+as http://jsbeautifier.org/), and use the result to update currentStatus.js.
+
+As a special case, if there are platform-specific failures, these are instead
+recorded manually in platformFailures.js. (Currently, this applies only to
+tests that are dependent on underlying platform support for the Thai script.)
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js
new file mode 100644
index 0000000000..e0a4cc0fb0
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js
@@ -0,0 +1,1768 @@
+/**
+ * The current status of the test suite.
+ *
+ * See README.Mozilla for details on how to generate this.
+ */
+const knownFailures = {
+ value: {
+ "A-Proposed-CB:name_TEXT-1_SI-dM": true,
+ "A-Proposed-CB:name_TEXT-1_SI-body": true,
+ "A-Proposed-CB:name_TEXT-1_SI-div": true,
+ "A-Proposed-DECFS:2_TEXT-1_SI-dM": true,
+ "A-Proposed-DECFS:2_TEXT-1_SI-body": true,
+ "A-Proposed-DECFS:2_TEXT-1_SI-div": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-dM": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-body": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-div": true,
+ "A-Proposed-FS:large_TEXT-1_SI-dM": true,
+ "A-Proposed-FS:large_TEXT-1_SI-body": true,
+ "A-Proposed-FS:large_TEXT-1_SI-div": true,
+ "A-Proposed-H:H1_TEXT-1_SC-dM": true,
+ "A-Proposed-H:H1_TEXT-1_SC-body": true,
+ "A-Proposed-H:H1_TEXT-1_SC-div": true,
+ "A-Proposed-INCFS:2_TEXT-1_SI-dM": true,
+ "A-Proposed-INCFS:2_TEXT-1_SI-body": true,
+ "A-Proposed-INCFS:2_TEXT-1_SI-div": true,
+ "AC-Proposed-SUB_TEXT-1_SI-dM": true,
+ "AC-Proposed-SUB_TEXT-1_SI-body": true,
+ "AC-Proposed-SUB_TEXT-1_SI-div": true,
+ "AC-Proposed-SUP_TEXT-1_SI-dM": true,
+ "AC-Proposed-SUP_TEXT-1_SI-body": true,
+ "AC-Proposed-SUP_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-div": true,
+
+ // Those tests expect that <font> elements can be nested, but they don't
+ // match with the other browsers' behavior.
+ "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-dM": true,
+ "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-body": true,
+ "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-div": true,
+
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-dM": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-body": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-div": true,
+
+ // Those tests expect that <font> elements can be nested, but they don't
+ // match with the other browsers' behavior.
+ "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-dM": true,
+ "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-body": true,
+ "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-div": true,
+
+ "C-Proposed-FS:larger_FONTsz:4-dM": true,
+ "C-Proposed-FS:larger_FONTsz:4-body": true,
+ "C-Proposed-FS:larger_FONTsz:4-div": true,
+ "C-Proposed-FS:smaller_FONTsz:4-dM": true,
+ "C-Proposed-FS:smaller_FONTsz:4-body": true,
+ "C-Proposed-FS:smaller_FONTsz:4-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-div": true,
+ "CC-Proposed-I_B-1_SW-dM": true,
+ "CC-Proposed-I_B-1_SW-body": true,
+ "CC-Proposed-I_B-1_SW-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-div": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-dM": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-body": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-div": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-dM": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-body": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-div": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-dM": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-body": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-div": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-dM": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-body": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-div": true,
+ "U-RFC-UNLINK_A-1_SO-dM": true,
+ "U-RFC-UNLINK_A-1_SO-body": true,
+ "U-RFC-UNLINK_A-1_SO-div": true,
+ "U-RFC-UNLINK_A-1_SW-dM": true,
+ "U-RFC-UNLINK_A-1_SW-body": true,
+ "U-RFC-UNLINK_A-1_SW-div": true,
+ "U-RFC-UNLINK_A-2_SO-dM": true,
+ "U-RFC-UNLINK_A-2_SO-body": true,
+ "U-RFC-UNLINK_A-2_SO-div": true,
+ "U-RFC-UNLINK_A2-1_SO-dM": true,
+ "U-RFC-UNLINK_A2-1_SO-body": true,
+ "U-RFC-UNLINK_A2-1_SO-div": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-dM": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-body": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-div": true,
+ "U-Proposed-B_B-2_SL-dM": true,
+ "U-Proposed-B_B-2_SL-body": true,
+ "U-Proposed-B_B-2_SL-div": true,
+ "U-Proposed-B_B-2_SR-dM": true,
+ "U-Proposed-B_B-2_SR-body": true,
+ "U-Proposed-B_B-2_SR-div": true,
+ "U-Proposed-U_U-S-2_SI-dM": true,
+ "U-Proposed-U_U-S-2_SI-body": true,
+ "U-Proposed-U_U-S-2_SI-div": true,
+ "U-Proposed-S_DEL-1_SW-dM": true,
+ "U-Proposed-S_DEL-1_SW-body": true,
+ "U-Proposed-S_DEL-1_SW-div": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-dM": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-body": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-div": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-dM": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-body": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-div": true,
+ "U-Proposed-UNLINK_A-1_SC-dM": true,
+ "U-Proposed-UNLINK_A-1_SC-body": true,
+ "U-Proposed-UNLINK_A-1_SC-div": true,
+ "U-Proposed-UNLINK_A-1_SI-dM": true,
+ "U-Proposed-UNLINK_A-1_SI-body": true,
+ "U-Proposed-UNLINK_A-1_SI-div": true,
+ "U-Proposed-UNLINK_A-2_SL-dM": true,
+ "U-Proposed-UNLINK_A-2_SL-body": true,
+ "U-Proposed-UNLINK_A-2_SL-div": true,
+ "U-Proposed-UNLINK_A-3_SR-dM": true,
+ "U-Proposed-UNLINK_A-3_SR-body": true,
+ "U-Proposed-UNLINK_A-3_SR-div": true,
+ "U-Proposed-OUTDENT_BQ-1_SW-dM": true,
+ "U-Proposed-OUTDENT_BQ-1_SW-body": true,
+ "U-Proposed-OUTDENT_BQ-1_SW-div": true,
+ "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-dM": true,
+ "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-body": true,
+ "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-div": true,
+ "U-Proposed-OUTDENT_OL-LI-1_SW-dM": true,
+ "U-Proposed-OUTDENT_OL-LI-1_SW-body": true,
+ "U-Proposed-OUTDENT_OL-LI-1_SW-div": true,
+ "U-Proposed-OUTDENT_UL-LI-1_SW-dM": true,
+ "U-Proposed-OUTDENT_UL-LI-1_SW-body": true,
+ "U-Proposed-OUTDENT_UL-LI-1_SW-div": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-dM": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-body": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-div": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-dM": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-body": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-div": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-dM": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-body": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-div": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-dM": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-body": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-div": true,
+ "UC-Proposed-S_SPANc:s-1_SW-dM": true,
+ "UC-Proposed-S_SPANc:s-1_SW-body": true,
+ "UC-Proposed-S_SPANc:s-1_SW-div": true,
+ "UC-Proposed-S_SPANc:s-2_SI-dM": true,
+ "UC-Proposed-S_SPANc:s-2_SI-body": true,
+ "UC-Proposed-S_SPANc:s-2_SI-div": true,
+ "D-Proposed-CHAR-3_SC-dM": true,
+ "D-Proposed-CHAR-3_SC-body": true,
+ "D-Proposed-CHAR-3_SC-div": true,
+ "D-Proposed-CHAR-4_SC-dM": true,
+ "D-Proposed-CHAR-4_SC-body": true,
+ "D-Proposed-CHAR-4_SC-div": true,
+ "D-Proposed-CHAR-5_SC-dM": true,
+ "D-Proposed-CHAR-5_SC-body": true,
+ "D-Proposed-CHAR-5_SC-div": true,
+ "D-Proposed-CHAR-5_SI-1-dM": true,
+ "D-Proposed-CHAR-5_SI-1-body": true,
+ "D-Proposed-CHAR-5_SI-1-div": true,
+ "D-Proposed-CHAR-5_SI-2-dM": true,
+ "D-Proposed-CHAR-5_SI-2-body": true,
+ "D-Proposed-CHAR-5_SI-2-div": true,
+ "D-Proposed-CHAR-5_SR-dM": true,
+ "D-Proposed-CHAR-5_SR-body": true,
+ "D-Proposed-CHAR-5_SR-div": true,
+ "D-Proposed-CHAR-6_SC-dM": true,
+ "D-Proposed-CHAR-6_SC-body": true,
+ "D-Proposed-CHAR-6_SC-div": true,
+ "D-Proposed-CHAR-7_SC-dM": true,
+ "D-Proposed-CHAR-7_SC-body": true,
+ "D-Proposed-CHAR-7_SC-div": true,
+ "D-Proposed-OL-LI-1_SW-dM": true,
+ "D-Proposed-OL-LI-1_SW-body": true,
+ "D-Proposed-OL-LI-1_SW-div": true,
+ "D-Proposed-OL-LI-1_SO-dM": true,
+ "D-Proposed-OL-LI-1_SO-body": true,
+ "D-Proposed-OL-LI-1_SO-div": true,
+ "D-Proposed-TR2rs:2-1_SO1-dM": true,
+ "D-Proposed-TR2rs:2-1_SO1-body": true,
+ "D-Proposed-TR2rs:2-1_SO1-div": true,
+ "D-Proposed-TR2rs:2-1_SO2-dM": true,
+ "D-Proposed-TR2rs:2-1_SO2-body": true,
+ "D-Proposed-TR2rs:2-1_SO2-div": true,
+ "D-Proposed-TR3rs:3-1_SO1-dM": true,
+ "D-Proposed-TR3rs:3-1_SO1-body": true,
+ "D-Proposed-TR3rs:3-1_SO1-div": true,
+ "D-Proposed-TR3rs:3-1_SO2-dM": true,
+ "D-Proposed-TR3rs:3-1_SO2-body": true,
+ "D-Proposed-TR3rs:3-1_SO2-div": true,
+ "D-Proposed-TR3rs:3-1_SO3-dM": true,
+ "D-Proposed-TR3rs:3-1_SO3-body": true,
+ "D-Proposed-TR3rs:3-1_SO3-div": true,
+ "D-Proposed-DIV:ce:false-1_SB-dM": true,
+ "D-Proposed-DIV:ce:false-1_SL-dM": true,
+ "D-Proposed-DIV:ce:false-1_SL-body": true,
+ "D-Proposed-DIV:ce:false-1_SL-div": true,
+ "D-Proposed-DIV:ce:false-1_SR-dM": true,
+ "D-Proposed-DIV:ce:false-1_SR-body": true,
+ "D-Proposed-DIV:ce:false-1_SR-div": true,
+ "D-Proposed-DIV:ce:false-1_SI-dM": true,
+ "FD-Proposed-OL-LI-1_SW-dM": true,
+ "FD-Proposed-OL-LI-1_SW-body": true,
+ "FD-Proposed-OL-LI-1_SW-div": true,
+ "FD-Proposed-OL-LI-1_SO-dM": true,
+ "FD-Proposed-OL-LI-1_SO-body": true,
+ "FD-Proposed-OL-LI-1_SO-div": true,
+ "FD-Proposed-TR2rs:2-1_SO1-dM": true,
+ "FD-Proposed-TR2rs:2-1_SO1-body": true,
+ "FD-Proposed-TR2rs:2-1_SO1-div": true,
+ "FD-Proposed-TR2rs:2-1_SO2-dM": true,
+ "FD-Proposed-TR2rs:2-1_SO2-body": true,
+ "FD-Proposed-TR2rs:2-1_SO2-div": true,
+ "FD-Proposed-TR3rs:3-1_SO1-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO1-body": true,
+ "FD-Proposed-TR3rs:3-1_SO1-div": true,
+ "FD-Proposed-TR3rs:3-1_SO2-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO2-body": true,
+ "FD-Proposed-TR3rs:3-1_SO2-div": true,
+ "FD-Proposed-TR3rs:3-1_SO3-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO3-body": true,
+ "FD-Proposed-TR3rs:3-1_SO3-div": true,
+ "FD-Proposed-DIV:ce:false-1_SB-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SL-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SL-body": true,
+ "FD-Proposed-DIV:ce:false-1_SL-div": true,
+ "FD-Proposed-DIV:ce:false-1_SR-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SR-body": true,
+ "FD-Proposed-DIV:ce:false-1_SR-div": true,
+ "FD-Proposed-DIV:ce:false-1_SI-dM": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-dM": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-body": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-div": true,
+ "I-Proposed-IIMG:._IMG-1_SO-dM": true,
+ "I-Proposed-IIMG:._IMG-1_SO-body": true,
+ "I-Proposed-IIMG:._IMG-1_SO-div": true,
+ "Q-Proposed-CONTENTREADONLY_TEXT-1-dM": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false),
+ "Q-Proposed-CONTENTREADONLY_TEXT-1-body": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false),
+ "Q-Proposed-CONTENTREADONLY_TEXT-1-div": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false),
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "Q-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true,
+ "Q-Proposed-DECREASEFONTSIZE_TEXT-1-body": true,
+ "Q-Proposed-DECREASEFONTSIZE_TEXT-1-div": true,
+ "Q-Proposed-HEADING_TEXT-1-dM": true,
+ "Q-Proposed-HEADING_TEXT-1-body": true,
+ "Q-Proposed-HEADING_TEXT-1-div": true,
+ "Q-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true,
+ "Q-Proposed-INCREASEFONTSIZE_TEXT-1-body": true,
+ "Q-Proposed-INCREASEFONTSIZE_TEXT-1-div": true,
+ "Q-Proposed-PASTE_TEXT-1-dM": true,
+ "Q-Proposed-PASTE_TEXT-1-body": true,
+ "Q-Proposed-PASTE_TEXT-1-div": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "Q-Proposed-UNSELECT_TEXT-1-dM": true,
+ "Q-Proposed-UNSELECT_TEXT-1-body": true,
+ "Q-Proposed-UNSELECT_TEXT-1-div": true,
+ "QE-Proposed-CONTENTREADONLY_TEXT-1-dM": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false),
+ "QE-Proposed-CONTENTREADONLY_TEXT-1-body": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false),
+ "QE-Proposed-CONTENTREADONLY_TEXT-1-div": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false),
+ "QE-Proposed-COPY_TEXT-1-dM": true,
+ "QE-Proposed-COPY_TEXT-1-body": true,
+ "QE-Proposed-COPY_TEXT-1-div": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "QE-Proposed-CUT_TEXT-1-dM": true,
+ "QE-Proposed-CUT_TEXT-1-body": true,
+ "QE-Proposed-CUT_TEXT-1-div": true,
+ "QE-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true,
+ "QE-Proposed-DECREASEFONTSIZE_TEXT-1-body": true,
+ "QE-Proposed-DECREASEFONTSIZE_TEXT-1-div": true,
+ "QE-Proposed-HEADING_TEXT-1-dM": true,
+ "QE-Proposed-HEADING_TEXT-1-body": true,
+ "QE-Proposed-HEADING_TEXT-1-div": true,
+ "QE-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true,
+ "QE-Proposed-INCREASEFONTSIZE_TEXT-1-body": true,
+ "QE-Proposed-INCREASEFONTSIZE_TEXT-1-div": true,
+ "QE-Proposed-PASTE_TEXT-1-dM": true,
+ "QE-Proposed-PASTE_TEXT-1-body": true,
+ "QE-Proposed-PASTE_TEXT-1-div": true,
+ "QE-Proposed-REDO_TEXT-1-dM": true,
+ "QE-Proposed-REDO_TEXT-1-body": true,
+ "QE-Proposed-REDO_TEXT-1-div": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "QE-Proposed-UNSELECT_TEXT-1-dM": true,
+ "QE-Proposed-UNSELECT_TEXT-1-body": true,
+ "QE-Proposed-UNSELECT_TEXT-1-div": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-dM": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-body": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-div": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-dM": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-body": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-div": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-dM": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-body": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-div": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-dM": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-body": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-div": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-dM": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-body": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-div": true,
+ "QS-Proposed-JC_MYJC-1-SI-dM": true,
+ "QS-Proposed-JC_MYJC-1-SI-body": true,
+ "QS-Proposed-JC_MYJC-1-SI-div": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-dM": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-body": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-div": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-dM": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-body": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-div": true,
+ "QS-Proposed-JF_MYJF-1-SI-dM": true,
+ "QS-Proposed-JF_MYJF-1-SI-body": true,
+ "QS-Proposed-JF_MYJF-1-SI-div": true,
+ "QS-Proposed-JL_TEXT_SI-dM": true,
+ "QS-Proposed-JL_TEXT_SI-body": true,
+ "QS-Proposed-JL_TEXT_SI-div": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-dM": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-body": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-div": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-dM": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-body": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-div": true,
+ "QS-Proposed-JR_MYJR-1-SI-dM": true,
+ "QS-Proposed-JR_MYJR-1-SI-body": true,
+ "QS-Proposed-JR_MYJR-1-SI-div": true,
+ "QV-Proposed-B_TEXT_SI-dM": true,
+ "QV-Proposed-B_TEXT_SI-body": true,
+ "QV-Proposed-B_TEXT_SI-div": true,
+ "QV-Proposed-B_B-1_SI-dM": true,
+ "QV-Proposed-B_B-1_SI-body": true,
+ "QV-Proposed-B_B-1_SI-div": true,
+ "QV-Proposed-B_STRONG-1_SI-dM": true,
+ "QV-Proposed-B_STRONG-1_SI-body": true,
+ "QV-Proposed-B_STRONG-1_SI-div": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-dM": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-body": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-div": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-dM": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-body": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-div": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-dM": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-body": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-div": true,
+ "QV-Proposed-B_SPAN.b-1_SI-dM": true,
+ "QV-Proposed-B_SPAN.b-1_SI-body": true,
+ "QV-Proposed-B_SPAN.b-1_SI-div": true,
+ "QV-Proposed-B_MYB-1-SI-dM": true,
+ "QV-Proposed-B_MYB-1-SI-body": true,
+ "QV-Proposed-B_MYB-1-SI-div": true,
+ "QV-Proposed-I_TEXT_SI-dM": true,
+ "QV-Proposed-I_TEXT_SI-body": true,
+ "QV-Proposed-I_TEXT_SI-div": true,
+ "QV-Proposed-I_I-1_SI-dM": true,
+ "QV-Proposed-I_I-1_SI-body": true,
+ "QV-Proposed-I_I-1_SI-div": true,
+ "QV-Proposed-I_EM-1_SI-dM": true,
+ "QV-Proposed-I_EM-1_SI-body": true,
+ "QV-Proposed-I_EM-1_SI-div": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-dM": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-body": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-div": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-dM": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-body": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-div": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-dM": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-body": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-div": true,
+ "QV-Proposed-I_SPAN.i-1_SI-dM": true,
+ "QV-Proposed-I_SPAN.i-1_SI-body": true,
+ "QV-Proposed-I_SPAN.i-1_SI-div": true,
+ "QV-Proposed-I_MYI-1-SI-dM": true,
+ "QV-Proposed-I_MYI-1-SI-body": true,
+ "QV-Proposed-I_MYI-1-SI-div": true,
+ "QV-Proposed-FB_H1-H2-1_SL-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SL-body": true,
+ "QV-Proposed-FB_H1-H2-1_SL-div": true,
+ "QV-Proposed-FB_H1-H2-1_SR-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SR-body": true,
+ "QV-Proposed-FB_H1-H2-1_SR-div": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-dM": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-body": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-div": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-dM": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-body": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-div": true,
+ "QV-Proposed-H_H1-1_SC-dM": true,
+ "QV-Proposed-H_H1-1_SC-body": true,
+ "QV-Proposed-H_H1-1_SC-div": true,
+ "QV-Proposed-H_H3-1_SC-dM": true,
+ "QV-Proposed-H_H3-1_SC-body": true,
+ "QV-Proposed-H_H3-1_SC-div": true,
+ "QV-Proposed-H_H1-H2-H3-H4-1_SC-dM": true,
+ "QV-Proposed-H_H1-H2-H3-H4-1_SC-body": true,
+ "QV-Proposed-H_H1-H2-H3-H4-1_SC-div": true,
+ "QV-Proposed-H_P-1_SC-dM": false,
+ "QV-Proposed-H_P-1_SC-body": false,
+ "QV-Proposed-H_P-1_SC-div": false,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-dM": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-body": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-div": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-dM": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-body": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-div": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-dM": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-body": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-div": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-dM": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-body": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-div": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-dM": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-body": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-div": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-dM": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-body": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-div": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-dM": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-body": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-div": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-dM": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-body": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-div": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-dM": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-body": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-div": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-dM": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-body": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-div": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-dM": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-body": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-div": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-dM": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-body": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-div": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-dM": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-body": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-div": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-dM": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-body": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-div": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-dM": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-body": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-div": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-dM": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-body": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-div": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-dM": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-body": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-div": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-dM": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-body": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-div": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-dM": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-body": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-div": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-dM": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-body": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-div": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-dM": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-body": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-div": true,
+ },
+ select: {
+ "S-Proposed-UNSEL_TEXT-1_SI-dM": true,
+ "S-Proposed-UNSEL_TEXT-1_SI-body": true,
+ "S-Proposed-UNSEL_TEXT-1_SI-div": true,
+ "S-Proposed-SM:m.f.c_TEXT-1_SI-1-dM": true,
+ "S-Proposed-SM:m.f.c_TEXT-1_SI-1-body": true,
+ "S-Proposed-SM:m.f.c_TEXT-1_SI-1-div": true,
+ "S-Proposed-SM:m.b.c_TEXT-1_SI-1-dM": true,
+ "S-Proposed-SM:m.b.c_TEXT-1_SI-1-body": true,
+ "S-Proposed-SM:m.b.c_TEXT-1_SI-1-div": true,
+ "S-Proposed-SM:m.b.w_TEXT-1_SI-1-dM": true,
+ "S-Proposed-SM:m.b.w_TEXT-1_SI-1-body": true,
+ "S-Proposed-SM:m.b.w_TEXT-1_SI-1-div": true,
+ "S-Proposed-SM:m.b.w_TEXT-th_SC-2-dM": SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true),
+ "S-Proposed-SM:m.b.w_TEXT-th_SC-2-body": SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true),
+ "S-Proposed-SM:m.b.w_TEXT-th_SC-2-div": SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true),
+ "S-Proposed-SM:m.f.c_CHAR-5_SI-2-dM": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SI-2-body": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SI-2-div": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SR-dM": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SR-body": true,
+ "S-Proposed-SM:m.f.c_CHAR-5_SR-div": true,
+ "S-Proposed-SM:m.b.c_CHAR-5_SR-dM": true,
+ "S-Proposed-SM:m.b.c_CHAR-5_SR-body": true,
+ "S-Proposed-SM:m.b.c_CHAR-5_SR-div": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-dM": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true),
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-body": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true),
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-div": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true),
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-dM": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true),
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-body": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true),
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-div": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true),
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-dM": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-body": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-div": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-3-dM": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-3-body": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-3-div": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-4-dM": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-4-body": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-4-div": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-5-dM": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-5-body": true,
+ "S-Proposed-SM:e.b.w_TEXT-1_SI-5-div": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-dM": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-body": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-div": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-dM": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-body": true,
+ "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-div": true,
+ "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-dM": true,
+ "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-body": true,
+ "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-div": true,
+ "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-dM": true,
+ "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-body": true,
+ "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-div": true,
+ "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-dM": true,
+ "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-body": true,
+ "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-div": true,
+ "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-dM": true,
+ "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-body": true,
+ "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-div": true,
+ "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-dM": true,
+ "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-body": true,
+ "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-div": true,
+ "A-Proposed-B_TEXT-1_SIR-dM": true,
+ "A-Proposed-B_TEXT-1_SIR-body": true,
+ "A-Proposed-B_TEXT-1_SIR-div": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-dM": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-body": true,
+ "A-Proposed-FS:18px_TEXT-1_SI-div": true,
+ "A-Proposed-FS:large_TEXT-1_SI-dM": true,
+ "A-Proposed-FS:large_TEXT-1_SI-body": true,
+ "A-Proposed-FS:large_TEXT-1_SI-div": true,
+ "A-Proposed-INCFS:2_TEXT-1_SI-dM": true,
+ "A-Proposed-INCFS:2_TEXT-1_SI-body": true,
+ "A-Proposed-INCFS:2_TEXT-1_SI-div": true,
+ "A-Proposed-DECFS:2_TEXT-1_SI-dM": true,
+ "A-Proposed-DECFS:2_TEXT-1_SI-body": true,
+ "A-Proposed-DECFS:2_TEXT-1_SI-div": true,
+ "A-Proposed-CB:name_TEXT-1_SI-dM": true,
+ "A-Proposed-CB:name_TEXT-1_SI-body": true,
+ "A-Proposed-CB:name_TEXT-1_SI-div": true,
+ "A-Proposed-H:H1_TEXT-1_SC-dM": true,
+ "A-Proposed-H:H1_TEXT-1_SC-body": true,
+ "A-Proposed-H:H1_TEXT-1_SC-div": true,
+ "AC-Proposed-SUB_TEXT-1_SI-dM": true,
+ "AC-Proposed-SUB_TEXT-1_SI-body": true,
+ "AC-Proposed-SUB_TEXT-1_SI-div": true,
+ "AC-Proposed-SUP_TEXT-1_SI-dM": true,
+ "AC-Proposed-SUP_TEXT-1_SI-body": true,
+ "AC-Proposed-SUP_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:2_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:18px_TEXT-1_SI-div": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-dM": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-body": true,
+ "AC-Proposed-FS:large_TEXT-1_SI-div": true,
+
+ // Those tests expect that <font> elements can be nested, but they don't
+ // match with the other browsers' behavior.
+ "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-dM": true,
+ "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-body": true,
+ "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-div": true,
+
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-dM": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-body": true,
+ "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-div": true,
+
+ // Those tests expect that <font> elements can be nested, but they don't
+ // match with the other browsers' behavior.
+ "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-dM": true,
+ "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-body": true,
+ "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-div": true,
+
+ "C-Proposed-FS:larger_FONTsz:4-dM": true,
+ "C-Proposed-FS:larger_FONTsz:4-body": true,
+ "C-Proposed-FS:larger_FONTsz:4-div": true,
+ "C-Proposed-FS:smaller_FONTsz:4-dM": true,
+ "C-Proposed-FS:smaller_FONTsz:4-body": true,
+ "C-Proposed-FS:smaller_FONTsz:4-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-div": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-dM": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-body": true,
+ "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-div": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-dM": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-body": true,
+ "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-div": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-dM": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-body": true,
+ "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-div": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:1_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-dM": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-body": true,
+ "CC-Proposed-FS:4_SPANs:fs:l-1_SW-div": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-dM": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-body": true,
+ "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-div": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-dM": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-body": true,
+ "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-div": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-dM": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-body": true,
+ "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-div": true,
+ "U-RFC-UNLINK_A-1_SO-dM": true,
+ "U-RFC-UNLINK_A-1_SO-body": true,
+ "U-RFC-UNLINK_A-1_SO-div": true,
+ "U-RFC-UNLINK_A-1_SW-dM": true,
+ "U-RFC-UNLINK_A-1_SW-body": true,
+ "U-RFC-UNLINK_A-1_SW-div": true,
+ "U-RFC-UNLINK_A-2_SO-dM": true,
+ "U-RFC-UNLINK_A-2_SO-body": true,
+ "U-RFC-UNLINK_A-2_SO-div": true,
+ "U-RFC-UNLINK_A2-1_SO-dM": true,
+ "U-RFC-UNLINK_A2-1_SO-body": true,
+ "U-RFC-UNLINK_A2-1_SO-div": true,
+ "U-Proposed-B_B-P3-1_SO12-dM": true,
+ "U-Proposed-B_B-P3-1_SO12-body": true,
+ "U-Proposed-B_B-P3-1_SO12-div": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-dM": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-body": true,
+ "U-Proposed-B_B-P-I..P-1_SO-I-div": true,
+ "U-Proposed-B_B-2_SL-dM": true,
+ "U-Proposed-B_B-2_SL-body": true,
+ "U-Proposed-B_B-2_SL-div": true,
+ "U-Proposed-B_B-2_SR-dM": true,
+ "U-Proposed-B_B-2_SR-body": true,
+ "U-Proposed-B_B-2_SR-div": true,
+ "U-Proposed-I_I-P3-1_SO2-dM": true,
+ "U-Proposed-I_I-P3-1_SO2-body": true,
+ "U-Proposed-I_I-P3-1_SO2-div": true,
+ "U-Proposed-U_U-S-2_SI-dM": true,
+ "U-Proposed-U_U-S-2_SI-body": true,
+ "U-Proposed-U_U-S-2_SI-div": true,
+ "U-Proposed-U_U-P3-1_SO-dM": true,
+ "U-Proposed-U_U-P3-1_SO-body": true,
+ "U-Proposed-U_U-P3-1_SO-div": true,
+ "U-Proposed-S_DEL-1_SW-dM": true,
+ "U-Proposed-S_DEL-1_SW-body": true,
+ "U-Proposed-S_DEL-1_SW-div": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-dM": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-body": true,
+ "U-Proposed-SUB_SPANs:va:sub-1_SW-div": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-dM": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-body": true,
+ "U-Proposed-SUP_SPANs:va:super-1_SW-div": true,
+ "U-Proposed-UNLINK_A-1_SC-dM": true,
+ "U-Proposed-UNLINK_A-1_SC-body": true,
+ "U-Proposed-UNLINK_A-1_SC-div": true,
+ "U-Proposed-UNLINK_A-1_SI-dM": true,
+ "U-Proposed-UNLINK_A-1_SI-body": true,
+ "U-Proposed-UNLINK_A-1_SI-div": true,
+ "U-Proposed-UNLINK_A-2_SL-dM": true,
+ "U-Proposed-UNLINK_A-2_SL-body": true,
+ "U-Proposed-UNLINK_A-2_SL-div": true,
+ "U-Proposed-UNLINK_A-3_SR-dM": true,
+ "U-Proposed-UNLINK_A-3_SR-body": true,
+ "U-Proposed-UNLINK_A-3_SR-div": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-dM": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-body": true,
+ "U-Proposed-OUTDENT_DIV-1_SW-div": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-dM": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-body": true,
+ "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-div": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-dM": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-body": true,
+ "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-div": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-dM": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-body": true,
+ "U-Proposed-UNBOOKMARK_An:name-1_SW-div": true,
+ "UC-Proposed-S_SPANc:s-1_SW-dM": true,
+ "UC-Proposed-S_SPANc:s-1_SW-body": true,
+ "UC-Proposed-S_SPANc:s-1_SW-div": true,
+ "UC-Proposed-S_SPANc:s-2_SI-dM": true,
+ "UC-Proposed-S_SPANc:s-2_SI-body": true,
+ "UC-Proposed-S_SPANc:s-2_SI-div": true,
+ "D-Proposed-CHAR-3_SC-dM": true,
+ "D-Proposed-CHAR-3_SC-body": true,
+ "D-Proposed-CHAR-3_SC-div": true,
+ "D-Proposed-CHAR-4_SC-dM": true,
+ "D-Proposed-CHAR-4_SC-body": true,
+ "D-Proposed-CHAR-4_SC-div": true,
+ "D-Proposed-CHAR-5_SC-dM": true,
+ "D-Proposed-CHAR-5_SC-body": true,
+ "D-Proposed-CHAR-5_SC-div": true,
+ "D-Proposed-CHAR-5_SI-1-dM": true,
+ "D-Proposed-CHAR-5_SI-1-body": true,
+ "D-Proposed-CHAR-5_SI-1-div": true,
+ "D-Proposed-CHAR-5_SI-2-dM": true,
+ "D-Proposed-CHAR-5_SI-2-body": true,
+ "D-Proposed-CHAR-5_SI-2-div": true,
+ "D-Proposed-CHAR-5_SR-dM": true,
+ "D-Proposed-CHAR-5_SR-body": true,
+ "D-Proposed-CHAR-5_SR-div": true,
+ "D-Proposed-CHAR-6_SC-dM": true,
+ "D-Proposed-CHAR-6_SC-body": true,
+ "D-Proposed-CHAR-6_SC-div": true,
+ "D-Proposed-CHAR-7_SC-dM": true,
+ "D-Proposed-CHAR-7_SC-body": true,
+ "D-Proposed-CHAR-7_SC-div": true,
+ "D-Proposed-B-1_SW-div": true,
+ "D-Proposed-B-1_SL-dM": true,
+ "D-Proposed-B-1_SL-body": true,
+ "D-Proposed-B-1_SL-div": true,
+ "D-Proposed-B-1_SR-dM": true,
+ "D-Proposed-B-1_SR-body": true,
+ "D-Proposed-B-1_SR-div": true,
+ "D-Proposed-B.I-1_SM-dM": true,
+ "D-Proposed-B.I-1_SM-body": true,
+ "D-Proposed-B.I-1_SM-div": true,
+ "D-Proposed-OL-LI2-1_SO1-dM": true,
+ "D-Proposed-OL-LI2-1_SO1-body": true,
+ "D-Proposed-OL-LI2-1_SO1-div": true,
+ "D-Proposed-OL-LI-1_SW-dM": true,
+ "D-Proposed-OL-LI-1_SW-body": true,
+ "D-Proposed-OL-LI-1_SW-div": true,
+ "D-Proposed-OL-LI-1_SO-dM": true,
+ "D-Proposed-OL-LI-1_SO-body": true,
+ "D-Proposed-OL-LI-1_SO-div": true,
+ "D-Proposed-HR.BR-1_SM-dM": true,
+ "D-Proposed-HR.BR-1_SM-body": true,
+ "D-Proposed-HR.BR-1_SM-div": true,
+ "D-Proposed-TR2rs:2-1_SO1-dM": true,
+ "D-Proposed-TR2rs:2-1_SO1-body": true,
+ "D-Proposed-TR2rs:2-1_SO1-div": true,
+ "D-Proposed-TR2rs:2-1_SO2-dM": true,
+ "D-Proposed-TR2rs:2-1_SO2-body": true,
+ "D-Proposed-TR2rs:2-1_SO2-div": true,
+ "D-Proposed-TR3rs:3-1_SO1-dM": true,
+ "D-Proposed-TR3rs:3-1_SO1-body": true,
+ "D-Proposed-TR3rs:3-1_SO1-div": true,
+ "D-Proposed-TR3rs:3-1_SO2-dM": true,
+ "D-Proposed-TR3rs:3-1_SO2-body": true,
+ "D-Proposed-TR3rs:3-1_SO2-div": true,
+ "D-Proposed-TR3rs:3-1_SO3-dM": true,
+ "D-Proposed-TR3rs:3-1_SO3-body": true,
+ "D-Proposed-TR3rs:3-1_SO3-div": true,
+ "D-Proposed-DIV:ce:false-1_SB-dM": true,
+ "D-Proposed-DIV:ce:false-1_SL-dM": true,
+ "D-Proposed-DIV:ce:false-1_SL-body": true,
+ "D-Proposed-DIV:ce:false-1_SL-div": true,
+ "D-Proposed-DIV:ce:false-1_SR-dM": true,
+ "D-Proposed-DIV:ce:false-1_SR-body": true,
+ "D-Proposed-DIV:ce:false-1_SR-div": true,
+ "D-Proposed-DIV:ce:false-1_SI-dM": true,
+ "D-Proposed-SPAN:d:ib-2_SL-dM": true,
+ "D-Proposed-SPAN:d:ib-2_SL-body": true,
+ "D-Proposed-SPAN:d:ib-2_SL-div": true,
+ "D-Proposed-SPAN:d:ib-3_SR-dM": true,
+ "D-Proposed-SPAN:d:ib-3_SR-body": true,
+ "D-Proposed-SPAN:d:ib-3_SR-div": true,
+ "FD-Proposed-B-1_SW-div": true,
+ "FD-Proposed-OL-LI-1_SW-dM": true,
+ "FD-Proposed-OL-LI-1_SW-body": true,
+ "FD-Proposed-OL-LI-1_SW-div": true,
+ "FD-Proposed-OL-LI-1_SO-dM": true,
+ "FD-Proposed-OL-LI-1_SO-body": true,
+ "FD-Proposed-OL-LI-1_SO-div": true,
+ "FD-Proposed-TABLE-1_SB-dM": true,
+ "FD-Proposed-TABLE-1_SB-body": true,
+ "FD-Proposed-TABLE-1_SB-div": true,
+ "FD-Proposed-TD-1_SE-dM": true,
+ "FD-Proposed-TD-1_SE-body": true,
+ "FD-Proposed-TD-1_SE-div": true,
+ "FD-Proposed-TD2-1_SE1-dM": true,
+ "FD-Proposed-TD2-1_SE1-body": true,
+ "FD-Proposed-TD2-1_SE1-div": true,
+ "FD-Proposed-TD2-1_SM-dM": true,
+ "FD-Proposed-TD2-1_SM-body": true,
+ "FD-Proposed-TD2-1_SM-div": true,
+ "FD-Proposed-TR2rs:2-1_SO1-dM": true,
+ "FD-Proposed-TR2rs:2-1_SO1-body": true,
+ "FD-Proposed-TR2rs:2-1_SO1-div": true,
+ "FD-Proposed-TR2rs:2-1_SO2-dM": true,
+ "FD-Proposed-TR2rs:2-1_SO2-body": true,
+ "FD-Proposed-TR2rs:2-1_SO2-div": true,
+ "FD-Proposed-TR3rs:3-1_SO1-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO1-body": true,
+ "FD-Proposed-TR3rs:3-1_SO1-div": true,
+ "FD-Proposed-TR3rs:3-1_SO2-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO2-body": true,
+ "FD-Proposed-TR3rs:3-1_SO2-div": true,
+ "FD-Proposed-TR3rs:3-1_SO3-dM": true,
+ "FD-Proposed-TR3rs:3-1_SO3-body": true,
+ "FD-Proposed-TR3rs:3-1_SO3-div": true,
+ "FD-Proposed-DIV:ce:false-1_SB-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SL-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SL-body": true,
+ "FD-Proposed-DIV:ce:false-1_SL-div": true,
+ "FD-Proposed-DIV:ce:false-1_SR-dM": true,
+ "FD-Proposed-DIV:ce:false-1_SR-body": true,
+ "FD-Proposed-DIV:ce:false-1_SR-div": true,
+ "FD-Proposed-DIV:ce:false-1_SI-dM": true,
+ "I-Proposed-IHR_TEXT-1_SC-dM": true,
+ "I-Proposed-IHR_TEXT-1_SC-body": true,
+ "I-Proposed-IHR_TEXT-1_SC-div": true,
+ "I-Proposed-IHR_TEXT-1_SI-dM": true,
+ "I-Proposed-IHR_TEXT-1_SI-body": true,
+ "I-Proposed-IHR_TEXT-1_SI-div": true,
+ "I-Proposed-IHR_B-1_SC-dM": true,
+ "I-Proposed-IHR_B-1_SC-body": true,
+ "I-Proposed-IHR_B-1_SC-div": true,
+ "I-Proposed-IHR_B-1_SS-dM": true,
+ "I-Proposed-IHR_B-1_SS-body": true,
+ "I-Proposed-IHR_B-1_SS-div": true,
+ "I-Proposed-IHR_B-I-1_SMR-dM": true,
+ "I-Proposed-IHR_B-I-1_SMR-body": true,
+ "I-Proposed-IHR_B-I-1_SMR-div": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-dM": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-body": true,
+ "I-Proposed-IIMG:._SPAN-IMG-1_SO-div": true,
+ "I-Proposed-IIMG:._IMG-1_SO-dM": true,
+ "I-Proposed-IIMG:._IMG-1_SO-body": true,
+ "I-Proposed-IIMG:._IMG-1_SO-div": true,
+ "I-Proposed-IHTML:BR_TEXT-1_SC-dM": true,
+ "I-Proposed-IHTML:BR_TEXT-1_SC-body": true,
+ "I-Proposed-IHTML:BR_TEXT-1_SC-div": true,
+ "I-Proposed-IHTML:S_TEXT-1_SI-dM": true,
+ "I-Proposed-IHTML:S_TEXT-1_SI-body": true,
+ "I-Proposed-IHTML:S_TEXT-1_SI-div": true,
+ "I-Proposed-IHTML:H1.H2_TEXT-1_SI-dM": true,
+ "I-Proposed-IHTML:H1.H2_TEXT-1_SI-body": true,
+ "I-Proposed-IHTML:H1.H2_TEXT-1_SI-div": true,
+ "I-Proposed-IHTML:P-B_TEXT-1_SI-dM": true,
+ "I-Proposed-IHTML:P-B_TEXT-1_SI-body": true,
+ "I-Proposed-IHTML:P-B_TEXT-1_SI-div": true,
+ "Q-Proposed-SELECTALL_TEXT-1-dM": true,
+ "Q-Proposed-SELECTALL_TEXT-1-body": true,
+ "Q-Proposed-SELECTALL_TEXT-1-div": true,
+ "Q-Proposed-UNSELECT_TEXT-1-dM": true,
+ "Q-Proposed-UNSELECT_TEXT-1-body": true,
+ "Q-Proposed-UNSELECT_TEXT-1-div": true,
+ "Q-Proposed-UNDO_TEXT-1-dM": true,
+ "Q-Proposed-UNDO_TEXT-1-body": true,
+ "Q-Proposed-UNDO_TEXT-1-div": true,
+ "Q-Proposed-REDO_TEXT-1-dM": true,
+ "Q-Proposed-REDO_TEXT-1-body": true,
+ "Q-Proposed-REDO_TEXT-1-div": true,
+ "Q-Proposed-BOLD_TEXT-1-dM": true,
+ "Q-Proposed-BOLD_TEXT-1-body": true,
+ "Q-Proposed-BOLD_TEXT-1-div": true,
+ "Q-Proposed-BOLD_B-dM": true,
+ "Q-Proposed-BOLD_B-body": true,
+ "Q-Proposed-BOLD_B-div": true,
+ "Q-Proposed-ITALIC_TEXT-1-dM": true,
+ "Q-Proposed-ITALIC_TEXT-1-body": true,
+ "Q-Proposed-ITALIC_TEXT-1-div": true,
+ "Q-Proposed-ITALIC_I-dM": true,
+ "Q-Proposed-ITALIC_I-body": true,
+ "Q-Proposed-ITALIC_I-div": true,
+ "Q-Proposed-UNDERLINE_TEXT-1-dM": true,
+ "Q-Proposed-UNDERLINE_TEXT-1-body": true,
+ "Q-Proposed-UNDERLINE_TEXT-1-div": true,
+ "Q-Proposed-STRIKETHROUGH_TEXT-1-dM": true,
+ "Q-Proposed-STRIKETHROUGH_TEXT-1-body": true,
+ "Q-Proposed-STRIKETHROUGH_TEXT-1-div": true,
+ "Q-Proposed-SUBSCRIPT_TEXT-1-dM": true,
+ "Q-Proposed-SUBSCRIPT_TEXT-1-body": true,
+ "Q-Proposed-SUBSCRIPT_TEXT-1-div": true,
+ "Q-Proposed-SUPERSCRIPT_TEXT-1-dM": true,
+ "Q-Proposed-SUPERSCRIPT_TEXT-1-body": true,
+ "Q-Proposed-SUPERSCRIPT_TEXT-1-div": true,
+ "Q-Proposed-FORMATBLOCK_TEXT-1-dM": true,
+ "Q-Proposed-FORMATBLOCK_TEXT-1-body": true,
+ "Q-Proposed-FORMATBLOCK_TEXT-1-div": true,
+ "Q-Proposed-CREATELINK_TEXT-1-dM": true,
+ "Q-Proposed-CREATELINK_TEXT-1-body": true,
+ "Q-Proposed-CREATELINK_TEXT-1-div": true,
+ "Q-Proposed-UNLINK_TEXT-1-dM": true,
+ "Q-Proposed-UNLINK_TEXT-1-body": true,
+ "Q-Proposed-UNLINK_TEXT-1-div": true,
+ "Q-Proposed-INSERTHTML_TEXT-1-dM": true,
+ "Q-Proposed-INSERTHTML_TEXT-1-body": true,
+ "Q-Proposed-INSERTHTML_TEXT-1-div": true,
+ "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true,
+ "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true,
+ "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true,
+ "Q-Proposed-INSERTIMAGE_TEXT-1-dM": true,
+ "Q-Proposed-INSERTIMAGE_TEXT-1-body": true,
+ "Q-Proposed-INSERTIMAGE_TEXT-1-div": true,
+ "Q-Proposed-INSERTLINEBREAK_TEXT-1-dM": true,
+ "Q-Proposed-INSERTLINEBREAK_TEXT-1-body": true,
+ "Q-Proposed-INSERTLINEBREAK_TEXT-1-div": true,
+ "Q-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true,
+ "Q-Proposed-INSERTPARAGRAPH_TEXT-1-body": true,
+ "Q-Proposed-INSERTPARAGRAPH_TEXT-1-div": true,
+ "Q-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true,
+ "Q-Proposed-INSERTORDEREDLIST_TEXT-1-body": true,
+ "Q-Proposed-INSERTORDEREDLIST_TEXT-1-div": true,
+ "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true,
+ "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true,
+ "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true,
+ "Q-Proposed-INSERTTEXT_TEXT-1-dM": true,
+ "Q-Proposed-INSERTTEXT_TEXT-1-body": true,
+ "Q-Proposed-INSERTTEXT_TEXT-1-div": true,
+ "Q-Proposed-DELETE_TEXT-1-dM": true,
+ "Q-Proposed-DELETE_TEXT-1-body": true,
+ "Q-Proposed-DELETE_TEXT-1-div": true,
+ "Q-Proposed-FORWARDDELETE_TEXT-1-dM": true,
+ "Q-Proposed-FORWARDDELETE_TEXT-1-body": true,
+ "Q-Proposed-FORWARDDELETE_TEXT-1-div": true,
+ "Q-Proposed-STYLEWITHCSS_TEXT-1-dM": true,
+ "Q-Proposed-STYLEWITHCSS_TEXT-1-body": true,
+ "Q-Proposed-STYLEWITHCSS_TEXT-1-div": true,
+ "Q-Proposed-CONTENTREADONLY_TEXT-1-dM": true,
+ "Q-Proposed-CONTENTREADONLY_TEXT-1-body": true,
+ "Q-Proposed-CONTENTREADONLY_TEXT-1-div": true,
+ "Q-Proposed-BACKCOLOR_TEXT-1-dM": true,
+ "Q-Proposed-BACKCOLOR_TEXT-1-body": true,
+ "Q-Proposed-BACKCOLOR_TEXT-1-div": true,
+ "Q-Proposed-FORECOLOR_TEXT-1-dM": true,
+ "Q-Proposed-FORECOLOR_TEXT-1-body": true,
+ "Q-Proposed-FORECOLOR_TEXT-1-div": true,
+ "Q-Proposed-HILITECOLOR_TEXT-1-dM": true,
+ "Q-Proposed-HILITECOLOR_TEXT-1-body": true,
+ "Q-Proposed-HILITECOLOR_TEXT-1-div": true,
+ "Q-Proposed-FONTNAME_TEXT-1-dM": true,
+ "Q-Proposed-FONTNAME_TEXT-1-body": true,
+ "Q-Proposed-FONTNAME_TEXT-1-div": true,
+ "Q-Proposed-FONTSIZE_TEXT-1-dM": true,
+ "Q-Proposed-FONTSIZE_TEXT-1-body": true,
+ "Q-Proposed-FONTSIZE_TEXT-1-div": true,
+ "Q-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true,
+ "Q-Proposed-INCREASEFONTSIZE_TEXT-1-body": true,
+ "Q-Proposed-INCREASEFONTSIZE_TEXT-1-div": true,
+ "Q-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true,
+ "Q-Proposed-DECREASEFONTSIZE_TEXT-1-body": true,
+ "Q-Proposed-DECREASEFONTSIZE_TEXT-1-div": true,
+ "Q-Proposed-HEADING_TEXT-1-dM": true,
+ "Q-Proposed-HEADING_TEXT-1-body": true,
+ "Q-Proposed-HEADING_TEXT-1-div": true,
+ "Q-Proposed-INDENT_TEXT-1-dM": true,
+ "Q-Proposed-INDENT_TEXT-1-body": true,
+ "Q-Proposed-INDENT_TEXT-1-div": true,
+ "Q-Proposed-OUTDENT_TEXT-1-dM": true,
+ "Q-Proposed-OUTDENT_TEXT-1-body": true,
+ "Q-Proposed-OUTDENT_TEXT-1-div": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "Q-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "Q-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "Q-Proposed-JUSTIFYCENTER_TEXT-1-dM": true,
+ "Q-Proposed-JUSTIFYCENTER_TEXT-1-body": true,
+ "Q-Proposed-JUSTIFYCENTER_TEXT-1-div": true,
+ "Q-Proposed-JUSTIFYFULL_TEXT-1-dM": true,
+ "Q-Proposed-JUSTIFYFULL_TEXT-1-body": true,
+ "Q-Proposed-JUSTIFYFULL_TEXT-1-div": true,
+ "Q-Proposed-JUSTIFYLEFT_TEXT-1-dM": true,
+ "Q-Proposed-JUSTIFYLEFT_TEXT-1-body": true,
+ "Q-Proposed-JUSTIFYLEFT_TEXT-1-div": true,
+ "Q-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true,
+ "Q-Proposed-JUSTIFYRIGHT_TEXT-1-body": true,
+ "Q-Proposed-JUSTIFYRIGHT_TEXT-1-div": true,
+ "Q-Proposed-REMOVEFORMAT_TEXT-1-dM": true,
+ "Q-Proposed-REMOVEFORMAT_TEXT-1-body": true,
+ "Q-Proposed-REMOVEFORMAT_TEXT-1-div": true,
+ "Q-Proposed-COPY_TEXT-1-dM": true,
+ "Q-Proposed-COPY_TEXT-1-body": true,
+ "Q-Proposed-COPY_TEXT-1-div": true,
+ "Q-Proposed-CUT_TEXT-1-dM": true,
+ "Q-Proposed-CUT_TEXT-1-body": true,
+ "Q-Proposed-CUT_TEXT-1-div": true,
+ "Q-Proposed-PASTE_TEXT-1-dM": true,
+ "Q-Proposed-PASTE_TEXT-1-body": true,
+ "Q-Proposed-PASTE_TEXT-1-div": true,
+ "Q-Proposed-garbage-1_TEXT-1-dM": true,
+ "Q-Proposed-garbage-1_TEXT-1-body": true,
+ "Q-Proposed-garbage-1_TEXT-1-div": true,
+ "QE-Proposed-SELECTALL_TEXT-1-dM": true,
+ "QE-Proposed-SELECTALL_TEXT-1-body": true,
+ "QE-Proposed-SELECTALL_TEXT-1-div": true,
+ "QE-Proposed-UNSELECT_TEXT-1-dM": true,
+ "QE-Proposed-UNSELECT_TEXT-1-body": true,
+ "QE-Proposed-UNSELECT_TEXT-1-div": true,
+ "QE-Proposed-UNDO_TEXT-1-dM": true,
+ "QE-Proposed-UNDO_TEXT-1-body": true,
+ "QE-Proposed-UNDO_TEXT-1-div": true,
+ "QE-Proposed-REDO_TEXT-1-dM": true,
+ "QE-Proposed-REDO_TEXT-1-body": true,
+ "QE-Proposed-REDO_TEXT-1-div": true,
+ "QE-Proposed-BOLD_TEXT-1-dM": true,
+ "QE-Proposed-BOLD_TEXT-1-body": true,
+ "QE-Proposed-BOLD_TEXT-1-div": true,
+ "QE-Proposed-ITALIC_TEXT-1-dM": true,
+ "QE-Proposed-ITALIC_TEXT-1-body": true,
+ "QE-Proposed-ITALIC_TEXT-1-div": true,
+ "QE-Proposed-UNDERLINE_TEXT-1-dM": true,
+ "QE-Proposed-UNDERLINE_TEXT-1-body": true,
+ "QE-Proposed-UNDERLINE_TEXT-1-div": true,
+ "QE-Proposed-STRIKETHROUGH_TEXT-1-dM": true,
+ "QE-Proposed-STRIKETHROUGH_TEXT-1-body": true,
+ "QE-Proposed-STRIKETHROUGH_TEXT-1-div": true,
+ "QE-Proposed-SUBSCRIPT_TEXT-1-dM": true,
+ "QE-Proposed-SUBSCRIPT_TEXT-1-body": true,
+ "QE-Proposed-SUBSCRIPT_TEXT-1-div": true,
+ "QE-Proposed-SUPERSCRIPT_TEXT-1-dM": true,
+ "QE-Proposed-SUPERSCRIPT_TEXT-1-body": true,
+ "QE-Proposed-SUPERSCRIPT_TEXT-1-div": true,
+ "QE-Proposed-FORMATBLOCK_TEXT-1-dM": true,
+ "QE-Proposed-FORMATBLOCK_TEXT-1-body": true,
+ "QE-Proposed-FORMATBLOCK_TEXT-1-div": true,
+ "QE-Proposed-CREATELINK_TEXT-1-dM": true,
+ "QE-Proposed-CREATELINK_TEXT-1-body": true,
+ "QE-Proposed-CREATELINK_TEXT-1-div": true,
+ "QE-Proposed-UNLINK_TEXT-1-dM": true,
+ "QE-Proposed-UNLINK_TEXT-1-body": true,
+ "QE-Proposed-UNLINK_TEXT-1-div": true,
+ "QE-Proposed-INSERTHTML_TEXT-1-dM": true,
+ "QE-Proposed-INSERTHTML_TEXT-1-body": true,
+ "QE-Proposed-INSERTHTML_TEXT-1-div": true,
+ "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true,
+ "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true,
+ "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true,
+ "QE-Proposed-INSERTIMAGE_TEXT-1-dM": true,
+ "QE-Proposed-INSERTIMAGE_TEXT-1-body": true,
+ "QE-Proposed-INSERTIMAGE_TEXT-1-div": true,
+ "QE-Proposed-INSERTLINEBREAK_TEXT-1-dM": true,
+ "QE-Proposed-INSERTLINEBREAK_TEXT-1-body": true,
+ "QE-Proposed-INSERTLINEBREAK_TEXT-1-div": true,
+ "QE-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true,
+ "QE-Proposed-INSERTPARAGRAPH_TEXT-1-body": true,
+ "QE-Proposed-INSERTPARAGRAPH_TEXT-1-div": true,
+ "QE-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true,
+ "QE-Proposed-INSERTORDEREDLIST_TEXT-1-body": true,
+ "QE-Proposed-INSERTORDEREDLIST_TEXT-1-div": true,
+ "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true,
+ "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true,
+ "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true,
+ "QE-Proposed-INSERTTEXT_TEXT-1-dM": true,
+ "QE-Proposed-INSERTTEXT_TEXT-1-body": true,
+ "QE-Proposed-INSERTTEXT_TEXT-1-div": true,
+ "QE-Proposed-DELETE_TEXT-1-dM": true,
+ "QE-Proposed-DELETE_TEXT-1-body": true,
+ "QE-Proposed-DELETE_TEXT-1-div": true,
+ "QE-Proposed-FORWARDDELETE_TEXT-1-dM": true,
+ "QE-Proposed-FORWARDDELETE_TEXT-1-body": true,
+ "QE-Proposed-FORWARDDELETE_TEXT-1-div": true,
+ "QE-Proposed-STYLEWITHCSS_TEXT-1-dM": true,
+ "QE-Proposed-STYLEWITHCSS_TEXT-1-body": true,
+ "QE-Proposed-STYLEWITHCSS_TEXT-1-div": true,
+ "QE-Proposed-CONTENTREADONLY_TEXT-1-dM": true,
+ "QE-Proposed-CONTENTREADONLY_TEXT-1-body": true,
+ "QE-Proposed-CONTENTREADONLY_TEXT-1-div": true,
+ "QE-Proposed-BACKCOLOR_TEXT-1-dM": true,
+ "QE-Proposed-BACKCOLOR_TEXT-1-body": true,
+ "QE-Proposed-BACKCOLOR_TEXT-1-div": true,
+ "QE-Proposed-FORECOLOR_TEXT-1-dM": true,
+ "QE-Proposed-FORECOLOR_TEXT-1-body": true,
+ "QE-Proposed-FORECOLOR_TEXT-1-div": true,
+ "QE-Proposed-HILITECOLOR_TEXT-1-dM": true,
+ "QE-Proposed-HILITECOLOR_TEXT-1-body": true,
+ "QE-Proposed-HILITECOLOR_TEXT-1-div": true,
+ "QE-Proposed-FONTNAME_TEXT-1-dM": true,
+ "QE-Proposed-FONTNAME_TEXT-1-body": true,
+ "QE-Proposed-FONTNAME_TEXT-1-div": true,
+ "QE-Proposed-FONTSIZE_TEXT-1-dM": true,
+ "QE-Proposed-FONTSIZE_TEXT-1-body": true,
+ "QE-Proposed-FONTSIZE_TEXT-1-div": true,
+ "QE-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true,
+ "QE-Proposed-INCREASEFONTSIZE_TEXT-1-body": true,
+ "QE-Proposed-INCREASEFONTSIZE_TEXT-1-div": true,
+ "QE-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true,
+ "QE-Proposed-DECREASEFONTSIZE_TEXT-1-body": true,
+ "QE-Proposed-DECREASEFONTSIZE_TEXT-1-div": true,
+ "QE-Proposed-HEADING_TEXT-1-dM": true,
+ "QE-Proposed-HEADING_TEXT-1-body": true,
+ "QE-Proposed-HEADING_TEXT-1-div": true,
+ "QE-Proposed-INDENT_TEXT-1-dM": true,
+ "QE-Proposed-INDENT_TEXT-1-body": true,
+ "QE-Proposed-INDENT_TEXT-1-div": true,
+ "QE-Proposed-OUTDENT_TEXT-1-dM": true,
+ "QE-Proposed-OUTDENT_TEXT-1-body": true,
+ "QE-Proposed-OUTDENT_TEXT-1-div": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "QE-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "QE-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "QE-Proposed-JUSTIFYCENTER_TEXT-1-dM": true,
+ "QE-Proposed-JUSTIFYCENTER_TEXT-1-body": true,
+ "QE-Proposed-JUSTIFYCENTER_TEXT-1-div": true,
+ "QE-Proposed-JUSTIFYFULL_TEXT-1-dM": true,
+ "QE-Proposed-JUSTIFYFULL_TEXT-1-body": true,
+ "QE-Proposed-JUSTIFYFULL_TEXT-1-div": true,
+ "QE-Proposed-JUSTIFYLEFT_TEXT-1-dM": true,
+ "QE-Proposed-JUSTIFYLEFT_TEXT-1-body": true,
+ "QE-Proposed-JUSTIFYLEFT_TEXT-1-div": true,
+ "QE-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true,
+ "QE-Proposed-JUSTIFYRIGHT_TEXT-1-body": true,
+ "QE-Proposed-JUSTIFYRIGHT_TEXT-1-div": true,
+ "QE-Proposed-REMOVEFORMAT_TEXT-1-dM": true,
+ "QE-Proposed-REMOVEFORMAT_TEXT-1-body": true,
+ "QE-Proposed-REMOVEFORMAT_TEXT-1-div": true,
+ "QE-Proposed-COPY_TEXT-1-dM": true,
+ "QE-Proposed-COPY_TEXT-1-body": true,
+ "QE-Proposed-COPY_TEXT-1-div": true,
+ "QE-Proposed-CUT_TEXT-1-dM": true,
+ "QE-Proposed-CUT_TEXT-1-body": true,
+ "QE-Proposed-CUT_TEXT-1-div": true,
+ "QE-Proposed-PASTE_TEXT-1-dM": true,
+ "QE-Proposed-PASTE_TEXT-1-body": true,
+ "QE-Proposed-PASTE_TEXT-1-div": true,
+ "QE-Proposed-garbage-1_TEXT-1-dM": true,
+ "QE-Proposed-garbage-1_TEXT-1-body": true,
+ "QE-Proposed-garbage-1_TEXT-1-div": true,
+ "QI-Proposed-SELECTALL_TEXT-1-dM": true,
+ "QI-Proposed-SELECTALL_TEXT-1-body": true,
+ "QI-Proposed-SELECTALL_TEXT-1-div": true,
+ "QI-Proposed-UNSELECT_TEXT-1-dM": true,
+ "QI-Proposed-UNSELECT_TEXT-1-body": true,
+ "QI-Proposed-UNSELECT_TEXT-1-div": true,
+ "QI-Proposed-UNDO_TEXT-1-dM": true,
+ "QI-Proposed-UNDO_TEXT-1-body": true,
+ "QI-Proposed-UNDO_TEXT-1-div": true,
+ "QI-Proposed-REDO_TEXT-1-dM": true,
+ "QI-Proposed-REDO_TEXT-1-body": true,
+ "QI-Proposed-REDO_TEXT-1-div": true,
+ "QI-Proposed-BOLD_TEXT-1-dM": true,
+ "QI-Proposed-BOLD_TEXT-1-body": true,
+ "QI-Proposed-BOLD_TEXT-1-div": true,
+ "QI-Proposed-ITALIC_TEXT-1-dM": true,
+ "QI-Proposed-ITALIC_TEXT-1-body": true,
+ "QI-Proposed-ITALIC_TEXT-1-div": true,
+ "QI-Proposed-UNDERLINE_TEXT-1-dM": true,
+ "QI-Proposed-UNDERLINE_TEXT-1-body": true,
+ "QI-Proposed-UNDERLINE_TEXT-1-div": true,
+ "QI-Proposed-STRIKETHROUGH_TEXT-1-dM": true,
+ "QI-Proposed-STRIKETHROUGH_TEXT-1-body": true,
+ "QI-Proposed-STRIKETHROUGH_TEXT-1-div": true,
+ "QI-Proposed-SUBSCRIPT_TEXT-1-dM": true,
+ "QI-Proposed-SUBSCRIPT_TEXT-1-body": true,
+ "QI-Proposed-SUBSCRIPT_TEXT-1-div": true,
+ "QI-Proposed-SUPERSCRIPT_TEXT-1-dM": true,
+ "QI-Proposed-SUPERSCRIPT_TEXT-1-body": true,
+ "QI-Proposed-SUPERSCRIPT_TEXT-1-div": true,
+ "QI-Proposed-FORMATBLOCK_TEXT-1-dM": true,
+ "QI-Proposed-FORMATBLOCK_TEXT-1-body": true,
+ "QI-Proposed-FORMATBLOCK_TEXT-1-div": true,
+ "QI-Proposed-CREATELINK_TEXT-1-dM": true,
+ "QI-Proposed-CREATELINK_TEXT-1-body": true,
+ "QI-Proposed-CREATELINK_TEXT-1-div": true,
+ "QI-Proposed-UNLINK_TEXT-1-dM": true,
+ "QI-Proposed-UNLINK_TEXT-1-body": true,
+ "QI-Proposed-UNLINK_TEXT-1-div": true,
+ "QI-Proposed-INSERTHTML_TEXT-1-dM": true,
+ "QI-Proposed-INSERTHTML_TEXT-1-body": true,
+ "QI-Proposed-INSERTHTML_TEXT-1-div": true,
+ "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true,
+ "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true,
+ "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true,
+ "QI-Proposed-INSERTIMAGE_TEXT-1-dM": true,
+ "QI-Proposed-INSERTIMAGE_TEXT-1-body": true,
+ "QI-Proposed-INSERTIMAGE_TEXT-1-div": true,
+ "QI-Proposed-INSERTLINEBREAK_TEXT-1-dM": true,
+ "QI-Proposed-INSERTLINEBREAK_TEXT-1-body": true,
+ "QI-Proposed-INSERTLINEBREAK_TEXT-1-div": true,
+ "QI-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true,
+ "QI-Proposed-INSERTPARAGRAPH_TEXT-1-body": true,
+ "QI-Proposed-INSERTPARAGRAPH_TEXT-1-div": true,
+ "QI-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true,
+ "QI-Proposed-INSERTORDEREDLIST_TEXT-1-body": true,
+ "QI-Proposed-INSERTORDEREDLIST_TEXT-1-div": true,
+ "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true,
+ "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true,
+ "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true,
+ "QI-Proposed-INSERTTEXT_TEXT-1-dM": true,
+ "QI-Proposed-INSERTTEXT_TEXT-1-body": true,
+ "QI-Proposed-INSERTTEXT_TEXT-1-div": true,
+ "QI-Proposed-DELETE_TEXT-1-dM": true,
+ "QI-Proposed-DELETE_TEXT-1-body": true,
+ "QI-Proposed-DELETE_TEXT-1-div": true,
+ "QI-Proposed-FORWARDDELETE_TEXT-1-dM": true,
+ "QI-Proposed-FORWARDDELETE_TEXT-1-body": true,
+ "QI-Proposed-FORWARDDELETE_TEXT-1-div": true,
+ "QI-Proposed-STYLEWITHCSS_TEXT-1-dM": true,
+ "QI-Proposed-STYLEWITHCSS_TEXT-1-body": true,
+ "QI-Proposed-STYLEWITHCSS_TEXT-1-div": true,
+ "QI-Proposed-CONTENTREADONLY_TEXT-1-dM": true,
+ "QI-Proposed-CONTENTREADONLY_TEXT-1-body": true,
+ "QI-Proposed-CONTENTREADONLY_TEXT-1-div": true,
+ "QI-Proposed-BACKCOLOR_TEXT-1-dM": true,
+ "QI-Proposed-BACKCOLOR_TEXT-1-body": true,
+ "QI-Proposed-BACKCOLOR_TEXT-1-div": true,
+ "QI-Proposed-FORECOLOR_TEXT-1-dM": true,
+ "QI-Proposed-FORECOLOR_TEXT-1-body": true,
+ "QI-Proposed-FORECOLOR_TEXT-1-div": true,
+ "QI-Proposed-HILITECOLOR_TEXT-1-dM": true,
+ "QI-Proposed-HILITECOLOR_TEXT-1-body": true,
+ "QI-Proposed-HILITECOLOR_TEXT-1-div": true,
+ "QI-Proposed-FONTNAME_TEXT-1-dM": true,
+ "QI-Proposed-FONTNAME_TEXT-1-body": true,
+ "QI-Proposed-FONTNAME_TEXT-1-div": true,
+ "QI-Proposed-FONTSIZE_TEXT-1-dM": true,
+ "QI-Proposed-FONTSIZE_TEXT-1-body": true,
+ "QI-Proposed-FONTSIZE_TEXT-1-div": true,
+ "QI-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true,
+ "QI-Proposed-INCREASEFONTSIZE_TEXT-1-body": true,
+ "QI-Proposed-INCREASEFONTSIZE_TEXT-1-div": true,
+ "QI-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true,
+ "QI-Proposed-DECREASEFONTSIZE_TEXT-1-body": true,
+ "QI-Proposed-DECREASEFONTSIZE_TEXT-1-div": true,
+ "QI-Proposed-HEADING_TEXT-1-dM": true,
+ "QI-Proposed-HEADING_TEXT-1-body": true,
+ "QI-Proposed-HEADING_TEXT-1-div": true,
+ "QI-Proposed-INDENT_TEXT-1-dM": true,
+ "QI-Proposed-INDENT_TEXT-1-body": true,
+ "QI-Proposed-INDENT_TEXT-1-div": true,
+ "QI-Proposed-OUTDENT_TEXT-1-dM": true,
+ "QI-Proposed-OUTDENT_TEXT-1-body": true,
+ "QI-Proposed-OUTDENT_TEXT-1-div": true,
+ "QI-Proposed-CREATEBOOKMARK_TEXT-1-dM": true,
+ "QI-Proposed-CREATEBOOKMARK_TEXT-1-body": true,
+ "QI-Proposed-CREATEBOOKMARK_TEXT-1-div": true,
+ "QI-Proposed-UNBOOKMARK_TEXT-1-dM": true,
+ "QI-Proposed-UNBOOKMARK_TEXT-1-body": true,
+ "QI-Proposed-UNBOOKMARK_TEXT-1-div": true,
+ "QI-Proposed-JUSTIFYCENTER_TEXT-1-dM": true,
+ "QI-Proposed-JUSTIFYCENTER_TEXT-1-body": true,
+ "QI-Proposed-JUSTIFYCENTER_TEXT-1-div": true,
+ "QI-Proposed-JUSTIFYFULL_TEXT-1-dM": true,
+ "QI-Proposed-JUSTIFYFULL_TEXT-1-body": true,
+ "QI-Proposed-JUSTIFYFULL_TEXT-1-div": true,
+ "QI-Proposed-JUSTIFYLEFT_TEXT-1-dM": true,
+ "QI-Proposed-JUSTIFYLEFT_TEXT-1-body": true,
+ "QI-Proposed-JUSTIFYLEFT_TEXT-1-div": true,
+ "QI-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true,
+ "QI-Proposed-JUSTIFYRIGHT_TEXT-1-body": true,
+ "QI-Proposed-JUSTIFYRIGHT_TEXT-1-div": true,
+ "QI-Proposed-REMOVEFORMAT_TEXT-1-dM": true,
+ "QI-Proposed-REMOVEFORMAT_TEXT-1-body": true,
+ "QI-Proposed-REMOVEFORMAT_TEXT-1-div": true,
+ "QI-Proposed-COPY_TEXT-1-dM": true,
+ "QI-Proposed-COPY_TEXT-1-body": true,
+ "QI-Proposed-COPY_TEXT-1-div": true,
+ "QI-Proposed-CUT_TEXT-1-dM": true,
+ "QI-Proposed-CUT_TEXT-1-body": true,
+ "QI-Proposed-CUT_TEXT-1-div": true,
+ "QI-Proposed-PASTE_TEXT-1-dM": true,
+ "QI-Proposed-PASTE_TEXT-1-body": true,
+ "QI-Proposed-PASTE_TEXT-1-div": true,
+ "QI-Proposed-garbage-1_TEXT-1-dM": true,
+ "QI-Proposed-garbage-1_TEXT-1-body": true,
+ "QI-Proposed-garbage-1_TEXT-1-div": true,
+ "QS-Proposed-B_TEXT_SI-dM": true,
+ "QS-Proposed-B_TEXT_SI-body": true,
+ "QS-Proposed-B_TEXT_SI-div": true,
+ "QS-Proposed-B_B-1_SI-dM": true,
+ "QS-Proposed-B_B-1_SI-body": true,
+ "QS-Proposed-B_B-1_SI-div": true,
+ "QS-Proposed-B_STRONG-1_SI-dM": true,
+ "QS-Proposed-B_STRONG-1_SI-body": true,
+ "QS-Proposed-B_STRONG-1_SI-div": true,
+ "QS-Proposed-B_SPANs:fw:b-1_SI-dM": true,
+ "QS-Proposed-B_SPANs:fw:b-1_SI-body": true,
+ "QS-Proposed-B_SPANs:fw:b-1_SI-div": true,
+ "QS-Proposed-B_SPANs:fw:n-1_SI-dM": true,
+ "QS-Proposed-B_SPANs:fw:n-1_SI-body": true,
+ "QS-Proposed-B_SPANs:fw:n-1_SI-div": true,
+ "QS-Proposed-B_Bs:fw:n-1_SI-dM": true,
+ "QS-Proposed-B_Bs:fw:n-1_SI-body": true,
+ "QS-Proposed-B_Bs:fw:n-1_SI-div": true,
+ "QS-Proposed-B_B-SPANs:fw:n-1_SI-dM": true,
+ "QS-Proposed-B_B-SPANs:fw:n-1_SI-body": true,
+ "QS-Proposed-B_B-SPANs:fw:n-1_SI-div": true,
+ "QS-Proposed-B_SPAN.b-1-SI-dM": true,
+ "QS-Proposed-B_SPAN.b-1-SI-body": true,
+ "QS-Proposed-B_SPAN.b-1-SI-div": true,
+ "QS-Proposed-B_MYB-1-SI-dM": true,
+ "QS-Proposed-B_MYB-1-SI-body": true,
+ "QS-Proposed-B_MYB-1-SI-div": true,
+ "QS-Proposed-B_B-I-1_SC-dM": true,
+ "QS-Proposed-B_B-I-1_SC-body": true,
+ "QS-Proposed-B_B-I-1_SC-div": true,
+ "QS-Proposed-B_B-I-1_SL-dM": true,
+ "QS-Proposed-B_B-I-1_SL-body": true,
+ "QS-Proposed-B_B-I-1_SL-div": true,
+ "QS-Proposed-B_B-I-1_SR-dM": true,
+ "QS-Proposed-B_B-I-1_SR-body": true,
+ "QS-Proposed-B_B-I-1_SR-div": true,
+ "QS-Proposed-B_STRONG-I-1_SC-dM": true,
+ "QS-Proposed-B_STRONG-I-1_SC-body": true,
+ "QS-Proposed-B_STRONG-I-1_SC-div": true,
+ "QS-Proposed-B_B-I-U-1_SC-dM": true,
+ "QS-Proposed-B_B-I-U-1_SC-body": true,
+ "QS-Proposed-B_B-I-U-1_SC-div": true,
+ "QS-Proposed-B_B-I-U-1_SM-dM": true,
+ "QS-Proposed-B_B-I-U-1_SM-body": true,
+ "QS-Proposed-B_B-I-U-1_SM-div": true,
+ "QS-Proposed-B_TEXT-B-1_SO-1-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SO-1-body": true,
+ "QS-Proposed-B_TEXT-B-1_SO-1-div": true,
+ "QS-Proposed-B_TEXT-B-1_SO-2-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SO-2-body": true,
+ "QS-Proposed-B_TEXT-B-1_SO-2-div": true,
+ "QS-Proposed-B_TEXT-B-1_SL-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SL-body": true,
+ "QS-Proposed-B_TEXT-B-1_SL-div": true,
+ "QS-Proposed-B_TEXT-B-1_SR-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SR-body": true,
+ "QS-Proposed-B_TEXT-B-1_SR-div": true,
+ "QS-Proposed-B_TEXT-B-1_SO-3-dM": true,
+ "QS-Proposed-B_TEXT-B-1_SO-3-body": true,
+ "QS-Proposed-B_TEXT-B-1_SO-3-div": true,
+ "QS-Proposed-B_B.TEXT.B-1_SM-dM": true,
+ "QS-Proposed-B_B.TEXT.B-1_SM-body": true,
+ "QS-Proposed-B_B.TEXT.B-1_SM-div": true,
+ "QS-Proposed-B_B.B.B-1_SM-dM": true,
+ "QS-Proposed-B_B.B.B-1_SM-body": true,
+ "QS-Proposed-B_B.B.B-1_SM-div": true,
+ "QS-Proposed-B_B.STRONG.B-1_SM-dM": true,
+ "QS-Proposed-B_B.STRONG.B-1_SM-body": true,
+ "QS-Proposed-B_B.STRONG.B-1_SM-div": true,
+ "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-dM": true,
+ "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-body": true,
+ "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-div": true,
+ "QS-Proposed-I_TEXT_SI-dM": true,
+ "QS-Proposed-I_TEXT_SI-body": true,
+ "QS-Proposed-I_TEXT_SI-div": true,
+ "QS-Proposed-I_I-1_SI-dM": true,
+ "QS-Proposed-I_I-1_SI-body": true,
+ "QS-Proposed-I_I-1_SI-div": true,
+ "QS-Proposed-I_EM-1_SI-dM": true,
+ "QS-Proposed-I_EM-1_SI-body": true,
+ "QS-Proposed-I_EM-1_SI-div": true,
+ "QS-Proposed-I_SPANs:fs:i-1_SI-dM": true,
+ "QS-Proposed-I_SPANs:fs:i-1_SI-body": true,
+ "QS-Proposed-I_SPANs:fs:i-1_SI-div": true,
+ "QS-Proposed-I_SPANs:fs:n-1_SI-dM": true,
+ "QS-Proposed-I_SPANs:fs:n-1_SI-body": true,
+ "QS-Proposed-I_SPANs:fs:n-1_SI-div": true,
+ "QS-Proposed-I_I-SPANs:fs:n-1_SI-dM": true,
+ "QS-Proposed-I_I-SPANs:fs:n-1_SI-body": true,
+ "QS-Proposed-I_I-SPANs:fs:n-1_SI-div": true,
+ "QS-Proposed-I_SPAN.i-1-SI-dM": true,
+ "QS-Proposed-I_SPAN.i-1-SI-body": true,
+ "QS-Proposed-I_SPAN.i-1-SI-div": true,
+ "QS-Proposed-I_MYI-1-SI-dM": true,
+ "QS-Proposed-I_MYI-1-SI-body": true,
+ "QS-Proposed-I_MYI-1-SI-div": true,
+ "QS-Proposed-U_TEXT_SI-dM": true,
+ "QS-Proposed-U_TEXT_SI-body": true,
+ "QS-Proposed-U_TEXT_SI-div": true,
+ "QS-Proposed-U_U-1_SI-dM": true,
+ "QS-Proposed-U_U-1_SI-body": true,
+ "QS-Proposed-U_U-1_SI-div": true,
+ "QS-Proposed-U_Us:td:n-1_SI-dM": true,
+ "QS-Proposed-U_Us:td:n-1_SI-body": true,
+ "QS-Proposed-U_Us:td:n-1_SI-div": true,
+ "QS-Proposed-U_Ah:url-1_SI-dM": true,
+ "QS-Proposed-U_Ah:url-1_SI-body": true,
+ "QS-Proposed-U_Ah:url-1_SI-div": true,
+ "QS-Proposed-U_Ah:url.s:td:n-1_SI-dM": true,
+ "QS-Proposed-U_Ah:url.s:td:n-1_SI-body": true,
+ "QS-Proposed-U_Ah:url.s:td:n-1_SI-div": true,
+ "QS-Proposed-U_SPANs:td:u-1_SI-dM": true,
+ "QS-Proposed-U_SPANs:td:u-1_SI-body": true,
+ "QS-Proposed-U_SPANs:td:u-1_SI-div": true,
+ "QS-Proposed-U_SPAN.u-1-SI-dM": true,
+ "QS-Proposed-U_SPAN.u-1-SI-body": true,
+ "QS-Proposed-U_SPAN.u-1-SI-div": true,
+ "QS-Proposed-U_MYU-1-SI-dM": true,
+ "QS-Proposed-U_MYU-1-SI-body": true,
+ "QS-Proposed-U_MYU-1-SI-div": true,
+ "QS-Proposed-S_TEXT_SI-dM": true,
+ "QS-Proposed-S_TEXT_SI-body": true,
+ "QS-Proposed-S_TEXT_SI-div": true,
+ "QS-Proposed-S_S-1_SI-dM": true,
+ "QS-Proposed-S_S-1_SI-body": true,
+ "QS-Proposed-S_S-1_SI-div": true,
+ "QS-Proposed-S_STRIKE-1_SI-dM": true,
+ "QS-Proposed-S_STRIKE-1_SI-body": true,
+ "QS-Proposed-S_STRIKE-1_SI-div": true,
+ "QS-Proposed-S_STRIKEs:td:n-1_SI-dM": true,
+ "QS-Proposed-S_STRIKEs:td:n-1_SI-body": true,
+ "QS-Proposed-S_STRIKEs:td:n-1_SI-div": true,
+ "QS-Proposed-S_DEL-1_SI-dM": true,
+ "QS-Proposed-S_DEL-1_SI-body": true,
+ "QS-Proposed-S_DEL-1_SI-div": true,
+ "QS-Proposed-S_SPANs:td:lt-1_SI-dM": true,
+ "QS-Proposed-S_SPANs:td:lt-1_SI-body": true,
+ "QS-Proposed-S_SPANs:td:lt-1_SI-div": true,
+ "QS-Proposed-S_SPAN.s-1-SI-dM": true,
+ "QS-Proposed-S_SPAN.s-1-SI-body": true,
+ "QS-Proposed-S_SPAN.s-1-SI-div": true,
+ "QS-Proposed-S_MYS-1-SI-dM": true,
+ "QS-Proposed-S_MYS-1-SI-body": true,
+ "QS-Proposed-S_MYS-1-SI-div": true,
+ "QS-Proposed-S_S.STRIKE.DEL-1_SM-dM": true,
+ "QS-Proposed-S_S.STRIKE.DEL-1_SM-body": true,
+ "QS-Proposed-S_S.STRIKE.DEL-1_SM-div": true,
+ "QS-Proposed-SUB_TEXT_SI-dM": true,
+ "QS-Proposed-SUB_TEXT_SI-body": true,
+ "QS-Proposed-SUB_TEXT_SI-div": true,
+ "QS-Proposed-SUB_SUB-1_SI-dM": true,
+ "QS-Proposed-SUB_SUB-1_SI-body": true,
+ "QS-Proposed-SUB_SUB-1_SI-div": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-dM": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-body": true,
+ "QS-Proposed-SUB_SPAN.sub-1-SI-div": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-dM": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-body": true,
+ "QS-Proposed-SUB_MYSUB-1-SI-div": true,
+ "QS-Proposed-SUP_TEXT_SI-dM": true,
+ "QS-Proposed-SUP_TEXT_SI-body": true,
+ "QS-Proposed-SUP_TEXT_SI-div": true,
+ "QS-Proposed-SUP_SUP-1_SI-dM": true,
+ "QS-Proposed-SUP_SUP-1_SI-body": true,
+ "QS-Proposed-SUP_SUP-1_SI-div": true,
+ "QS-Proposed-IOL_TEXT_SI-dM": true,
+ "QS-Proposed-IOL_TEXT_SI-body": true,
+ "QS-Proposed-IOL_TEXT_SI-div": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-dM": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-body": true,
+ "QS-Proposed-SUP_SPAN.sup-1-SI-div": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-dM": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-body": true,
+ "QS-Proposed-SUP_MYSUP-1-SI-div": true,
+ "QS-Proposed-IOL_TEXT-1_SI-dM": true,
+ "QS-Proposed-IOL_TEXT-1_SI-body": true,
+ "QS-Proposed-IOL_TEXT-1_SI-div": true,
+ "QS-Proposed-IOL_OL-LI-1_SI-dM": true,
+ "QS-Proposed-IOL_OL-LI-1_SI-body": true,
+ "QS-Proposed-IOL_OL-LI-1_SI-div": true,
+ "QS-Proposed-IOL_UL_LI-1_SI-dM": true,
+ "QS-Proposed-IOL_UL_LI-1_SI-body": true,
+ "QS-Proposed-IOL_UL_LI-1_SI-div": true,
+ "QS-Proposed-IUL_TEXT_SI-dM": true,
+ "QS-Proposed-IUL_TEXT_SI-body": true,
+ "QS-Proposed-IUL_TEXT_SI-div": true,
+ "QS-Proposed-IUL_OL-LI-1_SI-dM": true,
+ "QS-Proposed-IUL_OL-LI-1_SI-body": true,
+ "QS-Proposed-IUL_OL-LI-1_SI-div": true,
+ "QS-Proposed-IUL_UL-LI-1_SI-dM": true,
+ "QS-Proposed-IUL_UL-LI-1_SI-body": true,
+ "QS-Proposed-IUL_UL-LI-1_SI-div": true,
+ "QS-Proposed-JC_TEXT_SI-dM": true,
+ "QS-Proposed-JC_TEXT_SI-body": true,
+ "QS-Proposed-JC_TEXT_SI-div": true,
+ "QS-Proposed-JC_DIVa:c-1_SI-dM": true,
+ "QS-Proposed-JC_DIVa:c-1_SI-body": true,
+ "QS-Proposed-JC_DIVa:c-1_SI-div": true,
+ "QS-Proposed-JC_Pa:c-1_SI-dM": true,
+ "QS-Proposed-JC_Pa:c-1_SI-body": true,
+ "QS-Proposed-JC_Pa:c-1_SI-div": true,
+ "QS-Proposed-JC_SPANs:ta:c-1_SI-dM": true,
+ "QS-Proposed-JC_SPANs:ta:c-1_SI-body": true,
+ "QS-Proposed-JC_SPANs:ta:c-1_SI-div": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-dM": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-body": true,
+ "QS-Proposed-JC_SPAN.jc-1-SI-div": true,
+ "QS-Proposed-JC_MYJC-1-SI-dM": true,
+ "QS-Proposed-JC_MYJC-1-SI-body": true,
+ "QS-Proposed-JC_MYJC-1-SI-div": true,
+ "QS-Proposed-JF_TEXT_SI-dM": true,
+ "QS-Proposed-JF_TEXT_SI-body": true,
+ "QS-Proposed-JF_TEXT_SI-div": true,
+ "QS-Proposed-JF_DIVa:j-1_SI-dM": true,
+ "QS-Proposed-JF_DIVa:j-1_SI-body": true,
+ "QS-Proposed-JF_DIVa:j-1_SI-div": true,
+ "QS-Proposed-JF_Pa:j-1_SI-dM": true,
+ "QS-Proposed-JF_Pa:j-1_SI-body": true,
+ "QS-Proposed-JF_Pa:j-1_SI-div": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-dM": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-body": true,
+ "QS-Proposed-JF_SPANs:ta:j-1_SI-div": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-dM": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-body": true,
+ "QS-Proposed-JF_SPAN.jf-1-SI-div": true,
+ "QS-Proposed-JF_MYJF-1-SI-dM": true,
+ "QS-Proposed-JF_MYJF-1-SI-body": true,
+ "QS-Proposed-JF_MYJF-1-SI-div": true,
+ "QS-Proposed-JL_TEXT_SI-dM": true,
+ "QS-Proposed-JL_TEXT_SI-body": true,
+ "QS-Proposed-JL_TEXT_SI-div": true,
+ "QS-Proposed-JL_DIVa:l-1_SI-dM": true,
+ "QS-Proposed-JL_DIVa:l-1_SI-body": true,
+ "QS-Proposed-JL_DIVa:l-1_SI-div": true,
+ "QS-Proposed-JL_Pa:l-1_SI-dM": true,
+ "QS-Proposed-JL_Pa:l-1_SI-body": true,
+ "QS-Proposed-JL_Pa:l-1_SI-div": true,
+ "QS-Proposed-JL_SPANs:ta:l-1_SI-dM": true,
+ "QS-Proposed-JL_SPANs:ta:l-1_SI-body": true,
+ "QS-Proposed-JL_SPANs:ta:l-1_SI-div": true,
+ "QS-Proposed-JL_SPAN.jl-1-SI-dM": true,
+ "QS-Proposed-JL_SPAN.jl-1-SI-body": true,
+ "QS-Proposed-JL_SPAN.jl-1-SI-div": true,
+ "QS-Proposed-JL_MYJL-1-SI-dM": true,
+ "QS-Proposed-JL_MYJL-1-SI-body": true,
+ "QS-Proposed-JL_MYJL-1-SI-div": true,
+ "QS-Proposed-JR_TEXT_SI-dM": true,
+ "QS-Proposed-JR_TEXT_SI-body": true,
+ "QS-Proposed-JR_TEXT_SI-div": true,
+ "QS-Proposed-JR_DIVa:r-1_SI-dM": true,
+ "QS-Proposed-JR_DIVa:r-1_SI-body": true,
+ "QS-Proposed-JR_DIVa:r-1_SI-div": true,
+ "QS-Proposed-JR_Pa:r-1_SI-dM": true,
+ "QS-Proposed-JR_Pa:r-1_SI-body": true,
+ "QS-Proposed-JR_Pa:r-1_SI-div": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-dM": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-body": true,
+ "QS-Proposed-JR_SPANs:ta:r-1_SI-div": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-dM": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-body": true,
+ "QS-Proposed-JR_SPAN.jr-1-SI-div": true,
+ "QS-Proposed-JR_MYJR-1-SI-dM": true,
+ "QS-Proposed-JR_MYJR-1-SI-body": true,
+ "QS-Proposed-JR_MYJR-1-SI-div": true,
+ "QV-Proposed-B_TEXT_SI-dM": true,
+ "QV-Proposed-B_TEXT_SI-body": true,
+ "QV-Proposed-B_TEXT_SI-div": true,
+ "QV-Proposed-B_B-1_SI-dM": true,
+ "QV-Proposed-B_B-1_SI-body": true,
+ "QV-Proposed-B_B-1_SI-div": true,
+ "QV-Proposed-B_STRONG-1_SI-dM": true,
+ "QV-Proposed-B_STRONG-1_SI-body": true,
+ "QV-Proposed-B_STRONG-1_SI-div": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-dM": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-body": true,
+ "QV-Proposed-B_SPANs:fw:b-1_SI-div": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-dM": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-body": true,
+ "QV-Proposed-B_SPANs:fw:n-1_SI-div": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-dM": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-body": true,
+ "QV-Proposed-B_Bs:fw:n-1_SI-div": true,
+ "QV-Proposed-B_SPAN.b-1_SI-dM": true,
+ "QV-Proposed-B_SPAN.b-1_SI-body": true,
+ "QV-Proposed-B_SPAN.b-1_SI-div": true,
+ "QV-Proposed-B_MYB-1-SI-dM": true,
+ "QV-Proposed-B_MYB-1-SI-body": true,
+ "QV-Proposed-B_MYB-1-SI-div": true,
+ "QV-Proposed-I_TEXT_SI-dM": true,
+ "QV-Proposed-I_TEXT_SI-body": true,
+ "QV-Proposed-I_TEXT_SI-div": true,
+ "QV-Proposed-I_I-1_SI-dM": true,
+ "QV-Proposed-I_I-1_SI-body": true,
+ "QV-Proposed-I_I-1_SI-div": true,
+ "QV-Proposed-I_EM-1_SI-dM": true,
+ "QV-Proposed-I_EM-1_SI-body": true,
+ "QV-Proposed-I_EM-1_SI-div": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-dM": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-body": true,
+ "QV-Proposed-I_SPANs:fs:i-1_SI-div": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-dM": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-body": true,
+ "QV-Proposed-I_SPANs:fs:n-1_SI-div": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-dM": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-body": true,
+ "QV-Proposed-I_I-SPANs:fs:n-1_SI-div": true,
+ "QV-Proposed-I_SPAN.i-1_SI-dM": true,
+ "QV-Proposed-I_SPAN.i-1_SI-body": true,
+ "QV-Proposed-I_SPAN.i-1_SI-div": true,
+ "QV-Proposed-I_MYI-1-SI-dM": true,
+ "QV-Proposed-I_MYI-1-SI-body": true,
+ "QV-Proposed-I_MYI-1-SI-div": true,
+ "QV-Proposed-FB_TEXT-1_SC-dM": true,
+ "QV-Proposed-FB_TEXT-1_SC-body": true,
+ "QV-Proposed-FB_TEXT-1_SC-div": true,
+ "QV-Proposed-FB_H1-1_SC-dM": true,
+ "QV-Proposed-FB_H1-1_SC-body": true,
+ "QV-Proposed-FB_H1-1_SC-div": true,
+ "QV-Proposed-FB_PRE-1_SC-dM": true,
+ "QV-Proposed-FB_PRE-1_SC-body": true,
+ "QV-Proposed-FB_PRE-1_SC-div": true,
+ "QV-Proposed-FB_BQ-1_SC-dM": true,
+ "QV-Proposed-FB_BQ-1_SC-body": true,
+ "QV-Proposed-FB_BQ-1_SC-div": true,
+ "QV-Proposed-FB_ADDRESS-1_SC-dM": true,
+ "QV-Proposed-FB_ADDRESS-1_SC-body": true,
+ "QV-Proposed-FB_ADDRESS-1_SC-div": true,
+ "QV-Proposed-FB_H1-H2-1_SC-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SC-body": true,
+ "QV-Proposed-FB_H1-H2-1_SC-div": true,
+ "QV-Proposed-FB_H1-H2-1_SL-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SL-body": true,
+ "QV-Proposed-FB_H1-H2-1_SL-div": true,
+ "QV-Proposed-FB_H1-H2-1_SR-dM": true,
+ "QV-Proposed-FB_H1-H2-1_SR-body": true,
+ "QV-Proposed-FB_H1-H2-1_SR-div": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-dM": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-body": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SL-div": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SR-dM": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SR-body": true,
+ "QV-Proposed-FB_TEXT-ADDRESS-1_SR-div": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-dM": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-body": true,
+ "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-div": true,
+ "QV-Proposed-H_H1-1_SC-dM": true,
+ "QV-Proposed-H_H1-1_SC-body": true,
+ "QV-Proposed-H_H1-1_SC-div": true,
+ "QV-Proposed-H_H3-1_SC-dM": true,
+ "QV-Proposed-H_H3-1_SC-body": true,
+ "QV-Proposed-H_H3-1_SC-div": true,
+ "QV-Proposed-H_H1-H2-H3-H4-1_SC-dM": true,
+ "QV-Proposed-H_H1-H2-H3-H4-1_SC-body": true,
+ "QV-Proposed-H_H1-H2-H3-H4-1_SC-div": true,
+ "QV-Proposed-H_P-1_SC-dM": true,
+ "QV-Proposed-H_P-1_SC-body": true,
+ "QV-Proposed-H_P-1_SC-div": true,
+ "QV-Proposed-FN_FONTf:a-1_SI-dM": true,
+ "QV-Proposed-FN_FONTf:a-1_SI-body": true,
+ "QV-Proposed-FN_FONTf:a-1_SI-div": true,
+ "QV-Proposed-FN_SPANs:ff:a-1_SI-dM": true,
+ "QV-Proposed-FN_SPANs:ff:a-1_SI-body": true,
+ "QV-Proposed-FN_SPANs:ff:a-1_SI-div": true,
+ "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-dM": true,
+ "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-body": true,
+ "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-div": true,
+ "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-dM": true,
+ "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-body": true,
+ "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-div": true,
+ "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-dM": true,
+ "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-body": true,
+ "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-div": true,
+ "QV-Proposed-FN_SPAN.fs18px-1_SI-dM": true,
+ "QV-Proposed-FN_SPAN.fs18px-1_SI-body": true,
+ "QV-Proposed-FN_SPAN.fs18px-1_SI-div": true,
+ "QV-Proposed-FN_MYCOURIER-1-SI-dM": true,
+ "QV-Proposed-FN_MYCOURIER-1-SI-body": true,
+ "QV-Proposed-FN_MYCOURIER-1-SI-div": true,
+ "QV-Proposed-FS_FONTsz:4-1_SI-dM": true,
+ "QV-Proposed-FS_FONTsz:4-1_SI-body": true,
+ "QV-Proposed-FS_FONTsz:4-1_SI-div": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-dM": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-body": true,
+ "QV-Proposed-FS_FONTs:fs:l-1_SI-div": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-dM": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-body": true,
+ "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-div": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-dM": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-body": true,
+ "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-div": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-dM": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-body": true,
+ "QV-Proposed-FS_SPAN.large-1_SI-div": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-dM": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-body": true,
+ "QV-Proposed-FS_SPAN.fs18px-1_SI-div": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-dM": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-body": true,
+ "QV-Proposed-FA_MYLARGE-1-SI-div": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-dM": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-body": true,
+ "QV-Proposed-FA_MYFS18PX-1-SI-div": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-dM": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-body": true,
+ "QV-Proposed-BC_FONTs:bc:fca-1_SI-div": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-dM": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-body": true,
+ "QV-Proposed-BC_SPANs:bc:abc-1_SI-div": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-dM": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-body": true,
+ "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-div": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-dM": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-body": true,
+ "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-div": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-dM": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-body": true,
+ "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-div": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-dM": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-body": true,
+ "QV-Proposed-BC_SPAN.bcred-1_SI-div": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-dM": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-body": true,
+ "QV-Proposed-BC_MYBCRED-1-SI-div": true,
+ "QV-Proposed-FC_FONTc:f00-1_SI-dM": true,
+ "QV-Proposed-FC_FONTc:f00-1_SI-body": true,
+ "QV-Proposed-FC_FONTc:f00-1_SI-div": true,
+ "QV-Proposed-FC_SPANs:c:0f0-1_SI-dM": true,
+ "QV-Proposed-FC_SPANs:c:0f0-1_SI-body": true,
+ "QV-Proposed-FC_SPANs:c:0f0-1_SI-div": true,
+ "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-dM": true,
+ "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-body": true,
+ "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-div": true,
+ "QV-Proposed-FC_FONTc:641-SPAN-1_SI-dM": true,
+ "QV-Proposed-FC_FONTc:641-SPAN-1_SI-body": true,
+ "QV-Proposed-FC_FONTc:641-SPAN-1_SI-div": true,
+ "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-dM": true,
+ "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-body": true,
+ "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-div": true,
+ "QV-Proposed-FC_SPAN.red-1_SI-dM": true,
+ "QV-Proposed-FC_SPAN.red-1_SI-body": true,
+ "QV-Proposed-FC_SPAN.red-1_SI-div": true,
+ "QV-Proposed-FC_MYRED-1-SI-dM": true,
+ "QV-Proposed-FC_MYRED-1-SI-body": true,
+ "QV-Proposed-FC_MYRED-1-SI-div": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-dM": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-body": true,
+ "QV-Proposed-HC_FONTs:bc:fc0-1_SI-div": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-dM": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-body": true,
+ "QV-Proposed-HC_SPANs:bc:a0c-1_SI-div": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-dM": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-body": true,
+ "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-div": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-dM": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-body": true,
+ "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-div": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-dM": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-body": true,
+ "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-div": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-dM": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-body": true,
+ "QV-Proposed-HC_SPAN.bcred-1_SI-div": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-dM": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-body": true,
+ "QV-Proposed-HC_MYBCRED-1-SI-div": true,
+ },
+};
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/current_revision b/editor/libeditor/tests/browserscope/lib/richtext2/current_revision
new file mode 100644
index 0000000000..cc34bb3975
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/current_revision
@@ -0,0 +1 @@
+805
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/platformFailures.js b/editor/libeditor/tests/browserscope/lib/richtext2/platformFailures.js
new file mode 100644
index 0000000000..61e1dad671
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/platformFailures.js
@@ -0,0 +1,28 @@
+/**
+ * Platform-specific failures not included in the main currentStatus.js list.
+ */
+var platformFailures;
+if (navigator.appVersion.includes("Android")) {
+ platformFailures = {
+ "value": {},
+ "select": {
+ "S-Proposed-SM:m.f.w_TEXT-th_SC-1-dM": true,
+ "S-Proposed-SM:m.f.w_TEXT-th_SC-1-body": true,
+ "S-Proposed-SM:m.f.w_TEXT-th_SC-1-div": true,
+ "S-Proposed-SM:m.f.w_TEXT-th_SC-2-dM": true,
+ "S-Proposed-SM:m.f.w_TEXT-th_SC-2-body": true,
+ "S-Proposed-SM:m.f.w_TEXT-th_SC-2-div": true,
+ "S-Proposed-SM:m.b.w_TEXT-th_SC-1-dM": true,
+ "S-Proposed-SM:m.b.w_TEXT-th_SC-1-body": true,
+ "S-Proposed-SM:m.b.w_TEXT-th_SC-1-div": true,
+ "S-Proposed-SM:m.b.w_TEXT-th_SC-2-dM": true,
+ "S-Proposed-SM:m.b.w_TEXT-th_SC-2-body": true,
+ "S-Proposed-SM:m.b.w_TEXT-th_SC-2-div": true
+ }
+ }
+} else {
+ platformFailures = {
+ "value": {},
+ "select": {}
+ }
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py
new file mode 100644
index 0000000000..345f9bbb00
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py
@@ -0,0 +1,25 @@
+#!/usr/bin/python2.5
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the 'License')
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Common constants"""
+
+__author__ = 'rolandsteiner@google.com (Roland Steiner)'
+
+CATEGORY = 'richtext2'
+
+TEST_ID_PREFIX = 'RTE2'
+
+CLASSES = ['Finalized', 'RFC', 'Proposed'] \ No newline at end of file
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py
new file mode 100644
index 0000000000..2ee1e79ad3
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py
@@ -0,0 +1,107 @@
+#!/usr/bin/python2.5
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the 'License')
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Handlers for New Rich Text Tests"""
+
+__author__ = 'rolandsteiner@google.com (Roland Steiner)'
+
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.api import memcache
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+
+import django
+from django import http
+from django import shortcuts
+
+from django.template import add_to_builtins
+add_to_builtins('base.custom_filters')
+
+# Shared stuff
+from categories import all_test_sets
+from base import decorators
+from base import util
+
+# common to the RichText2 suite
+from categories.richtext2 import common
+
+# tests
+from categories.richtext2.tests.apply import APPLY_TESTS
+from categories.richtext2.tests.applyCSS import APPLY_TESTS_CSS
+from categories.richtext2.tests.change import CHANGE_TESTS
+from categories.richtext2.tests.changeCSS import CHANGE_TESTS_CSS
+from categories.richtext2.tests.delete import DELETE_TESTS
+from categories.richtext2.tests.forwarddelete import FORWARDDELETE_TESTS
+from categories.richtext2.tests.insert import INSERT_TESTS
+from categories.richtext2.tests.selection import SELECTION_TESTS
+from categories.richtext2.tests.unapply import UNAPPLY_TESTS
+from categories.richtext2.tests.unapplyCSS import UNAPPLY_TESTS_CSS
+
+from categories.richtext2.tests.querySupported import QUERYSUPPORTED_TESTS
+from categories.richtext2.tests.queryEnabled import QUERYENABLED_TESTS
+from categories.richtext2.tests.queryIndeterm import QUERYINDETERM_TESTS
+from categories.richtext2.tests.queryState import QUERYSTATE_TESTS, QUERYSTATE_TESTS_CSS
+from categories.richtext2.tests.queryValue import QUERYVALUE_TESTS, QUERYVALUE_TESTS_CSS
+
+
+def About(request):
+ """About page."""
+ overview = """These tests cover browers' implementations of
+ <a href="http://blog.whatwg.org/the-road-to-html-5-contenteditable">contenteditable</a>
+ for basic rich text formatting commands. Most browser implementations do very
+ well at editing the HTML which is generated by their own execCommands. But a
+ big problem happens when developers try to make cross-browser web
+ applications using contenteditable - most browsers are not able to correctly
+ change formatting generated by other browsers. On top of that, most browsers
+ allow users to to paste arbitrary HTML from other webpages into a
+ contenteditable region, which is even harder for browsers to properly
+ format. These tests check how well the execCommand, queryCommandState,
+ and queryCommandValue functions work with different types of HTML."""
+ return util.About(request, common.CATEGORY, category_title='Rich Text',
+ overview=overview, show_hidden=False)
+
+
+def RunRichText2Tests(request):
+ params = {
+ 'classes': common.CLASSES,
+ 'commonIDPrefix': common.TEST_ID_PREFIX,
+ 'strict': False,
+ 'suites': [
+ SELECTION_TESTS,
+ APPLY_TESTS,
+ APPLY_TESTS_CSS,
+ CHANGE_TESTS,
+ CHANGE_TESTS_CSS,
+ UNAPPLY_TESTS,
+ UNAPPLY_TESTS_CSS,
+ DELETE_TESTS,
+ FORWARDDELETE_TESTS,
+ INSERT_TESTS,
+
+ QUERYSUPPORTED_TESTS,
+ QUERYENABLED_TESTS,
+ QUERYINDETERM_TESTS,
+ QUERYSTATE_TESTS,
+ QUERYSTATE_TESTS_CSS,
+ QUERYVALUE_TESTS,
+ QUERYVALUE_TESTS_CSS
+ ]
+ }
+ return shortcuts.render_to_response('%s/templates/richtext2.html' % common.CATEGORY, params)
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css
new file mode 100644
index 0000000000..77c6bb8726
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css
@@ -0,0 +1,116 @@
+.framed {
+ vertical-align: top;
+ margin: 8px;
+ border: 1px solid black;
+}
+
+.legend {
+ padding: 12px;
+ background-color: #f8f8ff;
+}
+
+.legendHdr {
+ font-size: large;
+ text-decoration: underline;
+}
+
+table.legend {
+ display: inline-table;
+}
+
+.suite-thead {
+ text-align: left;
+}
+
+.lo {
+ background-color: #dddddd;
+}
+.hi {
+ background-color: #eeeeee;
+}
+
+.lo .grey {
+ background-color: #dddddd;
+}
+.lo .na {
+ background-color: #dddddd;
+}
+.lo .pass {
+ background-color: #d4ffc0;
+}
+.lo .canary {
+ background-color: #ffcccc;
+}
+.lo .fail {
+ background-color: #ffcccc;
+}
+.lo .accept {
+ background-color: #ffffc0;
+}
+.lo .exception {
+ background-color: #f0d0f4;
+}
+.lo .unsupported {
+ background-color: #f0d0f4;
+}
+
+.hi .grey {
+ background-color: #eeeeee;
+}
+.hi .na {
+ background-color: #eeeeee;
+}
+.hi .pass {
+ background-color: #e0ffdc;
+}
+.hi .canary {
+ background-color: #ffd8d8;
+}
+.hi .fail {
+ background-color: #ffd8d8;
+}
+.hi .accept {
+ background-color: #ffffd8;
+}
+.hi .exception {
+ background-color: #f4dcf8;
+}
+.hi .unsupported {
+ background-color: #f4dcf8;
+}
+
+
+.sel {
+ color: blue;
+}
+
+.txt {
+ padding: 1px;
+ margin: 1px;
+ border: 1px solid #b0b0b0;
+}
+
+.idLabel {
+ font-size: small;
+}
+
+.fade {
+ color: grey;
+}
+.accexp {
+ color: #606070;
+}
+.comment {
+ color: grey;
+}
+
+.score {
+ color: #666666;
+}
+
+.fatalerror {
+ color: red;
+ font-size: large;
+ font-weight: bold;
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html
new file mode 100644
index 0000000000..a254adc03e
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <link rel="stylesheet" href="editable.css" type="text/css">
+</head>
+<body contentEditable="true">
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html
new file mode 100644
index 0000000000..e16de3ab9f
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <link rel="stylesheet" href="editable.css" type="text/css">
+
+ <script>
+ function setDesignMode() {
+ window.document.designMode = "On";
+ }
+ </script>
+</head>
+<body onload="setDesignMode()">
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html
new file mode 100644
index 0000000000..7dd600dbd8
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <link rel="stylesheet" href="editable.css" type="text/css">
+</head>
+<body>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css
new file mode 100644
index 0000000000..99fec49506
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css
@@ -0,0 +1,66 @@
+.b, myb {
+ font-weight: bold;
+}
+
+.i, myi {
+ font-style: italic;
+}
+
+.s, mys {
+ text-decoration: line-through;
+}
+
+.u, myu {
+ text-decoration: underline;
+}
+
+.sub, mysub {
+ vertical-align: sub;
+}
+
+.sup, mysup {
+ vertical-align: super;
+}
+
+.jc, myjc {
+ text-align: center;
+}
+
+.jf, myjf {
+ text-align: justify;
+}
+
+.jl, myjl {
+ text-align: left;
+}
+
+.jr, myjr {
+ text-align: right;
+}
+
+.red, myred {
+ color: red;
+}
+
+.bcred, mybcred {
+ background-color: red;
+}
+
+.large, mylarge {
+ font-size: large;
+}
+
+.fs18px, myfs18px {
+ font-size: 18px;
+}
+
+.courier, mycourier {
+ font-family: courier;
+}
+
+gen::before {
+ content: "[BEFORE]";
+}
+gen::after {
+ content: "[AFTER]";
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js
new file mode 100644
index 0000000000..2236d9dfc5
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js
@@ -0,0 +1,436 @@
+/**
+ * @fileoverview
+ * Canonicalization functions used in the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * Canonicalize HTML entities to their actual character
+ *
+ * @param str {String} the HTML string to be canonicalized
+ * @return {String} the canonicalized string
+ */
+
+function canonicalizeEntities(str) {
+ // TODO(rolandsteiner): this function is very much not optimized, but that shouldn't
+ // theoretically matter too much - look into it at some point.
+ var match;
+ while (match = str.match(/&#x([0-9A-F]+);/i)) {
+ str = str.replace('&#x' + match[1] + ';', String.fromCharCode(parseInt(match[1], 16)));
+ }
+ while (match = str.match(/&#([0-9]+);/)) {
+ str = str.replace('&#' + match[1] + ';', String.fromCharCode(Number(match[1])));
+ }
+ return str;
+}
+
+/**
+ * Canonicalize the contents of the HTML 'style' attribute.
+ * I.e. sorts the CSS attributes alphabetically and canonicalizes the values
+ * CSS attributes where necessary.
+ *
+ * If this would return an empty string, return null instead to suppress the
+ * whole 'style' attribute.
+ *
+ * Avoid tests that contain {, } or : within CSS values!
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * FIXME: does not canonicalize the contents of compound attributes
+ * (e.g., 'border').
+ *
+ * @param str {String} contents of the 'style' attribute
+ * @param emitFlags {Object} flags used for this output
+ * @return {String/null} canonicalized string, null instead of the empty string
+ */
+function canonicalizeStyle(str, emitFlags) {
+ // Remove any enclosing curly brackets
+ str = str.replace(/ ?[\{\}] ?/g, '');
+
+ var attributes = str.split(';');
+ var count = attributes.length;
+ var resultArr = [];
+
+ for (var a = 0; a < count; ++a) {
+ // Retrieve "name: value" pair
+ // Note: may expectedly fail if the last pair was terminated with ';'
+ var avPair = attributes[a].match(/ ?([^ :]+) ?: ?(.+)/);
+ if (!avPair)
+ continue;
+
+ var name = avPair[1];
+ var value = avPair[2].replace(/ $/, ''); // Remove any trailing space.
+
+ switch (name) {
+ case 'color':
+ case 'background-color':
+ case 'border-color':
+ if (emitFlags.canonicalizeUnits) {
+ resultArr.push(name + ': #' + new Color(value).toHexString());
+ } else {
+ resultArr.push(name + ': ' + value);
+ }
+ break;
+
+ case 'font-family':
+ if (emitFlags.canonicalizeUnits) {
+ resultArr.push(name + ': ' + new FontName(value).toString());
+ } else {
+ resultArr.push(name + ': ' + value);
+ }
+ break;
+
+ case 'font-size':
+ if (emitFlags.canonicalizeUnits) {
+ resultArr.push(name + ': ' + new FontSize(value).toString());
+ } else {
+ resultArr.push(name + ': ' + value);
+ }
+ break;
+
+ default:
+ resultArr.push(name + ': ' + value);
+ }
+ }
+
+ // Sort by name, assuming no duplicate CSS attribute names.
+ resultArr.sort();
+
+ return resultArr.join('; ') || null;
+}
+
+/**
+ * Canonicalize a single attribute value.
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * @param elemName {String} the name of the element
+ * @param attrName {String} the name of the attribute
+ * @param attrValue {String} the value of the attribute
+ * @param emitFlags {Object} flags used for this output
+ * @return {String/null} the canonicalized value, or null if the attribute should be skipped.
+ */
+function canonicalizeSingleAttribute(elemName, attrName, attrValue, emitFlags) {
+ // We emit attributes as name="value", so change any contained apostrophes
+ // to quote marks.
+ attrValue = attrValue.replace(/\x22/, '\x27');
+
+ switch (attrName) {
+ case 'class':
+ return emitFlags.emitClass ? attrValue : null;
+
+ case 'id':
+ if (!emitFlags.emitID) {
+ return null;
+ }
+ if (attrValue && attrValue.substr(0, 7) == 'editor-') {
+ return null;
+ }
+ return attrValue;
+
+ // Remove empty style attributes, canonicalize the contents otherwise,
+ // provided the test cares for styles.
+ case 'style':
+ return (emitFlags.emitStyle && attrValue)
+ ? canonicalizeStyle(attrValue, emitFlags)
+ : null;
+
+ // Never output onload handlers as they are set by the test environment.
+ case 'onload':
+ return null;
+
+ // Canonicalize colors.
+ case 'bgcolor':
+ case 'color':
+ if (!attrValue) {
+ return null;
+ }
+ return emitFlags.canonicalizeUnits ? new Color(attrValue).toString() : attrValue;
+
+ // Canonicalize font names.
+ case 'face':
+ return emitFlags.canonicalizeUnits ? new FontName(attrValue).toString() : attrValue;
+
+ // Canonicalize font sizes (leave other 'size' attributes as-is).
+ case 'size':
+ if (!attrValue) {
+ return null;
+ }
+ switch (elemName) {
+ case 'basefont':
+ case 'font':
+ return emitFlags.canonicalizeUnits ? new FontSize(attrValue).toString() : attrValue;
+ }
+ return attrValue;
+
+ // Remove spans with value 1. Retain spans with other values, even if
+ // empty or with a value 0, since those indicate a flawed implementation.
+ case 'colspan':
+ case 'rowspan':
+ case 'span':
+ return (attrValue == '1' || attrValue === '') ? null : attrValue;
+
+ // Boolean attributes: presence equals true. If present, the value must be
+ // the empty string or the attribute's canonical name.
+ // (http://www.whatwg.org/specs/web-apps/current-work/#boolean-attributes)
+ // Below we only normalize empty string to the canonical name for
+ // comparison purposes. All other values are not touched and will therefore
+ // in all likelihood result in a failed test (even if they may be accepted
+ // by the UA).
+ case 'async':
+ case 'autofocus':
+ case 'checked':
+ case 'compact':
+ case 'declare':
+ case 'defer':
+ case 'disabled':
+ case 'formnovalidate':
+ case 'frameborder':
+ case 'ismap':
+ case 'loop':
+ case 'multiple':
+ case 'nohref':
+ case 'nosize':
+ case 'noshade':
+ case 'novalidate':
+ case 'nowrap':
+ case 'open':
+ case 'readonly':
+ case 'required':
+ case 'reversed':
+ case 'seamless':
+ case 'selected':
+ return attrValue ? attrValue : attrName;
+
+ default:
+ return attrValue;
+ }
+}
+
+/**
+ * Canonicalize the contents of an element tag.
+ *
+ * I.e. sorts the attributes alphabetically and canonicalizes their
+ * values where necessary. Also removes attributes we're not interested in.
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * @param str {String} the contens of the element tag, excluding < and >.
+ * @param emitFlags {Object} flags used for this output
+ * @return {String} the canonicalized contents.
+ */
+function canonicalizeElementTag(str, emitFlags) {
+ // FIXME: lowercase only if emitFlags.lowercase is set
+ str = str.toLowerCase();
+
+ var pos = str.search(' ');
+
+ // element name only
+ if (pos == -1) {
+ return str;
+ }
+
+ var elemName = str.substr(0, pos);
+ str = str.substr(pos + 1);
+
+ // Even if emitFlags.emitAttrs is not set, we must iterate over the
+ // attributes to catch the special selection attribute and/or selection
+ // markers. :(
+
+ // Iterate over attributes, add them to an array, canonicalize their
+ // contents, and finally output the (remaining) attributes in sorted order.
+ // Note: We can't do a simple split on space here, because the value of,
+ // e.g., 'style' attributes may also contain spaces.
+ var attrs = [];
+ var selStartInTag = false;
+ var selEndInTag = false;
+
+ while (str) {
+ var attrName;
+ var attrValue = '';
+
+ pos = str.search(/[ =]/);
+ if (pos >= 0) {
+ attrName = str.substr(0, pos);
+ if (str.charAt(pos) == ' ') {
+ ++pos;
+ }
+ if (str.charAt(pos) == '=') {
+ ++pos;
+ if (str.charAt(pos) == ' ') {
+ ++pos;
+ }
+ str = str.substr(pos);
+ switch (str.charAt(0)) {
+ case '"':
+ case "'":
+ pos = str.indexOf(str.charAt(0), 1);
+ pos = (pos < 0) ? str.length : pos;
+ attrValue = str.substring(1, pos);
+ ++pos;
+ break;
+
+ default:
+ pos = str.indexOf(' ', 0);
+ pos = (pos < 0) ? str.length : pos;
+ attrValue = (pos == -1) ? str : str.substr(0, pos);
+ break;
+ }
+ attrValue = attrValue.replace(/^ /, '');
+ attrValue = attrValue.replace(/ $/, '');
+ }
+ } else {
+ attrName = str;
+ }
+ str = (pos == -1 || pos >= str.length) ? '' : str.substr(pos + 1);
+
+ // Remove special selection attributes.
+ switch (attrName) {
+ case ATTRNAME_SEL_START:
+ selStartInTag = true;
+ continue;
+
+ case ATTRNAME_SEL_END:
+ selEndInTag = true;
+ continue;
+ }
+
+ switch (attrName) {
+ case '':
+ case 'onload':
+ case 'xmlns':
+ break;
+
+ default:
+ if (!emitFlags.emitAttrs) {
+ break;
+ }
+ // >>> fall through >>>
+
+ case 'contenteditable':
+ attrValue = canonicalizeEntities(attrValue);
+ attrValue = canonicalizeSingleAttribute(elemName, attrName, attrValue, emitFlags);
+ if (attrValue !== null) {
+ attrs.push(attrName + '="' + attrValue + '"');
+ }
+ }
+ }
+
+ var result = elemName;
+
+ // Sort alphabetically (on full string rather than just attribute value for
+ // simplicity. Also, attribute names will differ when encountering the '=').
+ if (attrs.length > 0) {
+ attrs.sort();
+ result += ' ' + attrs.join(' ');
+ }
+
+ // Add intra-tag selection marker(s) or attribute(s), if any, at the end.
+ if (selStartInTag && selEndInTag) {
+ result += ' |';
+ } else if (selStartInTag) {
+ result += ' {';
+ } else if (selEndInTag) {
+ result += ' }';
+ }
+
+ return result;
+}
+
+/**
+ * Canonicalize elements and attributes to facilitate comparison to the
+ * expectation string: sort attributes, canonicalize values and remove chaff.
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * @param str {String} the HTML string to be canonicalized
+ * @param emitFlags {Object} flags used for this output
+ * @return {String} the canonicalized string
+ */
+function canonicalizeElementsAndAttributes(str, emitFlags) {
+ var tagStart = str.indexOf('<');
+ var tagEnd = 0;
+ var result = '';
+
+ while (tagStart >= 0) {
+ ++tagStart;
+ if (str.charAt(tagStart) == '/') {
+ ++tagStart;
+ }
+ result = result + canonicalizeEntities(str.substring(tagEnd, tagStart));
+ tagEnd = str.indexOf('>', tagStart);
+ if (tagEnd < 0) {
+ tagEnd = str.length - 1;
+ }
+ if (str.charAt(tagEnd - 1) == '/') {
+ --tagEnd;
+ }
+ var elemStr = str.substring(tagStart, tagEnd);
+ elemStr = canonicalizeElementTag(elemStr, emitFlags);
+ result = result + elemStr;
+ tagStart = str.indexOf('<', tagEnd);
+ }
+ return result + canonicalizeEntities(str.substring(tagEnd));
+}
+
+/**
+ * Canonicalize an innerHTML string to uniform single whitespaces.
+ *
+ * FIXME: running this prevents testing for pre-formatted content
+ * and the CSS 'white-space' attribute.
+ *
+ * @param str {String} the HTML string to be canonicalized
+ * @return {String} the canonicalized string
+ */
+function canonicalizeSpaces(str) {
+ // Collapse sequential whitespace.
+ str = str.replace(/\s+/g, ' ');
+
+ // Remove spaces immediately inside angle brackets <, >, </ and />.
+ // While doing this also canonicalize <.../> to <...>.
+ str = str.replace(/\< ?/g, '<');
+ str = str.replace(/\<\/ ?/g, '</');
+ str = str.replace(/ ?\/?\>/g, '>');
+
+ return str;
+}
+
+/**
+ * Canonicalize an innerHTML string to uniform single whitespaces.
+ * Also remove comments to retain only embedded selection markers, and
+ * remove </br> and </hr> if present.
+ *
+ * FIXME: running this prevents testing for pre-formatted content
+ * and the CSS 'white-space' attribute.
+ *
+ * @param str {String} the HTML string to be canonicalized
+ * @return {String} the canonicalized string
+ */
+function initialCanonicalizationOf(str) {
+ str = canonicalizeSpaces(str);
+ str = str.replace(/ ?<!-- ?/g, '');
+ str = str.replace(/ ?--> ?/g, '');
+ str = str.replace(/<\/[bh]r>/g, '');
+
+ return str;
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js
new file mode 100644
index 0000000000..be059cfc86
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js
@@ -0,0 +1,489 @@
+/**
+ * @fileoverview
+ * Comparison functions used in the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * constants used only in the compare functions.
+ */
+var RESULT_DIFF = 0; // actual result doesn't match expectation
+var RESULT_SEL = 1; // actual result matches expectation in HTML only
+var RESULT_EQUAL = 2; // actual result matches expectation in both HTML and selection
+
+/**
+ * Gets the test expectations as an array from the passed-in field.
+ *
+ * @param {Array|String} the test expectation(s) as string or array.
+ * @return {Array} test expectations as an array.
+ */
+function getExpectationArray(expected) {
+ if (expected === undefined) {
+ return [];
+ }
+ if (expected === null) {
+ return [null];
+ }
+ switch (typeof expected) {
+ case 'string':
+ case 'boolean':
+ case 'number':
+ return [expected];
+ }
+ // Assume it's already an array.
+ return expected;
+}
+
+/**
+ * Compare a test result to a single expectation string.
+ *
+ * FIXME: add support for optional elements/attributes.
+ *
+ * @param expected {String} the already canonicalized (with the exception of selection marks) expectation string
+ * @param actual {String} the already canonicalized (with the exception of selection marks) actual result
+ * @return {Integer} one of the RESULT_... return values
+ * @see variables.js for return values
+ */
+function compareHTMLToSingleExpectation(expected, actual) {
+ // If the test checks the selection, then the actual string must match the
+ // expectation exactly.
+ if (expected == actual) {
+ return RESULT_EQUAL;
+ }
+
+ // Remove selection markers and see if strings match then.
+ expected = expected.replace(/ [{}\|]>/g, '>'); // intra-tag
+ expected = expected.replace(/[\[\]\^{}\|]/g, ''); // outside tag
+ actual = actual.replace(/ [{}\|]>/g, '>'); // intra-tag
+ actual = actual.replace(/[\[\]\^{}\|]/g, ''); // outside tag
+
+ return (expected == actual) ? RESULT_SEL : RESULT_DIFF;
+}
+
+/**
+ * Compare the current HTMLtest result to the expectation string(s).
+ *
+ * @param actual {String/Boolean} actual value
+ * @param expected {String/Array} expectation(s)
+ * @param emitFlags {Object} flags to use for canonicalization
+ * @return {Integer} one of the RESULT_... return values
+ * @see variables.js for return values
+ */
+function compareHTMLToExpectation(actual, expected, emitFlags) {
+ // Find the most favorable result among the possible expectation strings.
+ var expectedArr = getExpectationArray(expected);
+ var count = expectedArr ? expectedArr.length : 0;
+ var best = RESULT_DIFF;
+
+ for (var idx = 0; idx < count && best < RESULT_EQUAL; ++idx) {
+ var expected = expectedArr[idx];
+ expected = canonicalizeSpaces(expected);
+ expected = canonicalizeElementsAndAttributes(expected, emitFlags);
+
+ var singleResult = compareHTMLToSingleExpectation(expected, actual);
+
+ best = Math.max(best, singleResult);
+ }
+ return best;
+}
+
+/**
+ * Compare the current HTMLtest result to expected and acceptable results
+ *
+ * @param expected {String/Array} expected result(s)
+ * @param accepted {String/Array} accepted result(s)
+ * @param actual {String} actual result
+ * @param emitFlags {Object} how to canonicalize the HTML strings
+ * @param result {Object} [out] object recieving the result of the comparison.
+ */
+function compareHTMLTestResultTo(expected, accepted, actual, emitFlags, result) {
+ actual = actual.replace(/[\x60\xb4]/g, '');
+ actual = canonicalizeElementsAndAttributes(actual, emitFlags);
+
+ var bestExpected = compareHTMLToExpectation(actual, expected, emitFlags);
+
+ if (bestExpected == RESULT_EQUAL) {
+ // Shortcut - it doesn't get any better
+ result.valresult = VALRESULT_EQUAL;
+ result.selresult = SELRESULT_EQUAL;
+ return;
+ }
+
+ var bestAccepted = compareHTMLToExpectation(actual, accepted, emitFlags);
+
+ switch (bestExpected) {
+ case RESULT_SEL:
+ switch (bestAccepted) {
+ case RESULT_EQUAL:
+ // The HTML was equal to the/an expected HTML result as well
+ // (just not the selection there), therefore the difference
+ // between expected and accepted can only lie in the selection.
+ result.valresult = VALRESULT_EQUAL;
+ result.selresult = SELRESULT_ACCEPT;
+ return;
+
+ case RESULT_SEL:
+ case RESULT_DIFF:
+ // The acceptable expectations did not yield a better result
+ // -> stay with the original (i.e., comparison to 'expected') result.
+ result.valresult = VALRESULT_EQUAL;
+ result.selresult = SELRESULT_DIFF;
+ return;
+ }
+ break;
+
+ case RESULT_DIFF:
+ switch (bestAccepted) {
+ case RESULT_EQUAL:
+ result.valresult = VALRESULT_ACCEPT;
+ result.selresult = SELRESULT_EQUAL;
+ return;
+
+ case RESULT_SEL:
+ result.valresult = VALRESULT_ACCEPT;
+ result.selresult = SELRESULT_DIFF;
+ return;
+
+ case RESULT_DIFF:
+ result.valresult = VALRESULT_DIFF;
+ result.selresult = SELRESULT_NA;
+ return;
+ }
+ break;
+ }
+
+ throw INTERNAL_ERR + HTML_COMPARISON;
+}
+
+/**
+ * Verify that the canaries are unviolated.
+ *
+ * @param container {Object} the test container descriptor as object reference
+ * @param result {Object} object reference that contains the result data
+ * @return {Boolean} whether the canaries' HTML is OK (selection flagged, but not fatal)
+ */
+function verifyCanaries(container, result) {
+ if (!container.canary) {
+ return true;
+ }
+
+ var str = canonicalizeElementsAndAttributes(result.bodyInnerHTML, emitFlagsForCanary);
+
+ if (str.length < 2 * container.canary.length) {
+ result.valresult = VALRESULT_CANARY;
+ result.selresult = SELRESULT_NA;
+ result.output = result.bodyOuterHTML;
+ return false;
+ }
+
+ var strBefore = str.substr(0, container.canary.length);
+ var strAfter = str.substr(str.length - container.canary.length);
+
+ // Verify that the canary stretch doesn't contain any selection markers
+ if (SELECTION_MARKERS.test(strBefore) || SELECTION_MARKERS.test(strAfter)) {
+ str = str.replace(SELECTION_MARKERS, '');
+ if (str.length < 2 * container.canary.length) {
+ result.valresult = VALRESULT_CANARY;
+ result.selresult = SELRESULT_NA;
+ result.output = result.bodyOuterHTML;
+ return false;
+ }
+
+ // Selection escaped contentEditable element, but HTML may still be ok.
+ result.selresult = SELRESULT_CANARY;
+ strBefore = str.substr(0, container.canary.length);
+ strAfter = str.substr(str.length - container.canary.length);
+ }
+
+ if (strBefore !== container.canary || strAfter !== container.canary) {
+ result.valresult = VALRESULT_CANARY;
+ result.selresult = SELRESULT_NA;
+ result.output = result.bodyOuterHTML;
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Compare the current HTMLtest result to the expectation string(s).
+ * Sets the global result variables.
+ *
+ * @param suite {Object} the test suite as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test as object reference
+ * @param container {Object} the test container description
+ * @param result {Object} [in/out] the result description, incl. HTML strings
+ * @see variables.js for result values
+ */
+function compareHTMLTestResult(suite, group, test, container, result) {
+ if (!verifyCanaries(container, result)) {
+ return;
+ }
+
+ var emitFlags = {
+ emitAttrs: getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES),
+ emitStyle: getTestParameter(suite, group, test, PARAM_CHECK_STYLE),
+ emitClass: getTestParameter(suite, group, test, PARAM_CHECK_CLASS),
+ emitID: getTestParameter(suite, group, test, PARAM_CHECK_ID),
+ lowercase: true,
+ canonicalizeUnits: true
+ };
+
+ // 2a.) Compare opening tag -
+ // decide whether to compare vs. outer or inner HTML based on this.
+ var openingTagEnd = result.outerHTML.indexOf('>') + 1;
+ var openingTag = result.outerHTML.substr(0, openingTagEnd);
+
+ openingTag = canonicalizeElementsAndAttributes(openingTag, emitFlags);
+ var tagCmp = compareHTMLToExpectation(openingTag, container.tagOpen, emitFlags);
+
+ if (tagCmp == RESULT_EQUAL) {
+ result.output = result.innerHTML;
+ compareHTMLTestResultTo(
+ getTestParameter(suite, group, test, PARAM_EXPECTED),
+ getTestParameter(suite, group, test, PARAM_ACCEPT),
+ result.innerHTML,
+ emitFlags,
+ result)
+ } else {
+ result.output = result.outerHTML;
+ compareHTMLTestResultTo(
+ getContainerParameter(suite, group, test, container, PARAM_EXPECTED_OUTER),
+ getContainerParameter(suite, group, test, container, PARAM_ACCEPT_OUTER),
+ result.outerHTML,
+ emitFlags,
+ result)
+ }
+}
+
+/**
+ * Insert a selection position indicator.
+ *
+ * @param node {DOMNode} the node where to insert the selection indicator
+ * @param offs {Integer} the offset of the selection indicator
+ * @param textInd {String} the indicator to use if the node is a text node
+ * @param elemInd {String} the indicator to use if the node is an element node
+ */
+function insertSelectionIndicator(node, offs, textInd, elemInd) {
+ switch (node.nodeType) {
+ case DOM_NODE_TYPE_TEXT:
+ // Insert selection marker for text node into text content.
+ var text = node.data;
+ node.data = text.substring(0, offs) + textInd + text.substring(offs);
+ break;
+
+ case DOM_NODE_TYPE_ELEMENT:
+ var child = node.firstChild;
+ try {
+ // node has other children: insert marker as comment node
+ var comment = document.createComment(elemInd);
+ while (child && offs) {
+ --offs;
+ child = child.nextSibling;
+ }
+ if (child) {
+ node.insertBefore(comment, child);
+ } else {
+ node.appendChild(comment);
+ }
+ } catch (ex) {
+ // can't append child comment -> insert as special attribute(s)
+ switch (elemInd) {
+ case '|':
+ node.setAttribute(ATTRNAME_SEL_START, '1');
+ node.setAttribute(ATTRNAME_SEL_END, '1');
+ break;
+
+ case '{':
+ node.setAttribute(ATTRNAME_SEL_START, '1');
+ break;
+
+ case '}':
+ node.setAttribute(ATTRNAME_SEL_END, '1');
+ break;
+ }
+ }
+ break;
+ }
+}
+
+/**
+ * Adds quotes around all text nodes to show cases with non-normalized
+ * text nodes. Those are not a bug, but may still be usefil in helping to
+ * debug erroneous cases.
+ *
+ * @param node {DOMNode} root node from which to descend
+ */
+function encloseTextNodesWithQuotes(node) {
+ switch (node.nodeType) {
+ case DOM_NODE_TYPE_ELEMENT:
+ for (var i = 0; i < node.childNodes.length; ++i) {
+ encloseTextNodesWithQuotes(node.childNodes[i]);
+ }
+ break;
+
+ case DOM_NODE_TYPE_TEXT:
+ node.data = '\x60' + node.data + '\xb4';
+ break;
+ }
+}
+
+/**
+ * Retrieve the result of a test run and do some preliminary canonicalization.
+ *
+ * @param container {Object} the container where to retrieve the result from as object reference
+ * @param result {Object} object reference that contains the result data
+ * @return {String} a preliminarily canonicalized innerHTML with selection markers
+ */
+function prepareHTMLTestResult(container, result) {
+ // Start with empty strings in case any of the below throws.
+ result.innerHTML = '';
+ result.outerHTML = '';
+
+ // 1.) insert selection markers
+ var selRange = createFromWindow(container.win);
+ if (selRange) {
+ // save values, since range object gets auto-modified
+ var node1 = selRange.getAnchorNode();
+ var offs1 = selRange.getAnchorOffset();
+ var node2 = selRange.getFocusNode();
+ var offs2 = selRange.getFocusOffset();
+
+ // add markers
+ if (node1 && node1 == node2 && offs1 == offs2) {
+ // collapsed selection
+ insertSelectionIndicator(node1, offs1, '^', '|');
+ } else {
+ // Start point and end point are different
+ if (node1) {
+ insertSelectionIndicator(node1, offs1, '[', '{');
+ }
+
+ if (node2) {
+ if (node1 == node2 && offs1 < offs2) {
+ // Anchor indicator was inserted under the same node, so we need
+ // to shift the offset by 1
+ ++offs2;
+ }
+ insertSelectionIndicator(node2, offs2, ']', '}');
+ }
+ }
+ }
+
+ // 2.) insert markers for text node boundaries;
+ encloseTextNodesWithQuotes(container.editor);
+
+ // 3.) retrieve inner and outer HTML
+ result.innerHTML = initialCanonicalizationOf(container.editor.innerHTML);
+ result.bodyInnerHTML = initialCanonicalizationOf(container.body.innerHTML);
+ if (goog.userAgent.IE) {
+ result.outerHTML = initialCanonicalizationOf(container.editor.outerHTML);
+ result.bodyOuterHTML = initialCanonicalizationOf(container.body.outerHTML);
+ result.outerHTML = result.outerHTML.replace(/^\s+/, '');
+ result.outerHTML = result.outerHTML.replace(/\s+$/, '');
+ result.bodyOuterHTML = result.bodyOuterHTML.replace(/^\s+/, '');
+ result.bodyOuterHTML = result.bodyOuterHTML.replace(/\s+$/, '');
+ } else {
+ result.outerHTML = initialCanonicalizationOf(new XMLSerializer().serializeToString(container.editor));
+ result.bodyOuterHTML = initialCanonicalizationOf(new XMLSerializer().serializeToString(container.body));
+ }
+}
+
+/**
+ * Compare a text test result to the expectation string(s).
+ *
+ * @param suite {Object} the test suite as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test as object reference
+ * @param actual {String/Boolean} actual value
+ * @param expected {String/Array} expectation(s)
+ * @return {Boolean} whether we found a match
+ */
+function compareTextTestResultWith(suite, group, test, actual, expected) {
+ var expectedArr = getExpectationArray(expected);
+ // Find the most favorable result among the possible expectation strings.
+ var count = expectedArr.length;
+
+ // If the value matches the expectation exactly, then we're fine.
+ for (var idx = 0; idx < count; ++idx) {
+ if (actual === expectedArr[idx])
+ return true;
+ }
+
+ // Otherwise see if we should canonicalize specific value types.
+ //
+ // We only need to look at font name, color and size units if the originating
+ // test was both a) queryCommandValue and b) querying a font name/color/size
+ // specific criterion.
+ //
+ // TODO(rolandsteiner): This is ugly! Refactor!
+ switch (getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ for (var idx = 0; idx < count; ++idx) {
+ if (new Color(actual).compare(new Color(expectedArr[idx])))
+ return true;
+ }
+ return false;
+
+ case 'fontname':
+ for (var idx = 0; idx < count; ++idx) {
+ if (new FontName(actual).compare(new FontName(expectedArr[idx])))
+ return true;
+ }
+ return false;
+
+ case 'fontsize':
+ for (var idx = 0; idx < count; ++idx) {
+ if (new FontSize(actual).compare(new FontSize(expectedArr[idx])))
+ return true;
+ }
+ return false;
+ }
+
+ return false;
+}
+
+/**
+ * Compare the passed-in text test result to the expectation string(s).
+ * Sets the global result variables.
+ *
+ * @param suite {Object} the test suite as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test as object reference
+ * @param actual {String/Boolean} actual value
+ * @return {Integer} a RESUTLHTML... result value
+ * @see variables.js for result values
+ */
+function compareTextTestResult(suite, group, test, result) {
+ var expected = getTestParameter(suite, group, test, PARAM_EXPECTED);
+ if (compareTextTestResultWith(suite, group, test, result.output, expected)) {
+ result.valresult = VALRESULT_EQUAL;
+ return;
+ }
+ var accepted = getTestParameter(suite, group, test, PARAM_ACCEPT);
+ if (accepted && compareTextTestResultWith(suite, group, test, result.output, accepted)) {
+ result.valresult = VALRESULT_ACCEPT;
+ return;
+ }
+ result.valresult = VALRESULT_DIFF;
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js
new file mode 100644
index 0000000000..897efa0112
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js
@@ -0,0 +1,456 @@
+/**
+ * @fileoverview
+ * Functions used to format the test result output.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * Writes a fatal error to the output (replaces alert box)
+ *
+ * @param text {String} text to output
+ */
+function writeFatalError(text) {
+ var errorsStart = document.getElementById('errors');
+ var divider = document.getElementById('divider');
+ if (!errorsStart) {
+ errorsStart = document.createElement('hr');
+ errorsStart.id = 'errors';
+ divider.parentNode.insertBefore(errorsStart, divider);
+ }
+ var error = document.createElement('div');
+ error.className = 'fatalerror';
+ error.innerHTML = 'FATAL ERROR: ' + escapeOutput(text);
+ errorsStart.parentNode.insertBefore(error, divider);
+}
+
+/**
+ * Generates a unique ID for a given single test out of the suite ID and
+ * test ID.
+ *
+ * @param suiteID {string} ID string of the suite
+ * @param testID {string} ID string of the individual tests
+ * @return {string} globally unique ID
+ */
+function generateOutputID(suiteID, testID) {
+ return commonIDPrefix + '-' + suiteID + '_' + testID;
+}
+
+/**
+ * Function to highlight the selection markers
+ *
+ * @param str {String} a HTML string containing selection markers
+ * @return {String} the HTML string with highlighting tags around the markers
+ */
+function highlightSelectionMarkers(str) {
+ str = str.replace(/\[/g, '<span class="sel">[</span>');
+ str = str.replace(/\]/g, '<span class="sel">]</span>');
+ str = str.replace(/\^/g, '<span class="sel">^</span>');
+ str = str.replace(/{/g, '<span class="sel">{</span>');
+ str = str.replace(/}/g, '<span class="sel">}</span>');
+ str = str.replace(/\|/g, '<b class="sel">|</b>');
+ return str;
+}
+
+/**
+ * Function to highlight the selection markers
+ *
+ * @param str {String} a HTML string containing selection markers
+ * @return {String} the HTML string with highlighting tags around the markers
+ */
+function highlightSelectionMarkersAndTextNodes(str) {
+ str = highlightSelectionMarkers(str);
+ str = str.replace(/\x60/g, '<span class="txt">');
+ str = str.replace(/\xb4/g, '</span>');
+ return str;
+}
+
+/**
+ * Function to format output according to type
+ *
+ * @param value {String/Boolean} string or value to format
+ * @return {String} HTML-formatted string
+ */
+function formatValueOrString(value) {
+ if (value === undefined)
+ return '<i>undefined</i>';
+ if (value === null)
+ return '<i>null</i>';
+
+ switch (typeof value) {
+ case 'boolean':
+ return '<i>' + value.toString() + '</i>';
+
+ case 'number':
+ return value.toString();
+
+ case 'string':
+ return "'" + escapeOutput(value) + "'";
+
+ default:
+ return '<i>(' + escapeOutput(value.toString()) + ')</i>';
+ }
+}
+
+/**
+ * Function to highlight text nodes
+ *
+ * @param suite {Object} the suite the test belongs to
+ * @param group {Object} the group within the suite the test belongs to
+ * @param test {Object} the test description as object reference
+ * @param actual {String} a HTML string containing text nodes with markers
+ * @return {String} string with highlighting tags around the text node parts
+ */
+function formatActualResult(suite, group, test, actual) {
+ if (typeof actual != 'string')
+ return formatValueOrString(actual);
+
+ actual = escapeOutput(actual);
+
+ // Fade attributes (or just style) if not actually tested for
+ if (!getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES)) {
+ actual = actual.replace(/([^ =]+)=\x22([^\x22]*)\x22/g, '<span class="fade">$1="$2"</span>');
+ } else {
+ // NOTE: convert 'class="..."' first, before adding other <span class="fade">...</span> !!!
+ if (!getTestParameter(suite, group, test, PARAM_CHECK_CLASS)) {
+ actual = actual.replace(/class=\x22([^\x22]*)\x22/g, '<span class="fade">class="$1"</span>');
+ }
+ if (!getTestParameter(suite, group, test, PARAM_CHECK_STYLE)) {
+ actual = actual.replace(/style=\x22([^\x22]*)\x22/g, '<span class="fade">style="$1"</span>');
+ }
+ if (!getTestParameter(suite, group, test, PARAM_CHECK_ID)) {
+ actual = actual.replace(/id=\x22([^\x22]*)\x22/g, '<span class="fade">id="$1"</span>');
+ } else {
+ // fade out contenteditable host element's 'editor-<xyz>' ID.
+ actual = actual.replace(/id=\x22editor-([^\x22]*)\x22/g, '<span class="fade">id="editor-$1"</span>');
+ }
+ // grey out 'xmlns'
+ actual = actual.replace(/xmlns=\x22([^\x22]*)\x22/g, '<span class="fade">xmlns="$1"</span>');
+ // remove 'onload'
+ actual = actual.replace(/onload=\x22[^\x22]*\x22 ?/g, '');
+ }
+ // Highlight selection markers and text nodes.
+ actual = highlightSelectionMarkersAndTextNodes(actual);
+
+ return actual;
+}
+
+/**
+ * Escape text content for use with .innerHTML.
+ *
+ * @param str {String} HTML text to displayed
+ * @return {String} the escaped HTML
+ */
+function escapeOutput(str) {
+ return str ? str.replace(/\</g, '&lt;').replace(/\>/g, '&gt;') : '';
+}
+
+/**
+ * Fills in a single output table cell
+ *
+ * @param id {String} ID of the table cell
+ * @param val {String} inner HTML to set
+ * @param ttl {String, optional} value of the 'title' attribute
+ * @param cls {String, optional} class name for the cell
+ */
+function setTD(id, val, ttl, cls) {
+ var td = document.getElementById(id);
+ if (td) {
+ td.innerHTML = val;
+ if (ttl) {
+ td.title = ttl;
+ }
+ if (cls) {
+ td.className = cls;
+ }
+ }
+}
+
+/**
+ * Outputs the results of a single test suite
+ *
+ * @param suite {Object} test suite as object reference
+ * @param clsID {String} test class ID ('Proposed', 'RFC', 'Final')
+ * @param group {Object} the group of tests within the suite the test belongs to
+ * @param testIdx {Object} the test as object reference
+ */
+function outputTestResults(suite, clsID, group, test) {
+ var suiteID = suite.id;
+ var cls = suite[clsID];
+ var trID = generateOutputID(suiteID, test.id);
+ var testResult = results[suiteID][clsID][test.id];
+ var testValOut = VALOUTPUT[testResult.valresult];
+ var testSelOut = SELOUTPUT[testResult.selresult];
+
+ var suiteChecksSelOnly = !suiteChecksHTMLOrText(suite);
+ var testUsesHTML = !!getTestParameter(suite, group, test, PARAM_EXECCOMMAND) ||
+ !!getTestParameter(suite, group, test, PARAM_FUNCTION);
+
+ // Set background color for test ID
+ var td = document.getElementById(trID + IDOUT_TESTID);
+ if (td) {
+ td.className = (suiteChecksSelOnly && testResult.selresult != SELRESULT_NA) ? testSelOut.css : testValOut.css;
+ }
+
+ // Fill in "Command" and "Value" cells
+ var cmd;
+ var cmdOutput = '&nbsp;';
+ var valOutput = '&nbsp;';
+
+ if (cmd = getTestParameter(suite, group, test, PARAM_EXECCOMMAND)) {
+ cmdOutput = escapeOutput(cmd);
+ var val = getTestParameter(suite, group, test, PARAM_VALUE);
+ if (val !== undefined) {
+ valOutput = formatValueOrString(val);
+ }
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_FUNCTION)) {
+ cmdOutput = '<i>' + escapeOutput(cmd) + '</i>';
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSUPPORTED)) {
+ cmdOutput = '<i>queryCommandSupported</i>';
+ valOutput = escapeOutput(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDENABLED)) {
+ cmdOutput = '<i>queryCommandEnabled</i>';
+ valOutput = escapeOutput(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDINDETERM)) {
+ cmdOutput = '<i>queryCommandIndeterm</i>';
+ valOutput = escapeOutput(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSTATE)) {
+ cmdOutput = '<i>queryCommandState</i>';
+ valOutput = escapeOutput(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) {
+ cmdOutput = '<i>queryCommandValue</i>';
+ valOutput = escapeOutput(cmd);
+ } else {
+ cmdOutput = '<i>(none)</i>';
+ }
+ setTD(trID + IDOUT_COMMAND, cmdOutput);
+ setTD(trID + IDOUT_VALUE, valOutput);
+
+ // Fill in "Attribute checked?" and "Style checked?" cells
+ if (testUsesHTML) {
+ var checkAttrs = getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES);
+ var checkStyle = getTestParameter(suite, group, test, PARAM_CHECK_STYLE);
+
+ setTD(trID + IDOUT_CHECKATTRS,
+ checkAttrs ? OUTSTR_YES : OUTSTR_NO,
+ checkAttrs ? 'attributes must match' : 'attributes are ignored');
+
+ if (checkAttrs && checkStyle) {
+ setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_YES, 'style attribute contents must match');
+ } else if (checkAttrs) {
+ setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NO, 'style attribute contents is ignored');
+ } else {
+ setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NO, 'all attributes (incl. style) are ignored');
+ }
+ } else {
+ setTD(trID + IDOUT_CHECKATTRS, OUTSTR_NA, 'attributes not applicable');
+ setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NA, 'style not applicable');
+ }
+
+ // Fill in test pad specification cell (initial HTML + selection markers)
+ setTD(trID + IDOUT_PAD, highlightSelectionMarkers(escapeOutput(getTestParameter(suite, group, test, PARAM_PAD))));
+
+ // Fill in expected result(s) cell
+ var expectedOutput = '';
+ var expectedArr = getExpectationArray(getTestParameter(suite, group, test, PARAM_EXPECTED));
+ for (var idx = 0; idx < expectedArr.length; ++idx) {
+ if (expectedOutput) {
+ expectedOutput += '\xA0\xA0\xA0<i>or</i><br>';
+ }
+ expectedOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(expectedArr[idx]))
+ : formatValueOrString(expectedArr[idx]);
+ }
+ var acceptedArr = getExpectationArray(getTestParameter(suite, group, test, PARAM_ACCEPT));
+ for (var idx = 0; idx < acceptedArr.length; ++idx) {
+ expectedOutput += '<span class="accexp">\xA0\xA0\xA0<i>or</i></span><br><span class="accexp">';
+ expectedOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(acceptedArr[idx]))
+ : formatValueOrString(acceptedArr[idx]);
+ expectedOutput += '</span>';
+ }
+ // TODO(rolandsteiner): THIS IS UGLY, relying on 'div' container being index 2,
+ // AND not allowing other containers to have 'outer' results - change!!!
+ var outerOutput = '';
+ expectedArr = getExpectationArray(getContainerParameter(suite, group, test, containers[2], PARAM_EXPECTED_OUTER));
+ for (var idx = 0; idx < expectedArr.length; ++idx) {
+ if (outerOutput) {
+ outerOutput += '\xA0\xA0\xA0<i>or</i><br>';
+ }
+ outerOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(expectedArr[idx]))
+ : formatValueOrString(expectedArr[idx]);
+ }
+ acceptedArr = getExpectationArray(getContainerParameter(suite, group, test, containers[2], PARAM_ACCEPT_OUTER));
+ for (var idx = 0; idx < acceptedArr.length; ++idx) {
+ if (outerOutput) {
+ outerOutput += '<span class="accexp">\xA0\xA0\xA0<i>or</i></span><br>';
+ }
+ outerOutput += '<span class="accexp">';
+ outerOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(acceptedArr[idx]))
+ : formatValueOrString(acceptedArr[idx]);
+ outerOutput += '</span>';
+ }
+ if (outerOutput) {
+ expectedOutput += '<hr>' + outerOutput;
+ }
+ setTD(trID + IDOUT_EXPECTED, expectedOutput);
+
+ // Iterate over the individual container results
+ for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) {
+ var cntID = containers[cntIdx].id;
+ var cntTD = document.getElementById(trID + IDOUT_CONTAINER + cntID);
+ var cntResult = testResult[cntID];
+ var cntValOut = VALOUTPUT[cntResult.valresult];
+ var cntSelOut = SELOUTPUT[cntResult.selresult];
+ var cssVal = cntValOut.css;
+ var cssSel = (!suiteChecksSelOnly || cntResult.selresult != SELRESULT_NA) ? cntSelOut.css : cssVal;
+ var cssCnt = cssVal;
+
+ // Fill in result status cell ("PASS", "ACC.", "FAIL", "EXC.", etc.)
+ setTD(trID + IDOUT_STATUSVAL + cntID, cntValOut.output, cntValOut.title, cssVal);
+
+ // Fill in selection status cell ("PASS", "ACC.", "FAIL", "N/A")
+ setTD(trID + IDOUT_STATUSSEL + cntID, cntSelOut.output, cntSelOut.title, cssSel);
+
+ // Fill in actual result
+ switch (cntResult.valresult) {
+ case VALRESULT_SETUP_EXCEPTION:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ SETUP_EXCEPTION + '(mouseover)',
+ escapeOutput(cntResult.output),
+ cssVal);
+ break;
+
+ case VALRESULT_EXECUTION_EXCEPTION:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ EXECUTION_EXCEPTION + '(mouseover)',
+ escapeOutput(cntResult.output.toString()),
+ cssVal);
+ break;
+
+ case VALRESULT_VERIFICATION_EXCEPTION:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ VERIFICATION_EXCEPTION + '(mouseover)',
+ escapeOutput(cntResult.output.toString()),
+ cssVal);
+ break;
+
+ case VALRESULT_UNSUPPORTED:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ escapeOutput(cntResult.output),
+ '',
+ cssVal);
+ break;
+
+ case VALRESULT_CANARY:
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ highlightSelectionMarkersAndTextNodes(escapeOutput(cntResult.output)),
+ '',
+ cssVal);
+ break;
+
+ case VALRESULT_DIFF:
+ case VALRESULT_ACCEPT:
+ case VALRESULT_EQUAL:
+ if (!testUsesHTML) {
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ formatValueOrString(cntResult.output),
+ '',
+ cssVal);
+ } else if (cntResult.selresult == SELRESULT_CANARY) {
+ cssCnt = suiteChecksSelOnly ? cssSel : cssVal;
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ highlightSelectionMarkersAndTextNodes(escapeOutput(cntResult.output)),
+ '',
+ cssCnt);
+ } else {
+ cssCnt = suiteChecksSelOnly ? cssSel : cssVal;
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ formatActualResult(suite, group, test, cntResult.output),
+ '',
+ cssCnt);
+ }
+ break;
+
+ default:
+ cssCnt = 'exception';
+ setTD(trID + IDOUT_ACTUAL + cntID,
+ INTERNAL_ERR + 'UNKNOWN RESULT VALUE',
+ '',
+ cssCnt);
+ }
+
+ if (cntTD) {
+ cntTD.className = cssCnt;
+ }
+ }
+}
+
+/**
+ * Outputs the results of a single test suite
+ *
+ * @param {Object} suite as object reference
+ */
+function outputTestSuiteResults(suite) {
+ var suiteID = suite.id;
+ var span;
+
+ span = document.getElementById(suiteID + '-score');
+ if (span) {
+ span.innerHTML = results[suiteID].valscore + '/' + results[suiteID].count;
+ }
+ span = document.getElementById(suiteID + '-selscore');
+ if (span) {
+ span.innerHTML = results[suiteID].selscore + '/' + results[suiteID].count;
+ }
+ span = document.getElementById(suiteID + '-time');
+ if (span) {
+ span.innerHTML = results[suiteID].time;
+ }
+ span = document.getElementById(suiteID + '-progress');
+ if (span) {
+ span.style.color = 'green';
+ }
+
+ for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) {
+ var clsID = testClassIDs[clsIdx];
+ var cls = suite[clsID];
+ if (!cls)
+ continue;
+
+ span = document.getElementById(suiteID + '-' + clsID + '-score');
+ if (span) {
+ span.innerHTML = results[suiteID][clsID].valscore + '/' + results[suiteID][clsID].count;
+ }
+ span = document.getElementById(suiteID + '-' + clsID + '-selscore');
+ if (span) {
+ span.innerHTML = results[suiteID][clsID].selscore + '/' + results[suiteID][clsID].count;
+ }
+
+ var groupCount = cls.length;
+
+ for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) {
+ var group = cls[groupIdx];
+ var testCount = group.tests.length;
+
+ for (var testIdx = 0; testIdx < testCount; ++testIdx) {
+ var test = group.tests[testIdx];
+
+ outputTestResults(suite, clsID, group, test);
+ }
+ }
+ }
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js
new file mode 100644
index 0000000000..282f0d907d
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js
@@ -0,0 +1,269 @@
+/**
+ * @fileoverview
+ * Functions used to handle test and expectation strings.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * Normalize text selection indicators and convert inter-element selection
+ * indicators to comments.
+ *
+ * Note that this function relies on the spaces of the input string already
+ * having been normalized by canonicalizeSpaces!
+ *
+ * @param pad {String} HTML string that includes selection marker characters
+ * @return {String} the HTML string with the selection markers converted
+ */
+function convertSelectionIndicators(pad) {
+ // Sanity check: Markers { } | only directly before or after an element,
+ // or just before a closing > (i.e., not within a text node).
+ // Note that intra-tag selection markers have already been converted to the
+ // special selection attribute(s) above.
+ if (/[^>][{}\|][^<>]/.test(pad) ||
+ /^[{}\|][^<]/.test(pad) ||
+ /[^>][{}\|]$/.test(pad) ||
+ /^[{}\|]*$/.test(pad)) {
+ throw SETUP_BAD_SELECTION_SPEC;
+ }
+
+ // Convert intra-tag selection markers to special attributes.
+ pad = pad.replace(/\{\>/g, ATTRNAME_SEL_START + '="1">');
+ pad = pad.replace(/\}\>/g, ATTRNAME_SEL_END + '="1">');
+ pad = pad.replace(/\|\>/g, ATTRNAME_SEL_START + '="1" ' +
+ ATTRNAME_SEL_END + '="1">');
+
+ // Convert remaining {, }, | to comments with '[' and ']' data.
+ pad = pad.replace('{', '<!--[-->');
+ pad = pad.replace('}', '<!--]-->');
+ pad = pad.replace('|', '<!--[--><!--]-->');
+
+ // Convert caret indicator ^ to empty selection indicator []
+ // (this simplifies further processing).
+ pad = pad.replace(/\^/, '[]');
+
+ return pad;
+}
+
+/**
+ * Derives one point of the selection from the indicators with the HTML tree:
+ * '[' or ']' within a text or comment node, or the special selection
+ * attributes within an element node.
+ *
+ * @param root {DOMNode} root node of the recursive search
+ * @param marker {String} which marker to look for: '[' or ']'
+ * @return {Object} a pair object: {node: {DOMNode}/null, offset: {Integer}}
+ */
+function deriveSelectionPoint(root, marker) {
+ switch (root.nodeType) {
+ case DOM_NODE_TYPE_ELEMENT:
+ if (root.attributes) {
+ // Note: getAttribute() is necessary for this to work on all browsers!
+ if (marker == '[' && root.getAttribute(ATTRNAME_SEL_START)) {
+ root.removeAttribute(ATTRNAME_SEL_START);
+ return {node: root, offs: 0};
+ }
+ if (marker == ']' && root.getAttribute(ATTRNAME_SEL_END)) {
+ root.removeAttribute(ATTRNAME_SEL_END);
+ return {node: root, offs: 0};
+ }
+ }
+ for (var i = 0; i < root.childNodes.length; ++i) {
+ var pair = deriveSelectionPoint(root.childNodes[i], marker);
+ if (pair.node) {
+ return pair;
+ }
+ }
+ break;
+
+ case DOM_NODE_TYPE_TEXT:
+ var pos = root.data.indexOf(marker);
+ if (pos != -1) {
+ // Remove selection marker from text.
+ var nodeText = root.data;
+ root.data = nodeText.substr(0, pos) + nodeText.substr(pos + 1);
+ return {node: root, offs: pos };
+ }
+ break;
+
+ case DOM_NODE_TYPE_COMMENT:
+ var pos = root.data.indexOf(marker);
+ if (pos != -1) {
+ // Remove comment node from parent.
+ var helper = root.previousSibling;
+
+ for (pos = 0; helper; ++pos ) {
+ helper = helper.previousSibling;
+ }
+ helper = root;
+ root = root.parentNode;
+ root.removeChild(helper);
+ return {node: root, offs: pos };
+ }
+ break;
+ }
+
+ return {node: null, offs: 0 };
+}
+
+/**
+ * Initialize the test HTML with the starting state specified in the test.
+ *
+ * The selection is specified "inline", using special characters:
+ * ^ a collapsed text caret selection (same as [])
+ * [ the selection start within a text node
+ * ] the selection end within a text node
+ * | collapsed selection between elements (same as {})
+ * { selection starting with the following element
+ * } selection ending with the preceding element
+ * {, } and | can also be used within an element tag, just before the closing
+ * angle bracket > to specify a selection [element, 0] where the element
+ * doesn't otherwise have any children. Ex.: <hr {>foobarbaz<hr }>
+ *
+ * Explicit and implicit specification can also be mixed between the 2 points.
+ *
+ * A pad string must only contain at most ONE of the above that is suitable for
+ * that start or end point, respectively, and must contain either both or none.
+ *
+ * @param suite {Object} suite that test originates in as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} test to be run as object reference
+ * @param container {Object} container descriptor as object reference
+ */
+function initContainer(suite, group, test, container) {
+ var pad = getTestParameter(suite, group, test, PARAM_PAD);
+ pad = canonicalizeSpaces(pad);
+ pad = convertSelectionIndicators(pad);
+
+ if (container.editorID) {
+ container.body.innerHTML = container.canary + container.tagOpen + pad + container.tagClose + container.canary;
+ container.editor = container.doc.getElementById(container.editorID);
+ } else {
+ container.body.innerHTML = pad;
+ container.editor = container.body;
+ }
+
+ win = container.win;
+ doc = container.doc;
+ body = container.body;
+ editor = container.editor;
+ sel = null;
+
+ if (!editor) {
+ throw SETUP_CONTAINER;
+ }
+
+ if (getTestParameter(suite, group, test, PARAM_STYLE_WITH_CSS)) {
+ try {
+ container.doc.execCommand('styleWithCSS', false, true);
+ } catch (ex) {
+ // ignore exception if unsupported
+ }
+ }
+
+ var selAnchor = deriveSelectionPoint(editor, '[');
+ var selFocus = deriveSelectionPoint(editor, ']');
+
+ // sanity check
+ if (!selAnchor || !selFocus) {
+ throw SETUP_SELECTION;
+ }
+
+ if (!selAnchor.node || !selFocus.node) {
+ if (selAnchor.node || selFocus.node) {
+ // Broken test: only one selection point was specified
+ throw SETUP_BAD_SELECTION_SPEC;
+ }
+ sel = null;
+ return;
+ }
+
+ if (selAnchor.node === selFocus.node) {
+ if (selAnchor.offs > selFocus.offs) {
+ // Both selection points are within the same node, the selection was
+ // specified inline (thus the end indicator element or character was
+ // removed), and the end point is before the start (reversed selection).
+ // Start offset that was derived is now off by 1 and needs adjustment.
+ --selAnchor.offs;
+ }
+
+ if (selAnchor.offs === selFocus.offs) {
+ createCaret(selAnchor.node, selAnchor.offs).select();
+ try {
+ sel = win.getSelection();
+ } catch (ex) {
+ sel = undefined;
+ }
+ return;
+ }
+ }
+
+ createFromNodes(selAnchor.node, selAnchor.offs, selFocus.node, selFocus.offs).select();
+
+ try {
+ sel = win.getSelection();
+ } catch (ex) {
+ sel = undefined;
+ }
+}
+
+/**
+ * Reset the editor element after a test is run.
+ *
+ * @param container {Object} container descriptor as object reference
+ */
+function resetContainer(container) {
+ // Remove errant styles and attributes that may have been set on the <body>.
+ container.body.removeAttribute('style');
+ container.body.removeAttribute('color');
+ container.body.removeAttribute('bgcolor');
+
+ try {
+ container.doc.execCommand('styleWithCSS', false, false);
+ } catch (ex) {
+ // Ignore exception if unsupported.
+ }
+}
+
+/**
+ * Initialize the editor document.
+ */
+function initEditorDocs() {
+ for (var c = 0; c < containers.length; ++c) {
+ var container = containers[c];
+
+ container.iframe = document.getElementById('iframe-' + container.id);
+ container.win = container.iframe.contentWindow;
+ container.doc = container.win.document;
+ container.body = container.doc.body;
+ // container.editor is set per test (changes on embedded editor elements).
+
+ // Some browsers require a selection to go with their 'styleWithCSS'.
+ try {
+ container.win.getSelection().selectAllChildren(editor);
+ } catch (ex) {
+ // ignore exception if unsupported
+ }
+ // Default styleWithCSS to false.
+ try {
+ container.doc.execCommand('styleWithCSS', false, false);
+ } catch (ex) {
+ // ignore exception if unsupported
+ }
+ }
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js
new file mode 100644
index 0000000000..24aef7ae9c
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js
@@ -0,0 +1,5 @@
+goog.require('goog.dom.Range');
+
+window.createFromWindow = goog.dom.Range.createFromWindow;
+window.createFromNodes = goog.dom.Range.createFromNodes;
+window.createCaret = goog.dom.Range.createCaret;
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js
new file mode 100644
index 0000000000..3266761115
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js
@@ -0,0 +1,6184 @@
+var COMPILED = false;
+var goog = goog || {};
+goog.global = this;
+goog.DEBUG = true;
+goog.LOCALE = "en";
+goog.evalWorksForGlobals_ = null;
+goog.provide = function(name) {
+ if(!COMPILED) {
+ if(goog.getObjectByName(name) && !goog.implicitNamespaces_[name]) {
+ throw Error('Namespace "' + name + '" already declared.');
+ }
+ var namespace = name;
+ while(namespace = namespace.substring(0, namespace.lastIndexOf("."))) {
+ goog.implicitNamespaces_[namespace] = true
+ }
+ }
+ goog.exportPath_(name)
+};
+if(!COMPILED) {
+ goog.implicitNamespaces_ = {}
+}
+goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) {
+ var parts = name.split(".");
+ var cur = opt_objectToExportTo || goog.global;
+ if(!(parts[0] in cur) && cur.execScript) {
+ cur.execScript("var " + parts[0])
+ }
+ for(var part;parts.length && (part = parts.shift());) {
+ if(!parts.length && goog.isDef(opt_object)) {
+ cur[part] = opt_object
+ }else {
+ if(cur[part]) {
+ cur = cur[part]
+ }else {
+ cur = cur[part] = {}
+ }
+ }
+ }
+};
+goog.getObjectByName = function(name, opt_obj) {
+ var parts = name.split(".");
+ var cur = opt_obj || goog.global;
+ for(var part;part = parts.shift();) {
+ if(cur[part]) {
+ cur = cur[part]
+ }else {
+ return null
+ }
+ }
+ return cur
+};
+goog.globalize = function(obj, opt_global) {
+ var global = opt_global || goog.global;
+ for(var x in obj) {
+ global[x] = obj[x]
+ }
+};
+goog.addDependency = function(relPath, provides, requires) {
+ if(!COMPILED) {
+ var provide, require;
+ var path = relPath.replace(/\\/g, "/");
+ var deps = goog.dependencies_;
+ for(var i = 0;provide = provides[i];i++) {
+ deps.nameToPath[provide] = path;
+ if(!(path in deps.pathToNames)) {
+ deps.pathToNames[path] = {}
+ }
+ deps.pathToNames[path][provide] = true
+ }
+ for(var j = 0;require = requires[j];j++) {
+ if(!(path in deps.requires)) {
+ deps.requires[path] = {}
+ }
+ deps.requires[path][require] = true
+ }
+ }
+};
+goog.require = function(rule) {
+ if(!COMPILED) {
+ if(goog.getObjectByName(rule)) {
+ return
+ }
+ var path = goog.getPathFromDeps_(rule);
+ if(path) {
+ goog.included_[path] = true;
+ goog.writeScripts_()
+ }else {
+ var errorMessage = "goog.require could not find: " + rule;
+ if(goog.global.console) {
+ goog.global.console["error"](errorMessage)
+ }
+ throw Error(errorMessage);
+ }
+ }
+};
+goog.basePath = "";
+goog.global.CLOSURE_BASE_PATH;
+goog.nullFunction = function() {
+};
+goog.identityFunction = function(var_args) {
+ return arguments[0]
+};
+goog.abstractMethod = function() {
+ throw Error("unimplemented abstract method");
+};
+goog.addSingletonGetter = function(ctor) {
+ ctor.getInstance = function() {
+ return ctor.instance_ || (ctor.instance_ = new ctor)
+ }
+};
+if(!COMPILED) {
+ goog.included_ = {};
+ goog.dependencies_ = {pathToNames:{}, nameToPath:{}, requires:{}, visited:{}, written:{}};
+ goog.inHtmlDocument_ = function() {
+ var doc = goog.global.document;
+ return typeof doc != "undefined" && "write" in doc
+ };
+ goog.findBasePath_ = function() {
+ if(!goog.inHtmlDocument_()) {
+ return
+ }
+ var doc = goog.global.document;
+ if(goog.global.CLOSURE_BASE_PATH) {
+ goog.basePath = goog.global.CLOSURE_BASE_PATH;
+ return
+ }
+ var scripts = doc.getElementsByTagName("script");
+ for(var i = scripts.length - 1;i >= 0;--i) {
+ var src = scripts[i].src;
+ var l = src.length;
+ if(src.substr(l - 7) == "base.js") {
+ goog.basePath = src.substr(0, l - 7);
+ return
+ }
+ }
+ };
+ goog.writeScriptTag_ = function(src) {
+ if(goog.inHtmlDocument_() && !goog.dependencies_.written[src]) {
+ goog.dependencies_.written[src] = true;
+ var doc = goog.global.document;
+ doc.write('<script type="text/javascript" src="' + src + '"></' + "script>")
+ }
+ };
+ goog.writeScripts_ = function() {
+ var scripts = [];
+ var seenScript = {};
+ var deps = goog.dependencies_;
+ function visitNode(path) {
+ if(path in deps.written) {
+ return
+ }
+ if(path in deps.visited) {
+ if(!(path in seenScript)) {
+ seenScript[path] = true;
+ scripts.push(path)
+ }
+ return
+ }
+ deps.visited[path] = true;
+ if(path in deps.requires) {
+ for(var requireName in deps.requires[path]) {
+ if(requireName in deps.nameToPath) {
+ visitNode(deps.nameToPath[requireName])
+ }else {
+ if(!goog.getObjectByName(requireName)) {
+ throw Error("Undefined nameToPath for " + requireName);
+ }
+ }
+ }
+ }
+ if(!(path in seenScript)) {
+ seenScript[path] = true;
+ scripts.push(path)
+ }
+ }
+ for(var path in goog.included_) {
+ if(!deps.written[path]) {
+ visitNode(path)
+ }
+ }
+ for(var i = 0;i < scripts.length;i++) {
+ if(scripts[i]) {
+ goog.writeScriptTag_(goog.basePath + scripts[i])
+ }else {
+ throw Error("Undefined script input");
+ }
+ }
+ };
+ goog.getPathFromDeps_ = function(rule) {
+ if(rule in goog.dependencies_.nameToPath) {
+ return goog.dependencies_.nameToPath[rule]
+ }else {
+ return null
+ }
+ };
+ goog.findBasePath_();
+}
+goog.typeOf = function(value) {
+ var s = typeof value;
+ if(s == "object") {
+ if(value) {
+ if(value instanceof Array || !(value instanceof Object) && Object.prototype.toString.call(value) == "[object Array]" || typeof value.length == "number" && typeof value.splice != "undefined" && typeof value.propertyIsEnumerable != "undefined" && !value.propertyIsEnumerable("splice")) {
+ return"array"
+ }
+ if(!(value instanceof Object) && (Object.prototype.toString.call(value) == "[object Function]" || typeof value.call != "undefined" && typeof value.propertyIsEnumerable != "undefined" && !value.propertyIsEnumerable("call"))) {
+ return"function"
+ }
+ }else {
+ return"null"
+ }
+ }else {
+ if(s == "function" && typeof value.call == "undefined") {
+ return"object"
+ }
+ }
+ return s
+};
+goog.propertyIsEnumerableCustom_ = function(object, propName) {
+ if(propName in object) {
+ for(var key in object) {
+ if(key == propName && Object.prototype.hasOwnProperty.call(object, propName)) {
+ return true
+ }
+ }
+ }
+ return false
+};
+goog.propertyIsEnumerable_ = function(object, propName) {
+ if(object instanceof Object) {
+ return Object.prototype.propertyIsEnumerable.call(object, propName)
+ }else {
+ return goog.propertyIsEnumerableCustom_(object, propName)
+ }
+};
+goog.isDef = function(val) {
+ return val !== undefined
+};
+goog.isNull = function(val) {
+ return val === null
+};
+goog.isDefAndNotNull = function(val) {
+ return val != null
+};
+goog.isArray = function(val) {
+ return goog.typeOf(val) == "array"
+};
+goog.isArrayLike = function(val) {
+ var type = goog.typeOf(val);
+ return type == "array" || type == "object" && typeof val.length == "number"
+};
+goog.isDateLike = function(val) {
+ return goog.isObject(val) && typeof val.getFullYear == "function"
+};
+goog.isString = function(val) {
+ return typeof val == "string"
+};
+goog.isBoolean = function(val) {
+ return typeof val == "boolean"
+};
+goog.isNumber = function(val) {
+ return typeof val == "number"
+};
+goog.isFunction = function(val) {
+ return goog.typeOf(val) == "function"
+};
+goog.isObject = function(val) {
+ var type = goog.typeOf(val);
+ return type == "object" || type == "array" || type == "function"
+};
+goog.getUid = function(obj) {
+ return obj[goog.UID_PROPERTY_] || (obj[goog.UID_PROPERTY_] = ++goog.uidCounter_)
+};
+goog.removeUid = function(obj) {
+ if("removeAttribute" in obj) {
+ obj.removeAttribute(goog.UID_PROPERTY_)
+ }
+ try {
+ delete obj[goog.UID_PROPERTY_]
+ }catch(ex) {
+ }
+};
+goog.UID_PROPERTY_ = "closure_uid_" + Math.floor(Math.random() * 2147483648).toString(36);
+goog.uidCounter_ = 0;
+goog.getHashCode = goog.getUid;
+goog.removeHashCode = goog.removeUid;
+goog.cloneObject = function(obj) {
+ var type = goog.typeOf(obj);
+ if(type == "object" || type == "array") {
+ if(obj.clone) {
+ return obj.clone()
+ }
+ var clone = type == "array" ? [] : {};
+ for(var key in obj) {
+ clone[key] = goog.cloneObject(obj[key])
+ }
+ return clone
+ }
+ return obj
+};
+Object.prototype.clone;
+goog.bind = function(fn, selfObj, var_args) {
+ var context = selfObj || goog.global;
+ if(arguments.length > 2) {
+ var boundArgs = Array.prototype.slice.call(arguments, 2);
+ return function() {
+ var newArgs = Array.prototype.slice.call(arguments);
+ Array.prototype.unshift.apply(newArgs, boundArgs);
+ return fn.apply(context, newArgs)
+ }
+ }else {
+ return function() {
+ return fn.apply(context, arguments)
+ }
+ }
+};
+goog.partial = function(fn, var_args) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return function() {
+ var newArgs = Array.prototype.slice.call(arguments);
+ newArgs.unshift.apply(newArgs, args);
+ return fn.apply(this, newArgs)
+ }
+};
+goog.mixin = function(target, source) {
+ for(var x in source) {
+ target[x] = source[x]
+ }
+};
+goog.now = Date.now || function() {
+ return+new Date
+};
+goog.globalEval = function(script) {
+ if(goog.global.execScript) {
+ goog.global.execScript(script, "JavaScript")
+ }else {
+ if(goog.global.eval) {
+ if(goog.evalWorksForGlobals_ == null) {
+ goog.global.eval("var _et_ = 1;");
+ if(typeof goog.global["_et_"] != "undefined") {
+ delete goog.global["_et_"];
+ goog.evalWorksForGlobals_ = true
+ }else {
+ goog.evalWorksForGlobals_ = false
+ }
+ }
+ if(goog.evalWorksForGlobals_) {
+ goog.global.eval(script)
+ }else {
+ var doc = goog.global.document;
+ var scriptElt = doc.createElement("script");
+ scriptElt.type = "text/javascript";
+ scriptElt.defer = false;
+ scriptElt.appendChild(doc.createTextNode(script));
+ doc.body.appendChild(scriptElt);
+ doc.body.removeChild(scriptElt)
+ }
+ }else {
+ throw Error("goog.globalEval not available");
+ }
+ }
+};
+goog.typedef = true;
+goog.cssNameMapping_;
+goog.getCssName = function(className, opt_modifier) {
+ var cssName = className + (opt_modifier ? "-" + opt_modifier : "");
+ return goog.cssNameMapping_ && cssName in goog.cssNameMapping_ ? goog.cssNameMapping_[cssName] : cssName
+};
+goog.setCssNameMapping = function(mapping) {
+ goog.cssNameMapping_ = mapping
+};
+goog.getMsg = function(str, opt_values) {
+ var values = opt_values || {};
+ for(var key in values) {
+ var value = ("" + values[key]).replace(/\$/g, "$$$$");
+ str = str.replace(new RegExp("\\{\\$" + key + "\\}", "gi"), value)
+ }
+ return str
+};
+goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) {
+ goog.exportPath_(publicPath, object, opt_objectToExportTo)
+};
+goog.exportProperty = function(object, publicName, symbol) {
+ object[publicName] = symbol
+};
+goog.inherits = function(childCtor, parentCtor) {
+ function tempCtor() {
+ }
+ tempCtor.prototype = parentCtor.prototype;
+ childCtor.superClass_ = parentCtor.prototype;
+ childCtor.prototype = new tempCtor;
+ childCtor.prototype.constructor = childCtor
+};
+goog.base = function(me, opt_methodName, var_args) {
+ var caller = arguments.callee.caller;
+ if(caller.superClass_) {
+ return caller.superClass_.constructor.apply(me, Array.prototype.slice.call(arguments, 1))
+ }
+ var args = Array.prototype.slice.call(arguments, 2);
+ var foundCaller = false;
+ for(var ctor = me.constructor;ctor;ctor = ctor.superClass_ && ctor.superClass_.constructor) {
+ if(ctor.prototype[opt_methodName] === caller) {
+ foundCaller = true
+ }else {
+ if(foundCaller) {
+ return ctor.prototype[opt_methodName].apply(me, args)
+ }
+ }
+ }
+ if(me[opt_methodName] === caller) {
+ return me.constructor.prototype[opt_methodName].apply(me, args)
+ }else {
+ throw Error("goog.base called from a method of one name " + "to a method of a different name");
+ }
+};
+goog.scope = function(fn) {
+ fn.call(goog.global)
+};
+goog.provide("goog.debug.Error");
+goog.debug.Error = function(opt_msg) {
+ this.stack = (new Error).stack || "";
+ if(opt_msg) {
+ this.message = String(opt_msg)
+ }
+};
+goog.inherits(goog.debug.Error, Error);
+goog.debug.Error.prototype.name = "CustomError";
+goog.provide("goog.string");
+goog.provide("goog.string.Unicode");
+goog.string.Unicode = {NBSP:"\u00a0"};
+goog.string.startsWith = function(str, prefix) {
+ return str.lastIndexOf(prefix, 0) == 0
+};
+goog.string.endsWith = function(str, suffix) {
+ var l = str.length - suffix.length;
+ return l >= 0 && str.indexOf(suffix, l) == l
+};
+goog.string.caseInsensitiveStartsWith = function(str, prefix) {
+ return goog.string.caseInsensitiveCompare(prefix, str.substr(0, prefix.length)) == 0
+};
+goog.string.caseInsensitiveEndsWith = function(str, suffix) {
+ return goog.string.caseInsensitiveCompare(suffix, str.substr(str.length - suffix.length, suffix.length)) == 0
+};
+goog.string.subs = function(str, var_args) {
+ for(var i = 1;i < arguments.length;i++) {
+ var replacement = String(arguments[i]).replace(/\$/g, "$$$$");
+ str = str.replace(/\%s/, replacement)
+ }
+ return str
+};
+goog.string.collapseWhitespace = function(str) {
+ return str.replace(/[\s\xa0]+/g, " ").replace(/^\s+|\s+$/g, "")
+};
+goog.string.isEmpty = function(str) {
+ return/^[\s\xa0]*$/.test(str)
+};
+goog.string.isEmptySafe = function(str) {
+ return goog.string.isEmpty(goog.string.makeSafe(str))
+};
+goog.string.isBreakingWhitespace = function(str) {
+ return!/[^\t\n\r ]/.test(str)
+};
+goog.string.isAlpha = function(str) {
+ return!/[^a-zA-Z]/.test(str)
+};
+goog.string.isNumeric = function(str) {
+ return!/[^0-9]/.test(str)
+};
+goog.string.isAlphaNumeric = function(str) {
+ return!/[^a-zA-Z0-9]/.test(str)
+};
+goog.string.isSpace = function(ch) {
+ return ch == " "
+};
+goog.string.isUnicodeChar = function(ch) {
+ return ch.length == 1 && ch >= " " && ch <= "~" || ch >= "\u0080" && ch <= "\ufffd"
+};
+goog.string.stripNewlines = function(str) {
+ return str.replace(/(\r\n|\r|\n)+/g, " ")
+};
+goog.string.canonicalizeNewlines = function(str) {
+ return str.replace(/(\r\n|\r|\n)/g, "\n")
+};
+goog.string.normalizeWhitespace = function(str) {
+ return str.replace(/\xa0|\s/g, " ")
+};
+goog.string.normalizeSpaces = function(str) {
+ return str.replace(/\xa0|[ \t]+/g, " ")
+};
+goog.string.trim = function(str) {
+ return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, "")
+};
+goog.string.trimLeft = function(str) {
+ return str.replace(/^[\s\xa0]+/, "")
+};
+goog.string.trimRight = function(str) {
+ return str.replace(/[\s\xa0]+$/, "")
+};
+goog.string.caseInsensitiveCompare = function(str1, str2) {
+ var test1 = String(str1).toLowerCase();
+ var test2 = String(str2).toLowerCase();
+ if(test1 < test2) {
+ return-1
+ }else {
+ if(test1 == test2) {
+ return 0
+ }else {
+ return 1
+ }
+ }
+};
+goog.string.numerateCompareRegExp_ = /(\.\d+)|(\d+)|(\D+)/g;
+goog.string.numerateCompare = function(str1, str2) {
+ if(str1 == str2) {
+ return 0
+ }
+ if(!str1) {
+ return-1
+ }
+ if(!str2) {
+ return 1
+ }
+ var tokens1 = str1.toLowerCase().match(goog.string.numerateCompareRegExp_);
+ var tokens2 = str2.toLowerCase().match(goog.string.numerateCompareRegExp_);
+ var count = Math.min(tokens1.length, tokens2.length);
+ for(var i = 0;i < count;i++) {
+ var a = tokens1[i];
+ var b = tokens2[i];
+ if(a != b) {
+ var num1 = parseInt(a, 10);
+ if(!isNaN(num1)) {
+ var num2 = parseInt(b, 10);
+ if(!isNaN(num2) && num1 - num2) {
+ return num1 - num2
+ }
+ }
+ return a < b ? -1 : 1
+ }
+ }
+ if(tokens1.length != tokens2.length) {
+ return tokens1.length - tokens2.length
+ }
+ return str1 < str2 ? -1 : 1
+};
+goog.string.encodeUriRegExp_ = /^[a-zA-Z0-9\-_.!~*'()]*$/;
+goog.string.urlEncode = function(str) {
+ str = String(str);
+ if(!goog.string.encodeUriRegExp_.test(str)) {
+ return encodeURIComponent(str)
+ }
+ return str
+};
+goog.string.urlDecode = function(str) {
+ return decodeURIComponent(str.replace(/\+/g, " "))
+};
+goog.string.newLineToBr = function(str, opt_xml) {
+ return str.replace(/(\r\n|\r|\n)/g, opt_xml ? "<br />" : "<br>")
+};
+goog.string.htmlEscape = function(str, opt_isLikelyToContainHtmlChars) {
+ if(opt_isLikelyToContainHtmlChars) {
+ return str.replace(goog.string.amperRe_, "&amp;").replace(goog.string.ltRe_, "&lt;").replace(goog.string.gtRe_, "&gt;").replace(goog.string.quotRe_, "&quot;")
+ }else {
+ if(!goog.string.allRe_.test(str)) {
+ return str
+ }
+ if(str.includes("&")) {
+ str = str.replace(goog.string.amperRe_, "&amp;")
+ }
+ if(str.includes("<")) {
+ str = str.replace(goog.string.ltRe_, "&lt;")
+ }
+ if(str.includes(">")) {
+ str = str.replace(goog.string.gtRe_, "&gt;")
+ }
+ if(str.includes('"')) {
+ str = str.replace(goog.string.quotRe_, "&quot;")
+ }
+ return str
+ }
+};
+goog.string.amperRe_ = /&/g;
+goog.string.ltRe_ = /</g;
+goog.string.gtRe_ = />/g;
+goog.string.quotRe_ = /\"/g;
+goog.string.allRe_ = /[&<>\"]/;
+goog.string.unescapeEntities = function(str) {
+ if(goog.string.contains(str, "&")) {
+ if("document" in goog.global && !goog.string.contains(str, "<")) {
+ return goog.string.unescapeEntitiesUsingDom_(str)
+ }else {
+ return goog.string.unescapePureXmlEntities_(str)
+ }
+ }
+ return str
+};
+goog.string.unescapeEntitiesUsingDom_ = function(str) {
+ var el = goog.global["document"]["createElement"]("a");
+ el["innerHTML"] = str;
+ if(el[goog.string.NORMALIZE_FN_]) {
+ el[goog.string.NORMALIZE_FN_]()
+ }
+ str = el["firstChild"]["nodeValue"];
+ el["innerHTML"] = "";
+ return str
+};
+goog.string.unescapePureXmlEntities_ = function(str) {
+ return str.replace(/&([^;]+);/g, function(s, entity) {
+ switch(entity) {
+ case "amp":
+ return"&";
+ case "lt":
+ return"<";
+ case "gt":
+ return">";
+ case "quot":
+ return'"';
+ default:
+ if(entity.charAt(0) == "#") {
+ var n = Number("0" + entity.substr(1));
+ if(!isNaN(n)) {
+ return String.fromCharCode(n)
+ }
+ }
+ return s
+ }
+ })
+};
+goog.string.NORMALIZE_FN_ = "normalize";
+goog.string.whitespaceEscape = function(str, opt_xml) {
+ return goog.string.newLineToBr(str.replace(/ /g, " &#160;"), opt_xml)
+};
+goog.string.stripQuotes = function(str, quoteChars) {
+ var length = quoteChars.length;
+ for(var i = 0;i < length;i++) {
+ var quoteChar = length == 1 ? quoteChars : quoteChars.charAt(i);
+ if(str.charAt(0) == quoteChar && str.charAt(str.length - 1) == quoteChar) {
+ return str.substring(1, str.length - 1)
+ }
+ }
+ return str
+};
+goog.string.truncate = function(str, chars, opt_protectEscapedCharacters) {
+ if(opt_protectEscapedCharacters) {
+ str = goog.string.unescapeEntities(str)
+ }
+ if(str.length > chars) {
+ str = str.substring(0, chars - 3) + "..."
+ }
+ if(opt_protectEscapedCharacters) {
+ str = goog.string.htmlEscape(str)
+ }
+ return str
+};
+goog.string.truncateMiddle = function(str, chars, opt_protectEscapedCharacters) {
+ if(opt_protectEscapedCharacters) {
+ str = goog.string.unescapeEntities(str)
+ }
+ if(str.length > chars) {
+ var half = Math.floor(chars / 2);
+ var endPos = str.length - half;
+ half += chars % 2;
+ str = str.substring(0, half) + "..." + str.substring(endPos)
+ }
+ if(opt_protectEscapedCharacters) {
+ str = goog.string.htmlEscape(str)
+ }
+ return str
+};
+goog.string.specialEscapeChars_ = {"\u0000":"\\0", "\u0008":"\\b", "\u000c":"\\f", "\n":"\\n", "\r":"\\r", "\t":"\\t", "\u000b":"\\x0B", '"':'\\"', "\\":"\\\\"};
+goog.string.jsEscapeCache_ = {"'":"\\'"};
+goog.string.quote = function(s) {
+ s = String(s);
+ if(s.quote) {
+ return s.quote()
+ }else {
+ var sb = ['"'];
+ for(var i = 0;i < s.length;i++) {
+ var ch = s.charAt(i);
+ var cc = ch.charCodeAt(0);
+ sb[i + 1] = goog.string.specialEscapeChars_[ch] || (cc > 31 && cc < 127 ? ch : goog.string.escapeChar(ch))
+ }
+ sb.push('"');
+ return sb.join("")
+ }
+};
+goog.string.escapeString = function(str) {
+ var sb = [];
+ for(var i = 0;i < str.length;i++) {
+ sb[i] = goog.string.escapeChar(str.charAt(i))
+ }
+ return sb.join("")
+};
+goog.string.escapeChar = function(c) {
+ if(c in goog.string.jsEscapeCache_) {
+ return goog.string.jsEscapeCache_[c]
+ }
+ if(c in goog.string.specialEscapeChars_) {
+ return goog.string.jsEscapeCache_[c] = goog.string.specialEscapeChars_[c]
+ }
+ var rv = c;
+ var cc = c.charCodeAt(0);
+ if(cc > 31 && cc < 127) {
+ rv = c
+ }else {
+ if(cc < 256) {
+ rv = "\\x";
+ if(cc < 16 || cc > 256) {
+ rv += "0"
+ }
+ }else {
+ rv = "\\u";
+ if(cc < 4096) {
+ rv += "0"
+ }
+ }
+ rv += cc.toString(16).toUpperCase()
+ }
+ return goog.string.jsEscapeCache_[c] = rv
+};
+goog.string.toMap = function(s) {
+ var rv = {};
+ for(var i = 0;i < s.length;i++) {
+ rv[s.charAt(i)] = true
+ }
+ return rv
+};
+goog.string.contains = function(s, ss) {
+ return s.includes(ss)
+};
+goog.string.removeAt = function(s, index, stringLength) {
+ var resultStr = s;
+ if(index >= 0 && index < s.length && stringLength > 0) {
+ resultStr = s.substr(0, index) + s.substr(index + stringLength, s.length - index - stringLength)
+ }
+ return resultStr
+};
+goog.string.remove = function(s, ss) {
+ var re = new RegExp(goog.string.regExpEscape(ss), "");
+ return s.replace(re, "")
+};
+goog.string.removeAll = function(s, ss) {
+ var re = new RegExp(goog.string.regExpEscape(ss), "g");
+ return s.replace(re, "")
+};
+goog.string.regExpEscape = function(s) {
+ return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, "\\$1").replace(/\x08/g, "\\x08")
+};
+goog.string.repeat = function(string, length) {
+ return(new Array(length + 1)).join(string)
+};
+goog.string.padNumber = function(num, length, opt_precision) {
+ var s = goog.isDef(opt_precision) ? num.toFixed(opt_precision) : String(num);
+ var index = s.indexOf(".");
+ if(index == -1) {
+ index = s.length
+ }
+ return goog.string.repeat("0", Math.max(0, length - index)) + s
+};
+goog.string.makeSafe = function(obj) {
+ return obj == null ? "" : String(obj)
+};
+goog.string.buildString = function(var_args) {
+ return Array.prototype.join.call(arguments, "")
+};
+goog.string.getRandomString = function() {
+ return Math.floor(Math.random() * 2147483648).toString(36) + (Math.floor(Math.random() * 2147483648) ^ goog.now()).toString(36)
+};
+goog.string.compareVersions = function(version1, version2) {
+ var order = 0;
+ var v1Subs = goog.string.trim(String(version1)).split(".");
+ var v2Subs = goog.string.trim(String(version2)).split(".");
+ var subCount = Math.max(v1Subs.length, v2Subs.length);
+ for(var subIdx = 0;order == 0 && subIdx < subCount;subIdx++) {
+ var v1Sub = v1Subs[subIdx] || "";
+ var v2Sub = v2Subs[subIdx] || "";
+ var v1CompParser = new RegExp("(\\d*)(\\D*)", "g");
+ var v2CompParser = new RegExp("(\\d*)(\\D*)", "g");
+ do {
+ var v1Comp = v1CompParser.exec(v1Sub) || ["", "", ""];
+ var v2Comp = v2CompParser.exec(v2Sub) || ["", "", ""];
+ if(v1Comp[0].length == 0 && v2Comp[0].length == 0) {
+ break
+ }
+ var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10);
+ var v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10);
+ order = goog.string.compareElements_(v1CompNum, v2CompNum) || goog.string.compareElements_(v1Comp[2].length == 0, v2Comp[2].length == 0) || goog.string.compareElements_(v1Comp[2], v2Comp[2])
+ }while(order == 0)
+ }
+ return order
+};
+goog.string.compareElements_ = function(left, right) {
+ if(left < right) {
+ return-1
+ }else {
+ if(left > right) {
+ return 1
+ }
+ }
+ return 0
+};
+goog.string.HASHCODE_MAX_ = 4294967296;
+goog.string.hashCode = function(str) {
+ var result = 0;
+ for(var i = 0;i < str.length;++i) {
+ result = 31 * result + str.charCodeAt(i);
+ result %= goog.string.HASHCODE_MAX_
+ }
+ return result
+};
+goog.string.uniqueStringCounter_ = Math.random() * 2147483648 | 0;
+goog.string.createUniqueString = function() {
+ return"goog_" + goog.string.uniqueStringCounter_++
+};
+goog.string.toNumber = function(str) {
+ var num = Number(str);
+ if(num == 0 && goog.string.isEmpty(str)) {
+ return NaN
+ }
+ return num
+};
+goog.provide("goog.asserts");
+goog.provide("goog.asserts.AssertionError");
+goog.require("goog.debug.Error");
+goog.require("goog.string");
+goog.asserts.ENABLE_ASSERTS = goog.DEBUG;
+goog.asserts.AssertionError = function(messagePattern, messageArgs) {
+ messageArgs.unshift(messagePattern);
+ goog.debug.Error.call(this, goog.string.subs.apply(null, messageArgs));
+ messageArgs.shift();
+ this.messagePattern = messagePattern
+};
+goog.inherits(goog.asserts.AssertionError, goog.debug.Error);
+goog.asserts.AssertionError.prototype.name = "AssertionError";
+goog.asserts.doAssertFailure_ = function(defaultMessage, defaultArgs, givenMessage, givenArgs) {
+ var message = "Assertion failed";
+ if(givenMessage) {
+ message += ": " + givenMessage;
+ var args = givenArgs
+ }else {
+ if(defaultMessage) {
+ message += ": " + defaultMessage;
+ args = defaultArgs
+ }
+ }
+ throw new goog.asserts.AssertionError("" + message, args || []);
+};
+goog.asserts.assert = function(condition, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !condition) {
+ goog.asserts.doAssertFailure_("", null, opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return condition
+};
+goog.asserts.fail = function(opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS) {
+ throw new goog.asserts.AssertionError("Failure" + (opt_message ? ": " + opt_message : ""), Array.prototype.slice.call(arguments, 1));
+ }
+};
+goog.asserts.assertNumber = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isNumber(value)) {
+ goog.asserts.doAssertFailure_("Expected number but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertString = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isString(value)) {
+ goog.asserts.doAssertFailure_("Expected string but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertFunction = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isFunction(value)) {
+ goog.asserts.doAssertFailure_("Expected function but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertObject = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isObject(value)) {
+ goog.asserts.doAssertFailure_("Expected object but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertArray = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isArray(value)) {
+ goog.asserts.doAssertFailure_("Expected array but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertBoolean = function(value, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !goog.isBoolean(value)) {
+ goog.asserts.doAssertFailure_("Expected boolean but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2))
+ }
+ return value
+};
+goog.asserts.assertInstanceof = function(value, type, opt_message, var_args) {
+ if(goog.asserts.ENABLE_ASSERTS && !(value instanceof type)) {
+ goog.asserts.doAssertFailure_("instanceof check failed.", null, opt_message, Array.prototype.slice.call(arguments, 3))
+ }
+};
+goog.provide("goog.array");
+goog.require("goog.asserts");
+goog.array.ArrayLike;
+goog.array.peek = function(array) {
+ return array[array.length - 1]
+};
+goog.array.ARRAY_PROTOTYPE_ = Array.prototype;
+goog.array.indexOf = goog.array.ARRAY_PROTOTYPE_.indexOf ? function(arr, obj, opt_fromIndex) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.indexOf.call(arr, obj, opt_fromIndex)
+} : function(arr, obj, opt_fromIndex) {
+ var fromIndex = opt_fromIndex == null ? 0 : opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex;
+ if(goog.isString(arr)) {
+ if(!goog.isString(obj) || obj.length != 1) {
+ return-1
+ }
+ return arr.indexOf(obj, fromIndex)
+ }
+ for(var i = fromIndex;i < arr.length;i++) {
+ if(i in arr && arr[i] === obj) {
+ return i
+ }
+ }
+ return-1
+};
+goog.array.lastIndexOf = goog.array.ARRAY_PROTOTYPE_.lastIndexOf ? function(arr, obj, opt_fromIndex) {
+ goog.asserts.assert(arr.length != null);
+ var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;
+ return goog.array.ARRAY_PROTOTYPE_.lastIndexOf.call(arr, obj, fromIndex)
+} : function(arr, obj, opt_fromIndex) {
+ var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;
+ if(fromIndex < 0) {
+ fromIndex = Math.max(0, arr.length + fromIndex)
+ }
+ if(goog.isString(arr)) {
+ if(!goog.isString(obj) || obj.length != 1) {
+ return-1
+ }
+ return arr.lastIndexOf(obj, fromIndex)
+ }
+ for(var i = fromIndex;i >= 0;i--) {
+ if(i in arr && arr[i] === obj) {
+ return i
+ }
+ }
+ return-1
+};
+goog.array.forEach = goog.array.ARRAY_PROTOTYPE_.forEach ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ goog.array.ARRAY_PROTOTYPE_.forEach.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2) {
+ f.call(opt_obj, arr2[i], i, arr)
+ }
+ }
+};
+goog.array.forEachRight = function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = l - 1;i >= 0;--i) {
+ if(i in arr2) {
+ f.call(opt_obj, arr2[i], i, arr)
+ }
+ }
+};
+goog.array.filter = goog.array.ARRAY_PROTOTYPE_.filter ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.filter.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var res = [];
+ var resLength = 0;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2) {
+ var val = arr2[i];
+ if(f.call(opt_obj, val, i, arr)) {
+ res[resLength++] = val
+ }
+ }
+ }
+ return res
+};
+goog.array.map = goog.array.ARRAY_PROTOTYPE_.map ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.map.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var res = new Array(l);
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2) {
+ res[i] = f.call(opt_obj, arr2[i], i, arr)
+ }
+ }
+ return res
+};
+goog.array.reduce = function(arr, f, val, opt_obj) {
+ if(arr.reduce) {
+ if(opt_obj) {
+ return arr.reduce(goog.bind(f, opt_obj), val)
+ }else {
+ return arr.reduce(f, val)
+ }
+ }
+ var rval = val;
+ goog.array.forEach(arr, function(val, index) {
+ rval = f.call(opt_obj, rval, val, index, arr)
+ });
+ return rval
+};
+goog.array.reduceRight = function(arr, f, val, opt_obj) {
+ if(arr.reduceRight) {
+ if(opt_obj) {
+ return arr.reduceRight(goog.bind(f, opt_obj), val)
+ }else {
+ return arr.reduceRight(f, val)
+ }
+ }
+ var rval = val;
+ goog.array.forEachRight(arr, function(val, index) {
+ rval = f.call(opt_obj, rval, val, index, arr)
+ });
+ return rval
+};
+goog.array.some = goog.array.ARRAY_PROTOTYPE_.some ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.some.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) {
+ return true
+ }
+ }
+ return false
+};
+goog.array.every = goog.array.ARRAY_PROTOTYPE_.every ? function(arr, f, opt_obj) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.every.call(arr, f, opt_obj)
+} : function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2 && !f.call(opt_obj, arr2[i], i, arr)) {
+ return false
+ }
+ }
+ return true
+};
+goog.array.find = function(arr, f, opt_obj) {
+ var i = goog.array.findIndex(arr, f, opt_obj);
+ return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i]
+};
+goog.array.findIndex = function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = 0;i < l;i++) {
+ if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) {
+ return i
+ }
+ }
+ return-1
+};
+goog.array.findRight = function(arr, f, opt_obj) {
+ var i = goog.array.findIndexRight(arr, f, opt_obj);
+ return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i]
+};
+goog.array.findIndexRight = function(arr, f, opt_obj) {
+ var l = arr.length;
+ var arr2 = goog.isString(arr) ? arr.split("") : arr;
+ for(var i = l - 1;i >= 0;i--) {
+ if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) {
+ return i
+ }
+ }
+ return-1
+};
+goog.array.contains = function(arr, obj) {
+ return goog.array.includes(arr, obj)
+};
+goog.array.isEmpty = function(arr) {
+ return arr.length == 0
+};
+goog.array.clear = function(arr) {
+ if(!goog.isArray(arr)) {
+ for(var i = arr.length - 1;i >= 0;i--) {
+ delete arr[i]
+ }
+ }
+ arr.length = 0
+};
+goog.array.insert = function(arr, obj) {
+ if(!goog.array.contains(arr, obj)) {
+ arr.push(obj)
+ }
+};
+goog.array.insertAt = function(arr, obj, opt_i) {
+ goog.array.splice(arr, opt_i, 0, obj)
+};
+goog.array.insertArrayAt = function(arr, elementsToAdd, opt_i) {
+ goog.partial(goog.array.splice, arr, opt_i, 0).apply(null, elementsToAdd)
+};
+goog.array.insertBefore = function(arr, obj, opt_obj2) {
+ var i;
+ if(arguments.length == 2 || (i = goog.array.indexOf(arr, opt_obj2)) < 0) {
+ arr.push(obj)
+ }else {
+ goog.array.insertAt(arr, obj, i)
+ }
+};
+goog.array.remove = function(arr, obj) {
+ var i = goog.array.indexOf(arr, obj);
+ var rv;
+ if(rv = i >= 0) {
+ goog.array.removeAt(arr, i)
+ }
+ return rv
+};
+goog.array.removeAt = function(arr, i) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.splice.call(arr, i, 1).length == 1
+};
+goog.array.removeIf = function(arr, f, opt_obj) {
+ var i = goog.array.findIndex(arr, f, opt_obj);
+ if(i >= 0) {
+ goog.array.removeAt(arr, i);
+ return true
+ }
+ return false
+};
+goog.array.concat = function(var_args) {
+ return goog.array.ARRAY_PROTOTYPE_.concat.apply(goog.array.ARRAY_PROTOTYPE_, arguments)
+};
+goog.array.clone = function(arr) {
+ if(goog.isArray(arr)) {
+ return goog.array.concat(arr)
+ }else {
+ var rv = [];
+ for(var i = 0, len = arr.length;i < len;i++) {
+ rv[i] = arr[i]
+ }
+ return rv
+ }
+};
+goog.array.toArray = function(object) {
+ if(goog.isArray(object)) {
+ return goog.array.concat(object)
+ }
+ return goog.array.clone(object)
+};
+goog.array.extend = function(arr1, var_args) {
+ for(var i = 1;i < arguments.length;i++) {
+ var arr2 = arguments[i];
+ var isArrayLike;
+ if(goog.isArray(arr2) || (isArrayLike = goog.isArrayLike(arr2)) && arr2.hasOwnProperty("callee")) {
+ arr1.push.apply(arr1, arr2)
+ }else {
+ if(isArrayLike) {
+ var len1 = arr1.length;
+ var len2 = arr2.length;
+ for(var j = 0;j < len2;j++) {
+ arr1[len1 + j] = arr2[j]
+ }
+ }else {
+ arr1.push(arr2)
+ }
+ }
+ }
+};
+goog.array.splice = function(arr, index, howMany, var_args) {
+ goog.asserts.assert(arr.length != null);
+ return goog.array.ARRAY_PROTOTYPE_.splice.apply(arr, goog.array.slice(arguments, 1))
+};
+goog.array.slice = function(arr, start, opt_end) {
+ goog.asserts.assert(arr.length != null);
+ if(arguments.length <= 2) {
+ return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start)
+ }else {
+ return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start, opt_end)
+ }
+};
+goog.array.removeDuplicates = function(arr, opt_rv) {
+ var rv = opt_rv || arr;
+ var seen = {}, cursorInsert = 0, cursorRead = 0;
+ while(cursorRead < arr.length) {
+ var current = arr[cursorRead++];
+ var uid = goog.isObject(current) ? goog.getUid(current) : current;
+ if(!Object.prototype.hasOwnProperty.call(seen, uid)) {
+ seen[uid] = true;
+ rv[cursorInsert++] = current
+ }
+ }
+ rv.length = cursorInsert
+};
+goog.array.binarySearch = function(arr, target, opt_compareFn) {
+ return goog.array.binarySearch_(arr, opt_compareFn || goog.array.defaultCompare, false, target)
+};
+goog.array.binarySelect = function(arr, evaluator, opt_obj) {
+ return goog.array.binarySearch_(arr, evaluator, true, undefined, opt_obj)
+};
+goog.array.binarySearch_ = function(arr, compareFn, isEvaluator, opt_target, opt_selfObj) {
+ var left = 0;
+ var right = arr.length;
+ var found;
+ while(left < right) {
+ var middle = left + right >> 1;
+ var compareResult;
+ if(isEvaluator) {
+ compareResult = compareFn.call(opt_selfObj, arr[middle], middle, arr)
+ }else {
+ compareResult = compareFn(opt_target, arr[middle])
+ }
+ if(compareResult > 0) {
+ left = middle + 1
+ }else {
+ right = middle;
+ found = !compareResult
+ }
+ }
+ return found ? left : ~left
+};
+goog.array.sort = function(arr, opt_compareFn) {
+ goog.asserts.assert(arr.length != null);
+ goog.array.ARRAY_PROTOTYPE_.sort.call(arr, opt_compareFn || goog.array.defaultCompare)
+};
+goog.array.stableSort = function(arr, opt_compareFn) {
+ for(var i = 0;i < arr.length;i++) {
+ arr[i] = {index:i, value:arr[i]}
+ }
+ var valueCompareFn = opt_compareFn || goog.array.defaultCompare;
+ function stableCompareFn(obj1, obj2) {
+ return valueCompareFn(obj1.value, obj2.value) || obj1.index - obj2.index
+ }
+ goog.array.sort(arr, stableCompareFn);
+ for(var i = 0;i < arr.length;i++) {
+ arr[i] = arr[i].value
+ }
+};
+goog.array.sortObjectsByKey = function(arr, key, opt_compareFn) {
+ var compare = opt_compareFn || goog.array.defaultCompare;
+ goog.array.sort(arr, function(a, b) {
+ return compare(a[key], b[key])
+ })
+};
+goog.array.equals = function(arr1, arr2, opt_equalsFn) {
+ if(!goog.isArrayLike(arr1) || !goog.isArrayLike(arr2) || arr1.length != arr2.length) {
+ return false
+ }
+ var l = arr1.length;
+ var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality;
+ for(var i = 0;i < l;i++) {
+ if(!equalsFn(arr1[i], arr2[i])) {
+ return false
+ }
+ }
+ return true
+};
+goog.array.compare = function(arr1, arr2, opt_equalsFn) {
+ return goog.array.equals(arr1, arr2, opt_equalsFn)
+};
+goog.array.defaultCompare = function(a, b) {
+ return a > b ? 1 : a < b ? -1 : 0
+};
+goog.array.defaultCompareEquality = function(a, b) {
+ return a === b
+};
+goog.array.binaryInsert = function(array, value, opt_compareFn) {
+ var index = goog.array.binarySearch(array, value, opt_compareFn);
+ if(index < 0) {
+ goog.array.insertAt(array, value, -(index + 1));
+ return true
+ }
+ return false
+};
+goog.array.binaryRemove = function(array, value, opt_compareFn) {
+ var index = goog.array.binarySearch(array, value, opt_compareFn);
+ return index >= 0 ? goog.array.removeAt(array, index) : false
+};
+goog.array.bucket = function(array, sorter) {
+ var buckets = {};
+ for(var i = 0;i < array.length;i++) {
+ var value = array[i];
+ var key = sorter(value, i, array);
+ if(goog.isDef(key)) {
+ var bucket = buckets[key] || (buckets[key] = []);
+ bucket.push(value)
+ }
+ }
+ return buckets
+};
+goog.array.repeat = function(value, n) {
+ var array = [];
+ for(var i = 0;i < n;i++) {
+ array[i] = value
+ }
+ return array
+};
+goog.array.flatten = function(var_args) {
+ var result = [];
+ for(var i = 0;i < arguments.length;i++) {
+ var element = arguments[i];
+ if(goog.isArray(element)) {
+ result.push.apply(result, goog.array.flatten.apply(null, element))
+ }else {
+ result.push(element)
+ }
+ }
+ return result
+};
+goog.array.rotate = function(array, n) {
+ goog.asserts.assert(array.length != null);
+ if(array.length) {
+ n %= array.length;
+ if(n > 0) {
+ goog.array.ARRAY_PROTOTYPE_.unshift.apply(array, array.splice(-n, n))
+ }else {
+ if(n < 0) {
+ goog.array.ARRAY_PROTOTYPE_.push.apply(array, array.splice(0, -n))
+ }
+ }
+ }
+ return array
+};
+goog.array.zip = function(var_args) {
+ if(!arguments.length) {
+ return[]
+ }
+ var result = [];
+ for(var i = 0;true;i++) {
+ var value = [];
+ for(var j = 0;j < arguments.length;j++) {
+ var arr = arguments[j];
+ if(i >= arr.length) {
+ return result
+ }
+ value.push(arr[i])
+ }
+ result.push(value)
+ }
+};
+goog.provide("goog.userAgent");
+goog.require("goog.string");
+goog.userAgent.ASSUME_IE = false;
+goog.userAgent.ASSUME_GECKO = false;
+goog.userAgent.ASSUME_WEBKIT = false;
+goog.userAgent.ASSUME_MOBILE_WEBKIT = false;
+goog.userAgent.ASSUME_OPERA = false;
+goog.userAgent.BROWSER_KNOWN_ = goog.userAgent.ASSUME_IE || goog.userAgent.ASSUME_GECKO || goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_OPERA;
+goog.userAgent.getUserAgentString = function() {
+ return goog.global["navigator"] ? goog.global["navigator"].userAgent : null
+};
+goog.userAgent.getNavigator = function() {
+ return goog.global["navigator"]
+};
+goog.userAgent.init_ = function() {
+ goog.userAgent.detectedOpera_ = false;
+ goog.userAgent.detectedIe_ = false;
+ goog.userAgent.detectedWebkit_ = false;
+ goog.userAgent.detectedMobile_ = false;
+ goog.userAgent.detectedGecko_ = false;
+ var ua;
+ if(!goog.userAgent.BROWSER_KNOWN_ && (ua = goog.userAgent.getUserAgentString())) {
+ var navigator = goog.userAgent.getNavigator();
+ goog.userAgent.detectedOpera_ = ua.indexOf("Opera") == 0;
+ goog.userAgent.detectedIe_ = !goog.userAgent.detectedOpera_ && ua.includes("MSIE");
+ goog.userAgent.detectedWebkit_ = !goog.userAgent.detectedOpera_ && ua.includes("WebKit");
+ goog.userAgent.detectedMobile_ = goog.userAgent.detectedWebkit_ && ua.includes("Mobile");
+ goog.userAgent.detectedGecko_ = !goog.userAgent.detectedOpera_ && !goog.userAgent.detectedWebkit_ && navigator.product == "Gecko"
+ }
+};
+if(!goog.userAgent.BROWSER_KNOWN_) {
+ goog.userAgent.init_()
+}
+goog.userAgent.OPERA = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_OPERA : goog.userAgent.detectedOpera_;
+goog.userAgent.IE = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_IE : goog.userAgent.detectedIe_;
+goog.userAgent.GECKO = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_GECKO : goog.userAgent.detectedGecko_;
+goog.userAgent.WEBKIT = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_MOBILE_WEBKIT : goog.userAgent.detectedWebkit_;
+goog.userAgent.MOBILE = goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.detectedMobile_;
+goog.userAgent.SAFARI = goog.userAgent.WEBKIT;
+goog.userAgent.determinePlatform_ = function() {
+ var navigator = goog.userAgent.getNavigator();
+ return navigator && navigator.platform || ""
+};
+goog.userAgent.PLATFORM = goog.userAgent.determinePlatform_();
+goog.userAgent.ASSUME_MAC = false;
+goog.userAgent.ASSUME_WINDOWS = false;
+goog.userAgent.ASSUME_LINUX = false;
+goog.userAgent.ASSUME_X11 = false;
+goog.userAgent.PLATFORM_KNOWN_ = goog.userAgent.ASSUME_MAC || goog.userAgent.ASSUME_WINDOWS || goog.userAgent.ASSUME_LINUX || goog.userAgent.ASSUME_X11;
+goog.userAgent.initPlatform_ = function() {
+ goog.userAgent.detectedMac_ = goog.string.contains(goog.userAgent.PLATFORM, "Mac");
+ goog.userAgent.detectedWindows_ = goog.string.contains(goog.userAgent.PLATFORM, "Win");
+ goog.userAgent.detectedLinux_ = goog.string.contains(goog.userAgent.PLATFORM, "Linux");
+ goog.userAgent.detectedX11_ = !!goog.userAgent.getNavigator() && goog.string.contains(goog.userAgent.getNavigator()["appVersion"] || "", "X11")
+};
+if(!goog.userAgent.PLATFORM_KNOWN_) {
+ goog.userAgent.initPlatform_()
+}
+goog.userAgent.MAC = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_MAC : goog.userAgent.detectedMac_;
+goog.userAgent.WINDOWS = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_WINDOWS : goog.userAgent.detectedWindows_;
+goog.userAgent.LINUX = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_LINUX : goog.userAgent.detectedLinux_;
+goog.userAgent.X11 = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_X11 : goog.userAgent.detectedX11_;
+goog.userAgent.determineVersion_ = function() {
+ var version = "", re;
+ if(goog.userAgent.OPERA && goog.global["opera"]) {
+ var operaVersion = goog.global["opera"].version;
+ version = typeof operaVersion == "function" ? operaVersion() : operaVersion
+ }else {
+ if(goog.userAgent.GECKO) {
+ re = /rv\:([^\);]+)(\)|;)/
+ }else {
+ if(goog.userAgent.IE) {
+ re = /MSIE\s+([^\);]+)(\)|;)/
+ }else {
+ if(goog.userAgent.WEBKIT) {
+ re = /WebKit\/(\S+)/
+ }
+ }
+ }
+ if(re) {
+ var arr = re.exec(goog.userAgent.getUserAgentString());
+ version = arr ? arr[1] : ""
+ }
+ }
+ if(goog.userAgent.IE) {
+ var docMode = goog.userAgent.getDocumentMode_();
+ if(docMode > parseFloat(version)) {
+ return String(docMode)
+ }
+ }
+ return version
+};
+goog.userAgent.getDocumentMode_ = function() {
+ var doc = goog.global["document"];
+ return doc ? doc["documentMode"] : undefined
+};
+goog.userAgent.VERSION = goog.userAgent.determineVersion_();
+goog.userAgent.compare = function(v1, v2) {
+ return goog.string.compareVersions(v1, v2)
+};
+goog.userAgent.isVersionCache_ = {};
+goog.userAgent.isVersion = function(version) {
+ return goog.userAgent.isVersionCache_[version] || (goog.userAgent.isVersionCache_[version] = goog.string.compareVersions(goog.userAgent.VERSION, version) >= 0)
+};
+goog.provide("goog.dom.BrowserFeature");
+goog.require("goog.userAgent");
+goog.dom.BrowserFeature = {
+ CAN_ADD_NAME_OR_TYPE_ATTRIBUTES: !goog.userAgent.IE || goog.userAgent.isVersion("9"),
+ CAN_USE_INNER_TEXT: goog.userAgent.IE && !goog.userAgent.isVersion("9"),
+ INNER_HTML_NEEDS_SCOPED_ELEMENT: goog.userAgent.IE
+};
+goog.provide("goog.dom.TagName");
+goog.dom.TagName = {A:"A", ABBR:"ABBR", ACRONYM:"ACRONYM", ADDRESS:"ADDRESS", APPLET:"APPLET", AREA:"AREA", B:"B", BASE:"BASE", BASEFONT:"BASEFONT", BDO:"BDO", BIG:"BIG", BLOCKQUOTE:"BLOCKQUOTE", BODY:"BODY", BR:"BR", BUTTON:"BUTTON", CANVAS:"CANVAS", CAPTION:"CAPTION", CENTER:"CENTER", CITE:"CITE", CODE:"CODE", COL:"COL", COLGROUP:"COLGROUP", DD:"DD", DEL:"DEL", DFN:"DFN", DIR:"DIR", DIV:"DIV", DL:"DL", DT:"DT", EM:"EM", FIELDSET:"FIELDSET", FONT:"FONT", FORM:"FORM", FRAME:"FRAME", FRAMESET:"FRAMESET",
+H1:"H1", H2:"H2", H3:"H3", H4:"H4", H5:"H5", H6:"H6", HEAD:"HEAD", HR:"HR", HTML:"HTML", I:"I", IFRAME:"IFRAME", IMG:"IMG", INPUT:"INPUT", INS:"INS", ISINDEX:"ISINDEX", KBD:"KBD", LABEL:"LABEL", LEGEND:"LEGEND", LI:"LI", LINK:"LINK", MAP:"MAP", MENU:"MENU", META:"META", NOFRAMES:"NOFRAMES", NOSCRIPT:"NOSCRIPT", OBJECT:"OBJECT", OL:"OL", OPTGROUP:"OPTGROUP", OPTION:"OPTION", P:"P", PARAM:"PARAM", PRE:"PRE", Q:"Q", S:"S", SAMP:"SAMP", SCRIPT:"SCRIPT", SELECT:"SELECT", SMALL:"SMALL", SPAN:"SPAN", STRIKE:"STRIKE",
+STRONG:"STRONG", STYLE:"STYLE", SUB:"SUB", SUP:"SUP", TABLE:"TABLE", TBODY:"TBODY", TD:"TD", TEXTAREA:"TEXTAREA", TFOOT:"TFOOT", TH:"TH", THEAD:"THEAD", TITLE:"TITLE", TR:"TR", TT:"TT", U:"U", UL:"UL", VAR:"VAR"};
+goog.provide("goog.dom.classes");
+goog.require("goog.array");
+goog.dom.classes.set = function(element, className) {
+ element.className = className
+};
+goog.dom.classes.get = function(element) {
+ var className = element.className;
+ return className && typeof className.split == "function" ? className.split(/\s+/) : []
+};
+goog.dom.classes.add = function(element, var_args) {
+ var classes = goog.dom.classes.get(element);
+ var args = goog.array.slice(arguments, 1);
+ var b = goog.dom.classes.add_(classes, args);
+ element.className = classes.join(" ");
+ return b
+};
+goog.dom.classes.remove = function(element, var_args) {
+ var classes = goog.dom.classes.get(element);
+ var args = goog.array.slice(arguments, 1);
+ var b = goog.dom.classes.remove_(classes, args);
+ element.className = classes.join(" ");
+ return b
+};
+goog.dom.classes.add_ = function(classes, args) {
+ var rv = 0;
+ for(var i = 0;i < args.length;i++) {
+ if(!goog.array.contains(classes, args[i])) {
+ classes.push(args[i]);
+ rv++
+ }
+ }
+ return rv == args.length
+};
+goog.dom.classes.remove_ = function(classes, args) {
+ var rv = 0;
+ for(var i = 0;i < classes.length;i++) {
+ if(goog.array.contains(args, classes[i])) {
+ goog.array.splice(classes, i--, 1);
+ rv++
+ }
+ }
+ return rv == args.length
+};
+goog.dom.classes.swap = function(element, fromClass, toClass) {
+ var classes = goog.dom.classes.get(element);
+ var removed = false;
+ for(var i = 0;i < classes.length;i++) {
+ if(classes[i] == fromClass) {
+ goog.array.splice(classes, i--, 1);
+ removed = true
+ }
+ }
+ if(removed) {
+ classes.push(toClass);
+ element.className = classes.join(" ")
+ }
+ return removed
+};
+goog.dom.classes.addRemove = function(element, classesToRemove, classesToAdd) {
+ var classes = goog.dom.classes.get(element);
+ if(goog.isString(classesToRemove)) {
+ goog.array.remove(classes, classesToRemove)
+ }else {
+ if(goog.isArray(classesToRemove)) {
+ goog.dom.classes.remove_(classes, classesToRemove)
+ }
+ }
+ if(goog.isString(classesToAdd) && !goog.array.contains(classes, classesToAdd)) {
+ classes.push(classesToAdd)
+ }else {
+ if(goog.isArray(classesToAdd)) {
+ goog.dom.classes.add_(classes, classesToAdd)
+ }
+ }
+ element.className = classes.join(" ")
+};
+goog.dom.classes.has = function(element, className) {
+ return goog.array.contains(goog.dom.classes.get(element), className)
+};
+goog.dom.classes.enable = function(element, className, enabled) {
+ if(enabled) {
+ goog.dom.classes.add(element, className)
+ }else {
+ goog.dom.classes.remove(element, className)
+ }
+};
+goog.dom.classes.toggle = function(element, className) {
+ var add = !goog.dom.classes.has(element, className);
+ goog.dom.classes.enable(element, className, add);
+ return add
+};
+goog.provide("goog.math.Coordinate");
+goog.math.Coordinate = function(opt_x, opt_y) {
+ this.x = goog.isDef(opt_x) ? opt_x : 0;
+ this.y = goog.isDef(opt_y) ? opt_y : 0
+};
+goog.math.Coordinate.prototype.clone = function() {
+ return new goog.math.Coordinate(this.x, this.y)
+};
+if(goog.DEBUG) {
+ goog.math.Coordinate.prototype.toString = function() {
+ return"(" + this.x + ", " + this.y + ")"
+ }
+}
+goog.math.Coordinate.equals = function(a, b) {
+ if(a == b) {
+ return true
+ }
+ if(!a || !b) {
+ return false
+ }
+ return a.x == b.x && a.y == b.y
+};
+goog.math.Coordinate.distance = function(a, b) {
+ var dx = a.x - b.x;
+ var dy = a.y - b.y;
+ return Math.sqrt(dx * dx + dy * dy)
+};
+goog.math.Coordinate.squaredDistance = function(a, b) {
+ var dx = a.x - b.x;
+ var dy = a.y - b.y;
+ return dx * dx + dy * dy
+};
+goog.math.Coordinate.difference = function(a, b) {
+ return new goog.math.Coordinate(a.x - b.x, a.y - b.y)
+};
+goog.math.Coordinate.sum = function(a, b) {
+ return new goog.math.Coordinate(a.x + b.x, a.y + b.y)
+};
+goog.provide("goog.math.Size");
+goog.math.Size = function(width, height) {
+ this.width = width;
+ this.height = height
+};
+goog.math.Size.equals = function(a, b) {
+ if(a == b) {
+ return true
+ }
+ if(!a || !b) {
+ return false
+ }
+ return a.width == b.width && a.height == b.height
+};
+goog.math.Size.prototype.clone = function() {
+ return new goog.math.Size(this.width, this.height)
+};
+if(goog.DEBUG) {
+ goog.math.Size.prototype.toString = function() {
+ return"(" + this.width + " x " + this.height + ")"
+ }
+}
+goog.math.Size.prototype.getLongest = function() {
+ return Math.max(this.width, this.height)
+};
+goog.math.Size.prototype.getShortest = function() {
+ return Math.min(this.width, this.height)
+};
+goog.math.Size.prototype.area = function() {
+ return this.width * this.height
+};
+goog.math.Size.prototype.perimeter = function() {
+ return(this.width + this.height) * 2
+};
+goog.math.Size.prototype.aspectRatio = function() {
+ return this.width / this.height
+};
+goog.math.Size.prototype.isEmpty = function() {
+ return!this.area()
+};
+goog.math.Size.prototype.ceil = function() {
+ this.width = Math.ceil(this.width);
+ this.height = Math.ceil(this.height);
+ return this
+};
+goog.math.Size.prototype.fitsInside = function(target) {
+ return this.width <= target.width && this.height <= target.height
+};
+goog.math.Size.prototype.floor = function() {
+ this.width = Math.floor(this.width);
+ this.height = Math.floor(this.height);
+ return this
+};
+goog.math.Size.prototype.round = function() {
+ this.width = Math.round(this.width);
+ this.height = Math.round(this.height);
+ return this
+};
+goog.math.Size.prototype.scale = function(s) {
+ this.width *= s;
+ this.height *= s;
+ return this
+};
+goog.math.Size.prototype.scaleToFit = function(target) {
+ var s = this.aspectRatio() > target.aspectRatio() ? target.width / this.width : target.height / this.height;
+ return this.scale(s)
+};
+goog.provide("goog.object");
+goog.object.forEach = function(obj, f, opt_obj) {
+ for(var key in obj) {
+ f.call(opt_obj, obj[key], key, obj)
+ }
+};
+goog.object.filter = function(obj, f, opt_obj) {
+ var res = {};
+ for(var key in obj) {
+ if(f.call(opt_obj, obj[key], key, obj)) {
+ res[key] = obj[key]
+ }
+ }
+ return res
+};
+goog.object.map = function(obj, f, opt_obj) {
+ var res = {};
+ for(var key in obj) {
+ res[key] = f.call(opt_obj, obj[key], key, obj)
+ }
+ return res
+};
+goog.object.some = function(obj, f, opt_obj) {
+ for(var key in obj) {
+ if(f.call(opt_obj, obj[key], key, obj)) {
+ return true
+ }
+ }
+ return false
+};
+goog.object.every = function(obj, f, opt_obj) {
+ for(var key in obj) {
+ if(!f.call(opt_obj, obj[key], key, obj)) {
+ return false
+ }
+ }
+ return true
+};
+goog.object.getCount = function(obj) {
+ var rv = 0;
+ for(var key in obj) {
+ rv++
+ }
+ return rv
+};
+goog.object.getAnyKey = function(obj) {
+ for(var key in obj) {
+ return key
+ }
+};
+goog.object.getAnyValue = function(obj) {
+ for(var key in obj) {
+ return obj[key]
+ }
+};
+goog.object.contains = function(obj, val) {
+ return goog.object.containsValue(obj, val)
+};
+goog.object.getValues = function(obj) {
+ var res = [];
+ var i = 0;
+ for(var key in obj) {
+ res[i++] = obj[key]
+ }
+ return res
+};
+goog.object.getKeys = function(obj) {
+ var res = [];
+ var i = 0;
+ for(var key in obj) {
+ res[i++] = key
+ }
+ return res
+};
+goog.object.containsKey = function(obj, key) {
+ return key in obj
+};
+goog.object.containsValue = function(obj, val) {
+ for(var key in obj) {
+ if(obj[key] == val) {
+ return true
+ }
+ }
+ return false
+};
+goog.object.findKey = function(obj, f, opt_this) {
+ for(var key in obj) {
+ if(f.call(opt_this, obj[key], key, obj)) {
+ return key
+ }
+ }
+ return undefined
+};
+goog.object.findValue = function(obj, f, opt_this) {
+ var key = goog.object.findKey(obj, f, opt_this);
+ return key && obj[key]
+};
+goog.object.isEmpty = function(obj) {
+ for(var key in obj) {
+ return false
+ }
+ return true
+};
+goog.object.clear = function(obj) {
+ var keys = goog.object.getKeys(obj);
+ for(var i = keys.length - 1;i >= 0;i--) {
+ goog.object.remove(obj, keys[i])
+ }
+};
+goog.object.remove = function(obj, key) {
+ var rv;
+ if(rv = key in obj) {
+ delete obj[key]
+ }
+ return rv
+};
+goog.object.add = function(obj, key, val) {
+ if(key in obj) {
+ throw Error('The object already contains the key "' + key + '"');
+ }
+ goog.object.set(obj, key, val)
+};
+goog.object.get = function(obj, key, opt_val) {
+ if(key in obj) {
+ return obj[key]
+ }
+ return opt_val
+};
+goog.object.set = function(obj, key, value) {
+ obj[key] = value
+};
+goog.object.setIfUndefined = function(obj, key, value) {
+ return key in obj ? obj[key] : obj[key] = value
+};
+goog.object.clone = function(obj) {
+ var res = {};
+ for(var key in obj) {
+ res[key] = obj[key]
+ }
+ return res
+};
+goog.object.transpose = function(obj) {
+ var transposed = {};
+ for(var key in obj) {
+ transposed[obj[key]] = key
+ }
+ return transposed
+};
+goog.object.PROTOTYPE_FIELDS_ = ["constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf"];
+goog.object.extend = function(target, var_args) {
+ var key, source;
+ for(var i = 1;i < arguments.length;i++) {
+ source = arguments[i];
+ for(key in source) {
+ target[key] = source[key]
+ }
+ for(var j = 0;j < goog.object.PROTOTYPE_FIELDS_.length;j++) {
+ key = goog.object.PROTOTYPE_FIELDS_[j];
+ if(Object.prototype.hasOwnProperty.call(source, key)) {
+ target[key] = source[key]
+ }
+ }
+ }
+};
+goog.object.create = function(var_args) {
+ var argLength = arguments.length;
+ if(argLength == 1 && goog.isArray(arguments[0])) {
+ return goog.object.create.apply(null, arguments[0])
+ }
+ if(argLength % 2) {
+ throw Error("Uneven number of arguments");
+ }
+ var rv = {};
+ for(var i = 0;i < argLength;i += 2) {
+ rv[arguments[i]] = arguments[i + 1]
+ }
+ return rv
+};
+goog.object.createSet = function(var_args) {
+ var argLength = arguments.length;
+ if(argLength == 1 && goog.isArray(arguments[0])) {
+ return goog.object.createSet.apply(null, arguments[0])
+ }
+ var rv = {};
+ for(var i = 0;i < argLength;i++) {
+ rv[arguments[i]] = true
+ }
+ return rv
+};
+goog.provide("goog.dom");
+goog.provide("goog.dom.DomHelper");
+goog.provide("goog.dom.NodeType");
+goog.require("goog.array");
+goog.require("goog.dom.BrowserFeature");
+goog.require("goog.dom.TagName");
+goog.require("goog.dom.classes");
+goog.require("goog.math.Coordinate");
+goog.require("goog.math.Size");
+goog.require("goog.object");
+goog.require("goog.string");
+goog.require("goog.userAgent");
+goog.dom.ASSUME_QUIRKS_MODE = false;
+goog.dom.ASSUME_STANDARDS_MODE = false;
+goog.dom.COMPAT_MODE_KNOWN_ = goog.dom.ASSUME_QUIRKS_MODE || goog.dom.ASSUME_STANDARDS_MODE;
+goog.dom.NodeType = {ELEMENT:1, ATTRIBUTE:2, TEXT:3, CDATA_SECTION:4, ENTITY_REFERENCE:5, ENTITY:6, PROCESSING_INSTRUCTION:7, COMMENT:8, DOCUMENT:9, DOCUMENT_TYPE:10, DOCUMENT_FRAGMENT:11, NOTATION:12};
+goog.dom.getDomHelper = function(opt_element) {
+ return opt_element ? new goog.dom.DomHelper(goog.dom.getOwnerDocument(opt_element)) : goog.dom.defaultDomHelper_ || (goog.dom.defaultDomHelper_ = new goog.dom.DomHelper)
+};
+goog.dom.defaultDomHelper_;
+goog.dom.getDocument = function() {
+ return document
+};
+goog.dom.getElement = function(element) {
+ return goog.isString(element) ? document.getElementById(element) : element
+};
+goog.dom.$ = goog.dom.getElement;
+goog.dom.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) {
+ return goog.dom.getElementsByTagNameAndClass_(document, opt_tag, opt_class, opt_el)
+};
+goog.dom.getElementsByClass = function(className, opt_el) {
+ var parent = opt_el || document;
+ if(goog.dom.canUseQuerySelector_(parent)) {
+ return parent.querySelectorAll("." + className)
+ }else {
+ if(parent.getElementsByClassName) {
+ return parent.getElementsByClassName(className)
+ }
+ }
+ return goog.dom.getElementsByTagNameAndClass_(document, "*", className, opt_el)
+};
+goog.dom.getElementByClass = function(className, opt_el) {
+ var parent = opt_el || document;
+ var retVal = null;
+ if(goog.dom.canUseQuerySelector_(parent)) {
+ retVal = parent.querySelector("." + className)
+ }else {
+ retVal = goog.dom.getElementsByClass(className, opt_el)[0]
+ }
+ return retVal || null
+};
+goog.dom.canUseQuerySelector_ = function(parent) {
+ return parent.querySelectorAll && parent.querySelector && (!goog.userAgent.WEBKIT || goog.dom.isCss1CompatMode_(document) || goog.userAgent.isVersion("528"))
+};
+goog.dom.getElementsByTagNameAndClass_ = function(doc, opt_tag, opt_class, opt_el) {
+ var parent = opt_el || doc;
+ var tagName = opt_tag && opt_tag != "*" ? opt_tag.toUpperCase() : "";
+ if(goog.dom.canUseQuerySelector_(parent) && (tagName || opt_class)) {
+ var query = tagName + (opt_class ? "." + opt_class : "");
+ return parent.querySelectorAll(query)
+ }
+ if(opt_class && parent.getElementsByClassName) {
+ var els = parent.getElementsByClassName(opt_class);
+ if(tagName) {
+ var arrayLike = {};
+ var len = 0;
+ for(var i = 0, el;el = els[i];i++) {
+ if(tagName == el.nodeName) {
+ arrayLike[len++] = el
+ }
+ }
+ arrayLike.length = len;
+ return arrayLike
+ }else {
+ return els
+ }
+ }
+ var els = parent.getElementsByTagName(tagName || "*");
+ if(opt_class) {
+ var arrayLike = {};
+ var len = 0;
+ for(var i = 0, el;el = els[i];i++) {
+ var className = el.className;
+ if(typeof className.split == "function" && goog.array.contains(className.split(/\s+/), opt_class)) {
+ arrayLike[len++] = el
+ }
+ }
+ arrayLike.length = len;
+ return arrayLike
+ }else {
+ return els
+ }
+};
+goog.dom.$$ = goog.dom.getElementsByTagNameAndClass;
+goog.dom.setProperties = function(element, properties) {
+ goog.object.forEach(properties, function(val, key) {
+ if(key == "style") {
+ element.style.cssText = val
+ }else {
+ if(key == "class") {
+ element.className = val
+ }else {
+ if(key == "for") {
+ element.htmlFor = val
+ }else {
+ if(key in goog.dom.DIRECT_ATTRIBUTE_MAP_) {
+ element.setAttribute(goog.dom.DIRECT_ATTRIBUTE_MAP_[key], val)
+ }else {
+ element[key] = val
+ }
+ }
+ }
+ }
+ })
+};
+goog.dom.DIRECT_ATTRIBUTE_MAP_ = {cellpadding:"cellPadding", cellspacing:"cellSpacing", colspan:"colSpan", rowspan:"rowSpan", valign:"vAlign", height:"height", width:"width", usemap:"useMap", frameborder:"frameBorder", type:"type"};
+goog.dom.getViewportSize = function(opt_window) {
+ return goog.dom.getViewportSize_(opt_window || window)
+};
+goog.dom.getViewportSize_ = function(win) {
+ var doc = win.document;
+ if(goog.userAgent.WEBKIT && !goog.userAgent.isVersion("500") && !goog.userAgent.MOBILE) {
+ if(typeof win.innerHeight == "undefined") {
+ win = window
+ }
+ var innerHeight = win.innerHeight;
+ var scrollHeight = win.document.documentElement.scrollHeight;
+ if(win == win.top) {
+ if(scrollHeight < innerHeight) {
+ innerHeight -= 15
+ }
+ }
+ return new goog.math.Size(win.innerWidth, innerHeight)
+ }
+ var readsFromDocumentElement = goog.dom.isCss1CompatMode_(doc);
+ if(goog.userAgent.OPERA && !goog.userAgent.isVersion("9.50")) {
+ readsFromDocumentElement = false
+ }
+ var el = readsFromDocumentElement ? doc.documentElement : doc.body;
+ return new goog.math.Size(el.clientWidth, el.clientHeight)
+};
+goog.dom.getDocumentHeight = function() {
+ return goog.dom.getDocumentHeight_(window)
+};
+goog.dom.getDocumentHeight_ = function(win) {
+ var doc = win.document;
+ var height = 0;
+ if(doc) {
+ var vh = goog.dom.getViewportSize_(win).height;
+ var body = doc.body;
+ var docEl = doc.documentElement;
+ if(goog.dom.isCss1CompatMode_(doc) && docEl.scrollHeight) {
+ height = docEl.scrollHeight != vh ? docEl.scrollHeight : docEl.offsetHeight
+ }else {
+ var sh = docEl.scrollHeight;
+ var oh = docEl.offsetHeight;
+ if(docEl.clientHeight != oh) {
+ sh = body.scrollHeight;
+ oh = body.offsetHeight
+ }
+ if(sh > vh) {
+ height = sh > oh ? sh : oh
+ }else {
+ height = sh < oh ? sh : oh
+ }
+ }
+ }
+ return height
+};
+goog.dom.getPageScroll = function(opt_window) {
+ var win = opt_window || goog.global || window;
+ return goog.dom.getDomHelper(win.document).getDocumentScroll()
+};
+goog.dom.getDocumentScroll = function() {
+ return goog.dom.getDocumentScroll_(document)
+};
+goog.dom.getDocumentScroll_ = function(doc) {
+ var el = goog.dom.getDocumentScrollElement_(doc);
+ return new goog.math.Coordinate(el.scrollLeft, el.scrollTop)
+};
+goog.dom.getDocumentScrollElement = function() {
+ return goog.dom.getDocumentScrollElement_(document)
+};
+goog.dom.getDocumentScrollElement_ = function(doc) {
+ return!goog.userAgent.WEBKIT && goog.dom.isCss1CompatMode_(doc) ? doc.documentElement : doc.body
+};
+goog.dom.getWindow = function(opt_doc) {
+ return opt_doc ? goog.dom.getWindow_(opt_doc) : window
+};
+goog.dom.getWindow_ = function(doc) {
+ return doc.parentWindow || doc.defaultView
+};
+goog.dom.createDom = function(tagName, opt_attributes, var_args) {
+ return goog.dom.createDom_(document, arguments)
+};
+goog.dom.createDom_ = function(doc, args) {
+ var tagName = args[0];
+ var attributes = args[1];
+ if(!goog.dom.BrowserFeature.CAN_ADD_NAME_OR_TYPE_ATTRIBUTES && attributes && (attributes.name || attributes.type)) {
+ var tagNameArr = ["<", tagName];
+ if(attributes.name) {
+ tagNameArr.push(' name="', goog.string.htmlEscape(attributes.name), '"')
+ }
+ if(attributes.type) {
+ tagNameArr.push(' type="', goog.string.htmlEscape(attributes.type), '"');
+ var clone = {};
+ goog.object.extend(clone, attributes);
+ attributes = clone;
+ delete attributes.type
+ }
+ tagNameArr.push(">");
+ tagName = tagNameArr.join("")
+ }
+ var element = doc.createElement(tagName);
+ if(attributes) {
+ if(goog.isString(attributes)) {
+ element.className = attributes
+ }else {
+ if(goog.isArray(attributes)) {
+ goog.dom.classes.add.apply(null, [element].concat(attributes))
+ }else {
+ goog.dom.setProperties(element, attributes)
+ }
+ }
+ }
+ if(args.length > 2) {
+ goog.dom.append_(doc, element, args, 2)
+ }
+ return element
+};
+goog.dom.append_ = function(doc, parent, args, startIndex) {
+ function childHandler(child) {
+ if(child) {
+ parent.appendChild(goog.isString(child) ? doc.createTextNode(child) : child)
+ }
+ }
+ for(var i = startIndex;i < args.length;i++) {
+ var arg = args[i];
+ if(goog.isArrayLike(arg) && !goog.dom.isNodeLike(arg)) {
+ goog.array.forEach(goog.dom.isNodeList(arg) ? goog.array.clone(arg) : arg, childHandler)
+ }else {
+ childHandler(arg)
+ }
+ }
+};
+goog.dom.$dom = goog.dom.createDom;
+goog.dom.createElement = function(name) {
+ return document.createElement(name)
+};
+goog.dom.createTextNode = function(content) {
+ return document.createTextNode(content)
+};
+goog.dom.createTable = function(rows, columns, opt_fillWithNbsp) {
+ return goog.dom.createTable_(document, rows, columns, !!opt_fillWithNbsp)
+};
+goog.dom.createTable_ = function(doc, rows, columns, fillWithNbsp) {
+ var rowHtml = ["<tr>"];
+ for(var i = 0;i < columns;i++) {
+ rowHtml.push(fillWithNbsp ? "<td>&nbsp;</td>" : "<td></td>")
+ }
+ rowHtml.push("</tr>");
+ rowHtml = rowHtml.join("");
+ var totalHtml = ["<table>"];
+ for(i = 0;i < rows;i++) {
+ totalHtml.push(rowHtml)
+ }
+ totalHtml.push("</table>");
+ var elem = doc.createElement(goog.dom.TagName.DIV);
+ elem.innerHTML = totalHtml.join("");
+ return elem.firstChild.remove()
+};
+goog.dom.htmlToDocumentFragment = function(htmlString) {
+ return goog.dom.htmlToDocumentFragment_(document, htmlString)
+};
+goog.dom.htmlToDocumentFragment_ = function(doc, htmlString) {
+ var tempDiv = doc.createElement("div");
+ if(goog.dom.BrowserFeature.INNER_HTML_NEEDS_SCOPED_ELEMENT) {
+ tempDiv.innerHTML = "<br>" + htmlString;
+ tempDiv.firstChild.remove()
+ }else {
+ tempDiv.innerHTML = htmlString
+ }
+ if(tempDiv.childNodes.length == 1) {
+ return tempDiv.firstChild.remove()
+ }else {
+ var fragment = doc.createDocumentFragment();
+ while(tempDiv.firstChild) {
+ fragment.appendChild(tempDiv.firstChild)
+ }
+ return fragment
+ }
+};
+goog.dom.getCompatMode = function() {
+ return goog.dom.isCss1CompatMode() ? "CSS1Compat" : "BackCompat"
+};
+goog.dom.isCss1CompatMode = function() {
+ return goog.dom.isCss1CompatMode_(document)
+};
+goog.dom.isCss1CompatMode_ = function(doc) {
+ if(goog.dom.COMPAT_MODE_KNOWN_) {
+ return goog.dom.ASSUME_STANDARDS_MODE
+ }
+ return doc.compatMode == "CSS1Compat"
+};
+goog.dom.canHaveChildren = function(node) {
+ if(node.nodeType != goog.dom.NodeType.ELEMENT) {
+ return false
+ }
+ switch(node.tagName) {
+ case goog.dom.TagName.APPLET:
+ ;
+ case goog.dom.TagName.AREA:
+ ;
+ case goog.dom.TagName.BASE:
+ ;
+ case goog.dom.TagName.BR:
+ ;
+ case goog.dom.TagName.COL:
+ ;
+ case goog.dom.TagName.FRAME:
+ ;
+ case goog.dom.TagName.HR:
+ ;
+ case goog.dom.TagName.IMG:
+ ;
+ case goog.dom.TagName.INPUT:
+ ;
+ case goog.dom.TagName.IFRAME:
+ ;
+ case goog.dom.TagName.ISINDEX:
+ ;
+ case goog.dom.TagName.LINK:
+ ;
+ case goog.dom.TagName.NOFRAMES:
+ ;
+ case goog.dom.TagName.NOSCRIPT:
+ ;
+ case goog.dom.TagName.META:
+ ;
+ case goog.dom.TagName.OBJECT:
+ ;
+ case goog.dom.TagName.PARAM:
+ ;
+ case goog.dom.TagName.SCRIPT:
+ ;
+ case goog.dom.TagName.STYLE:
+ return false
+ }
+ return true
+};
+goog.dom.appendChild = function(parent, child) {
+ parent.appendChild(child)
+};
+goog.dom.append = function(parent, var_args) {
+ goog.dom.append_(goog.dom.getOwnerDocument(parent), parent, arguments, 1)
+};
+goog.dom.removeChildren = function(node) {
+ var child;
+ while(child = node.firstChild) {
+ node.removeChild(child)
+ }
+};
+goog.dom.insertSiblingBefore = function(newNode, refNode) {
+ if(refNode.parentNode) {
+ refNode.parentNode.insertBefore(newNode, refNode)
+ }
+};
+goog.dom.insertSiblingAfter = function(newNode, refNode) {
+ if(refNode.parentNode) {
+ refNode.parentNode.insertBefore(newNode, refNode.nextSibling)
+ }
+};
+goog.dom.removeNode = function(node) {
+ return node && node.parentNode ? node.remove() : null
+};
+goog.dom.replaceNode = function(newNode, oldNode) {
+ var parent = oldNode.parentNode;
+ if(parent) {
+ parent.replaceChild(newNode, oldNode)
+ }
+};
+goog.dom.flattenElement = function(element) {
+ var child, parent = element.parentNode;
+ if(parent && parent.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) {
+ if(element.removeNode) {
+ return element.removeNode(false)
+ }else {
+ while(child = element.firstChild) {
+ parent.insertBefore(child, element)
+ }
+ return goog.dom.removeNode(element)
+ }
+ }
+};
+goog.dom.getFirstElementChild = function(node) {
+ return goog.dom.getNextElementNode_(node.firstChild, true)
+};
+goog.dom.getLastElementChild = function(node) {
+ return goog.dom.getNextElementNode_(node.lastChild, false)
+};
+goog.dom.getNextElementSibling = function(node) {
+ return goog.dom.getNextElementNode_(node.nextSibling, true)
+};
+goog.dom.getPreviousElementSibling = function(node) {
+ return goog.dom.getNextElementNode_(node.previousSibling, false)
+};
+goog.dom.getNextElementNode_ = function(node, forward) {
+ while(node && node.nodeType != goog.dom.NodeType.ELEMENT) {
+ node = forward ? node.nextSibling : node.previousSibling
+ }
+ return node
+};
+goog.dom.getNextNode = function(node) {
+ if(!node) {
+ return null
+ }
+ if(node.firstChild) {
+ return node.firstChild
+ }
+ while(node && !node.nextSibling) {
+ node = node.parentNode
+ }
+ return node ? node.nextSibling : null
+};
+goog.dom.getPreviousNode = function(node) {
+ if(!node) {
+ return null
+ }
+ if(!node.previousSibling) {
+ return node.parentNode
+ }
+ node = node.previousSibling;
+ while(node && node.lastChild) {
+ node = node.lastChild
+ }
+ return node
+};
+goog.dom.isNodeLike = function(obj) {
+ return goog.isObject(obj) && obj.nodeType > 0
+};
+goog.dom.contains = function(parent, descendant) {
+ if(parent.contains && descendant.nodeType == goog.dom.NodeType.ELEMENT) {
+ return parent == descendant || parent.contains(descendant)
+ }
+ if(typeof parent.compareDocumentPosition != "undefined") {
+ return parent == descendant || Boolean(parent.compareDocumentPosition(descendant) & 16)
+ }
+ while(descendant && parent != descendant) {
+ descendant = descendant.parentNode
+ }
+ return descendant == parent
+};
+goog.dom.compareNodeOrder = function(node1, node2) {
+ if(node1 == node2) {
+ return 0
+ }
+ if(node1.compareDocumentPosition) {
+ return node1.compareDocumentPosition(node2) & 2 ? 1 : -1
+ }
+ if("sourceIndex" in node1 || node1.parentNode && "sourceIndex" in node1.parentNode) {
+ var isElement1 = node1.nodeType == goog.dom.NodeType.ELEMENT;
+ var isElement2 = node2.nodeType == goog.dom.NodeType.ELEMENT;
+ if(isElement1 && isElement2) {
+ return node1.sourceIndex - node2.sourceIndex
+ }else {
+ var parent1 = node1.parentNode;
+ var parent2 = node2.parentNode;
+ if(parent1 == parent2) {
+ return goog.dom.compareSiblingOrder_(node1, node2)
+ }
+ if(!isElement1 && goog.dom.contains(parent1, node2)) {
+ return-1 * goog.dom.compareParentsDescendantNodeIe_(node1, node2)
+ }
+ if(!isElement2 && goog.dom.contains(parent2, node1)) {
+ return goog.dom.compareParentsDescendantNodeIe_(node2, node1)
+ }
+ return(isElement1 ? node1.sourceIndex : parent1.sourceIndex) - (isElement2 ? node2.sourceIndex : parent2.sourceIndex)
+ }
+ }
+ var doc = goog.dom.getOwnerDocument(node1);
+ var range1, range2;
+ range1 = doc.createRange();
+ range1.selectNode(node1);
+ range1.collapse(true);
+ range2 = doc.createRange();
+ range2.selectNode(node2);
+ range2.collapse(true);
+ return range1.compareBoundaryPoints(goog.global["Range"].START_TO_END, range2)
+};
+goog.dom.compareParentsDescendantNodeIe_ = function(textNode, node) {
+ var parent = textNode.parentNode;
+ if(parent == node) {
+ return-1
+ }
+ var sibling = node;
+ while(sibling.parentNode != parent) {
+ sibling = sibling.parentNode
+ }
+ return goog.dom.compareSiblingOrder_(sibling, textNode)
+};
+goog.dom.compareSiblingOrder_ = function(node1, node2) {
+ var s = node2;
+ while(s = s.previousSibling) {
+ if(s == node1) {
+ return-1
+ }
+ }
+ return 1
+};
+goog.dom.findCommonAncestor = function(var_args) {
+ var i, count = arguments.length;
+ if(!count) {
+ return null
+ }else {
+ if(count == 1) {
+ return arguments[0]
+ }
+ }
+ var paths = [];
+ var minLength = Infinity;
+ for(i = 0;i < count;i++) {
+ var ancestors = [];
+ var node = arguments[i];
+ while(node) {
+ ancestors.unshift(node);
+ node = node.parentNode
+ }
+ paths.push(ancestors);
+ minLength = Math.min(minLength, ancestors.length)
+ }
+ var output = null;
+ for(i = 0;i < minLength;i++) {
+ var first = paths[0][i];
+ for(var j = 1;j < count;j++) {
+ if(first != paths[j][i]) {
+ return output
+ }
+ }
+ output = first
+ }
+ return output
+};
+goog.dom.getOwnerDocument = function(node) {
+ return node.nodeType == goog.dom.NodeType.DOCUMENT ? node : node.ownerDocument || node.document
+};
+goog.dom.getFrameContentDocument = function(frame) {
+ var doc;
+ if(goog.userAgent.WEBKIT) {
+ doc = frame.document || frame.contentWindow.document
+ }else {
+ doc = frame.contentDocument || frame.contentWindow.document
+ }
+ return doc
+};
+goog.dom.getFrameContentWindow = function(frame) {
+ return frame.contentWindow || goog.dom.getWindow_(goog.dom.getFrameContentDocument(frame))
+};
+goog.dom.setTextContent = function(element, text) {
+ if("textContent" in element) {
+ element.textContent = text
+ }else {
+ if(element.firstChild && element.firstChild.nodeType == goog.dom.NodeType.TEXT) {
+ while(element.lastChild != element.firstChild) {
+ element.removeChild(element.lastChild)
+ }
+ element.firstChild.data = text
+ }else {
+ goog.dom.removeChildren(element);
+ var doc = goog.dom.getOwnerDocument(element);
+ element.appendChild(doc.createTextNode(text))
+ }
+ }
+};
+goog.dom.getOuterHtml = function(element) {
+ if("outerHTML" in element) {
+ return element.outerHTML
+ }else {
+ var doc = goog.dom.getOwnerDocument(element);
+ var div = doc.createElement("div");
+ div.appendChild(element.cloneNode(true));
+ return div.innerHTML
+ }
+};
+goog.dom.findNode = function(root, p) {
+ var rv = [];
+ var found = goog.dom.findNodes_(root, p, rv, true);
+ return found ? rv[0] : undefined
+};
+goog.dom.findNodes = function(root, p) {
+ var rv = [];
+ goog.dom.findNodes_(root, p, rv, false);
+ return rv
+};
+goog.dom.findNodes_ = function(root, p, rv, findOne) {
+ if(root != null) {
+ for(var i = 0, child;child = root.childNodes[i];i++) {
+ if(p(child)) {
+ rv.push(child);
+ if(findOne) {
+ return true
+ }
+ }
+ if(goog.dom.findNodes_(child, p, rv, findOne)) {
+ return true
+ }
+ }
+ }
+ return false
+};
+goog.dom.TAGS_TO_IGNORE_ = {SCRIPT:1, STYLE:1, HEAD:1, IFRAME:1, OBJECT:1};
+goog.dom.PREDEFINED_TAG_VALUES_ = {IMG:" ", BR:"\n"};
+goog.dom.isFocusableTabIndex = function(element) {
+ var attrNode = element.getAttributeNode("tabindex");
+ if(attrNode && attrNode.specified) {
+ var index = element.tabIndex;
+ return goog.isNumber(index) && index >= 0
+ }
+ return false
+};
+goog.dom.setFocusableTabIndex = function(element, enable) {
+ if(enable) {
+ element.tabIndex = 0
+ }else {
+ element.removeAttribute("tabIndex")
+ }
+};
+goog.dom.getTextContent = function(node) {
+ var textContent;
+ if(goog.dom.BrowserFeature.CAN_USE_INNER_TEXT && "innerText" in node) {
+ textContent = goog.string.canonicalizeNewlines(node.innerText)
+ }else {
+ var buf = [];
+ goog.dom.getTextContent_(node, buf, true);
+ textContent = buf.join("")
+ }
+ textContent = textContent.replace(/ \xAD /g, " ").replace(/\xAD/g, "");
+ if(!goog.userAgent.IE) {
+ textContent = textContent.replace(/ +/g, " ")
+ }
+ if(textContent != " ") {
+ textContent = textContent.replace(/^\s*/, "")
+ }
+ return textContent
+};
+goog.dom.getRawTextContent = function(node) {
+ var buf = [];
+ goog.dom.getTextContent_(node, buf, false);
+ return buf.join("")
+};
+goog.dom.getTextContent_ = function(node, buf, normalizeWhitespace) {
+ if(node.nodeName in goog.dom.TAGS_TO_IGNORE_) {
+ }else {
+ if(node.nodeType == goog.dom.NodeType.TEXT) {
+ if(normalizeWhitespace) {
+ buf.push(String(node.nodeValue).replace(/(\r\n|\r|\n)/g, ""))
+ }else {
+ buf.push(node.nodeValue)
+ }
+ }else {
+ if(node.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) {
+ buf.push(goog.dom.PREDEFINED_TAG_VALUES_[node.nodeName])
+ }else {
+ var child = node.firstChild;
+ while(child) {
+ goog.dom.getTextContent_(child, buf, normalizeWhitespace);
+ child = child.nextSibling
+ }
+ }
+ }
+ }
+};
+goog.dom.getNodeTextLength = function(node) {
+ return goog.dom.getTextContent(node).length
+};
+goog.dom.getNodeTextOffset = function(node, opt_offsetParent) {
+ var root = opt_offsetParent || goog.dom.getOwnerDocument(node).body;
+ var buf = [];
+ while(node && node != root) {
+ var cur = node;
+ while(cur = cur.previousSibling) {
+ buf.unshift(goog.dom.getTextContent(cur))
+ }
+ node = node.parentNode
+ }
+ return goog.string.trimLeft(buf.join("")).replace(/ +/g, " ").length
+};
+goog.dom.getNodeAtOffset = function(parent, offset, opt_result) {
+ var stack = [parent], pos = 0, cur;
+ while(stack.length > 0 && pos < offset) {
+ cur = stack.pop();
+ if(cur.nodeName in goog.dom.TAGS_TO_IGNORE_) {
+ }else {
+ if(cur.nodeType == goog.dom.NodeType.TEXT) {
+ var text = cur.nodeValue.replace(/(\r\n|\r|\n)/g, "").replace(/ +/g, " ");
+ pos += text.length
+ }else {
+ if(cur.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) {
+ pos += goog.dom.PREDEFINED_TAG_VALUES_[cur.nodeName].length
+ }else {
+ for(var i = cur.childNodes.length - 1;i >= 0;i--) {
+ stack.push(cur.childNodes[i])
+ }
+ }
+ }
+ }
+ }
+ if(goog.isObject(opt_result)) {
+ opt_result.remainder = cur ? cur.nodeValue.length + offset - pos - 1 : 0;
+ opt_result.node = cur
+ }
+ return cur
+};
+goog.dom.isNodeList = function(val) {
+ if(val && typeof val.length == "number") {
+ if(goog.isObject(val)) {
+ return typeof val.item == "function" || typeof val.item == "string"
+ }else {
+ if(goog.isFunction(val)) {
+ return typeof val.item == "function"
+ }
+ }
+ }
+ return false
+};
+goog.dom.getAncestorByTagNameAndClass = function(element, opt_tag, opt_class) {
+ var tagName = opt_tag ? opt_tag.toUpperCase() : null;
+ return goog.dom.getAncestor(element, function(node) {
+ return(!tagName || node.nodeName == tagName) && (!opt_class || goog.dom.classes.has(node, opt_class))
+ }, true)
+};
+goog.dom.getAncestor = function(element, matcher, opt_includeNode, opt_maxSearchSteps) {
+ if(!opt_includeNode) {
+ element = element.parentNode
+ }
+ var ignoreSearchSteps = opt_maxSearchSteps == null;
+ var steps = 0;
+ while(element && (ignoreSearchSteps || steps <= opt_maxSearchSteps)) {
+ if(matcher(element)) {
+ return element
+ }
+ element = element.parentNode;
+ steps++
+ }
+ return null
+};
+goog.dom.DomHelper = function(opt_document) {
+ this.document_ = opt_document || goog.global.document || document
+};
+goog.dom.DomHelper.prototype.getDomHelper = goog.dom.getDomHelper;
+goog.dom.DomHelper.prototype.setDocument = function(document) {
+ this.document_ = document
+};
+goog.dom.DomHelper.prototype.getDocument = function() {
+ return this.document_
+};
+goog.dom.DomHelper.prototype.getElement = function(element) {
+ if(goog.isString(element)) {
+ return this.document_.getElementById(element)
+ }else {
+ return element
+ }
+};
+goog.dom.DomHelper.prototype.$ = goog.dom.DomHelper.prototype.getElement;
+goog.dom.DomHelper.prototype.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) {
+ return goog.dom.getElementsByTagNameAndClass_(this.document_, opt_tag, opt_class, opt_el)
+};
+goog.dom.DomHelper.prototype.getElementsByClass = function(className, opt_el) {
+ var doc = opt_el || this.document_;
+ return goog.dom.getElementsByClass(className, doc)
+};
+goog.dom.DomHelper.prototype.getElementByClass = function(className, opt_el) {
+ var doc = opt_el || this.document_;
+ return goog.dom.getElementByClass(className, doc)
+};
+goog.dom.DomHelper.prototype.$$ = goog.dom.DomHelper.prototype.getElementsByTagNameAndClass;
+goog.dom.DomHelper.prototype.setProperties = goog.dom.setProperties;
+goog.dom.DomHelper.prototype.getViewportSize = function(opt_window) {
+ return goog.dom.getViewportSize(opt_window || this.getWindow())
+};
+goog.dom.DomHelper.prototype.getDocumentHeight = function() {
+ return goog.dom.getDocumentHeight_(this.getWindow())
+};
+goog.dom.Appendable;
+goog.dom.DomHelper.prototype.createDom = function(tagName, opt_attributes, var_args) {
+ return goog.dom.createDom_(this.document_, arguments)
+};
+goog.dom.DomHelper.prototype.$dom = goog.dom.DomHelper.prototype.createDom;
+goog.dom.DomHelper.prototype.createElement = function(name) {
+ return this.document_.createElement(name)
+};
+goog.dom.DomHelper.prototype.createTextNode = function(content) {
+ return this.document_.createTextNode(content)
+};
+goog.dom.DomHelper.prototype.createTable = function(rows, columns, opt_fillWithNbsp) {
+ return goog.dom.createTable_(this.document_, rows, columns, !!opt_fillWithNbsp)
+};
+goog.dom.DomHelper.prototype.htmlToDocumentFragment = function(htmlString) {
+ return goog.dom.htmlToDocumentFragment_(this.document_, htmlString)
+};
+goog.dom.DomHelper.prototype.getCompatMode = function() {
+ return this.isCss1CompatMode() ? "CSS1Compat" : "BackCompat"
+};
+goog.dom.DomHelper.prototype.isCss1CompatMode = function() {
+ return goog.dom.isCss1CompatMode_(this.document_)
+};
+goog.dom.DomHelper.prototype.getWindow = function() {
+ return goog.dom.getWindow_(this.document_)
+};
+goog.dom.DomHelper.prototype.getDocumentScrollElement = function() {
+ return goog.dom.getDocumentScrollElement_(this.document_)
+};
+goog.dom.DomHelper.prototype.getDocumentScroll = function() {
+ return goog.dom.getDocumentScroll_(this.document_)
+};
+goog.dom.DomHelper.prototype.appendChild = goog.dom.appendChild;
+goog.dom.DomHelper.prototype.append = goog.dom.append;
+goog.dom.DomHelper.prototype.removeChildren = goog.dom.removeChildren;
+goog.dom.DomHelper.prototype.insertSiblingBefore = goog.dom.insertSiblingBefore;
+goog.dom.DomHelper.prototype.insertSiblingAfter = goog.dom.insertSiblingAfter;
+goog.dom.DomHelper.prototype.removeNode = goog.dom.removeNode;
+goog.dom.DomHelper.prototype.replaceNode = goog.dom.replaceNode;
+goog.dom.DomHelper.prototype.flattenElement = goog.dom.flattenElement;
+goog.dom.DomHelper.prototype.getFirstElementChild = goog.dom.getFirstElementChild;
+goog.dom.DomHelper.prototype.getLastElementChild = goog.dom.getLastElementChild;
+goog.dom.DomHelper.prototype.getNextElementSibling = goog.dom.getNextElementSibling;
+goog.dom.DomHelper.prototype.getPreviousElementSibling = goog.dom.getPreviousElementSibling;
+goog.dom.DomHelper.prototype.getNextNode = goog.dom.getNextNode;
+goog.dom.DomHelper.prototype.getPreviousNode = goog.dom.getPreviousNode;
+goog.dom.DomHelper.prototype.isNodeLike = goog.dom.isNodeLike;
+goog.dom.DomHelper.prototype.contains = goog.dom.contains;
+goog.dom.DomHelper.prototype.getOwnerDocument = goog.dom.getOwnerDocument;
+goog.dom.DomHelper.prototype.getFrameContentDocument = goog.dom.getFrameContentDocument;
+goog.dom.DomHelper.prototype.getFrameContentWindow = goog.dom.getFrameContentWindow;
+goog.dom.DomHelper.prototype.setTextContent = goog.dom.setTextContent;
+goog.dom.DomHelper.prototype.findNode = goog.dom.findNode;
+goog.dom.DomHelper.prototype.findNodes = goog.dom.findNodes;
+goog.dom.DomHelper.prototype.getTextContent = goog.dom.getTextContent;
+goog.dom.DomHelper.prototype.getNodeTextLength = goog.dom.getNodeTextLength;
+goog.dom.DomHelper.prototype.getNodeTextOffset = goog.dom.getNodeTextOffset;
+goog.dom.DomHelper.prototype.getAncestorByTagNameAndClass = goog.dom.getAncestorByTagNameAndClass;
+goog.dom.DomHelper.prototype.getAncestor = goog.dom.getAncestor;
+goog.provide("goog.Disposable");
+goog.provide("goog.dispose");
+goog.Disposable = function() {
+};
+goog.Disposable.prototype.disposed_ = false;
+goog.Disposable.prototype.isDisposed = function() {
+ return this.disposed_
+};
+goog.Disposable.prototype.getDisposed = goog.Disposable.prototype.isDisposed;
+goog.Disposable.prototype.dispose = function() {
+ if(!this.disposed_) {
+ this.disposed_ = true;
+ this.disposeInternal()
+ }
+};
+goog.Disposable.prototype.disposeInternal = function() {
+};
+goog.dispose = function(obj) {
+ if(obj && typeof obj.dispose == "function") {
+ obj.dispose()
+ }
+};
+goog.provide("goog.structs");
+goog.require("goog.array");
+goog.require("goog.object");
+goog.structs.getCount = function(col) {
+ if(typeof col.getCount == "function") {
+ return col.getCount()
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return col.length
+ }
+ return goog.object.getCount(col)
+};
+goog.structs.getValues = function(col) {
+ if(typeof col.getValues == "function") {
+ return col.getValues()
+ }
+ if(goog.isString(col)) {
+ return col.split("")
+ }
+ if(goog.isArrayLike(col)) {
+ var rv = [];
+ var l = col.length;
+ for(var i = 0;i < l;i++) {
+ rv.push(col[i])
+ }
+ return rv
+ }
+ return goog.object.getValues(col)
+};
+goog.structs.getKeys = function(col) {
+ if(typeof col.getKeys == "function") {
+ return col.getKeys()
+ }
+ if(typeof col.getValues == "function") {
+ return undefined
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ var rv = [];
+ var l = col.length;
+ for(var i = 0;i < l;i++) {
+ rv.push(i)
+ }
+ return rv
+ }
+ return goog.object.getKeys(col)
+};
+goog.structs.contains = function(col, val) {
+ if(typeof col.contains == "function") {
+ return col.contains(val)
+ }
+ if(typeof col.containsValue == "function") {
+ return col.containsValue(val)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.contains(col, val)
+ }
+ return goog.object.containsValue(col, val)
+};
+goog.structs.isEmpty = function(col) {
+ if(typeof col.isEmpty == "function") {
+ return col.isEmpty()
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.isEmpty(col)
+ }
+ return goog.object.isEmpty(col)
+};
+goog.structs.clear = function(col) {
+ if(typeof col.clear == "function") {
+ col.clear()
+ }else {
+ if(goog.isArrayLike(col)) {
+ goog.array.clear(col)
+ }else {
+ goog.object.clear(col)
+ }
+ }
+};
+goog.structs.forEach = function(col, f, opt_obj) {
+ if(typeof col.forEach == "function") {
+ col.forEach(f, opt_obj)
+ }else {
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ goog.array.forEach(col, f, opt_obj)
+ }else {
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ f.call(opt_obj, values[i], keys && keys[i], col)
+ }
+ }
+ }
+};
+goog.structs.filter = function(col, f, opt_obj) {
+ if(typeof col.filter == "function") {
+ return col.filter(f, opt_obj)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.filter(col, f, opt_obj)
+ }
+ var rv;
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ if(keys) {
+ rv = {};
+ for(var i = 0;i < l;i++) {
+ if(f.call(opt_obj, values[i], keys[i], col)) {
+ rv[keys[i]] = values[i]
+ }
+ }
+ }else {
+ rv = [];
+ for(var i = 0;i < l;i++) {
+ if(f.call(opt_obj, values[i], undefined, col)) {
+ rv.push(values[i])
+ }
+ }
+ }
+ return rv
+};
+goog.structs.map = function(col, f, opt_obj) {
+ if(typeof col.map == "function") {
+ return col.map(f, opt_obj)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.map(col, f, opt_obj)
+ }
+ var rv;
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ if(keys) {
+ rv = {};
+ for(var i = 0;i < l;i++) {
+ rv[keys[i]] = f.call(opt_obj, values[i], keys[i], col)
+ }
+ }else {
+ rv = [];
+ for(var i = 0;i < l;i++) {
+ rv[i] = f.call(opt_obj, values[i], undefined, col)
+ }
+ }
+ return rv
+};
+goog.structs.some = function(col, f, opt_obj) {
+ if(typeof col.some == "function") {
+ return col.some(f, opt_obj)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.some(col, f, opt_obj)
+ }
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ if(f.call(opt_obj, values[i], keys && keys[i], col)) {
+ return true
+ }
+ }
+ return false
+};
+goog.structs.every = function(col, f, opt_obj) {
+ if(typeof col.every == "function") {
+ return col.every(f, opt_obj)
+ }
+ if(goog.isArrayLike(col) || goog.isString(col)) {
+ return goog.array.every(col, f, opt_obj)
+ }
+ var keys = goog.structs.getKeys(col);
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ if(!f.call(opt_obj, values[i], keys && keys[i], col)) {
+ return false
+ }
+ }
+ return true
+};
+goog.provide("goog.iter");
+goog.provide("goog.iter.Iterator");
+goog.provide("goog.iter.StopIteration");
+goog.require("goog.array");
+goog.iter.Iterable;
+if("StopIteration" in goog.global) {
+ goog.iter.StopIteration = goog.global["StopIteration"]
+}else {
+ goog.iter.StopIteration = Error("StopIteration")
+}
+goog.iter.Iterator = function() {
+};
+goog.iter.Iterator.prototype.next = function() {
+ throw goog.iter.StopIteration;
+};
+goog.iter.Iterator.prototype.__iterator__ = function(opt_keys) {
+ return this
+};
+goog.iter.toIterator = function(iterable) {
+ if(iterable instanceof goog.iter.Iterator) {
+ return iterable
+ }
+ if(typeof iterable.__iterator__ == "function") {
+ return iterable.__iterator__(false)
+ }
+ if(goog.isArrayLike(iterable)) {
+ var i = 0;
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ while(true) {
+ if(i >= iterable.length) {
+ throw goog.iter.StopIteration;
+ }
+ if(!(i in iterable)) {
+ i++;
+ continue
+ }
+ return iterable[i++]
+ }
+ };
+ return newIter
+ }
+ throw Error("Not implemented");
+};
+goog.iter.forEach = function(iterable, f, opt_obj) {
+ if(goog.isArrayLike(iterable)) {
+ try {
+ goog.array.forEach(iterable, f, opt_obj)
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }
+ }
+ }else {
+ iterable = goog.iter.toIterator(iterable);
+ try {
+ while(true) {
+ f.call(opt_obj, iterable.next(), undefined, iterable)
+ }
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }
+ }
+ }
+};
+goog.iter.filter = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ while(true) {
+ var val = iterable.next();
+ if(f.call(opt_obj, val, undefined, iterable)) {
+ return val
+ }
+ }
+ };
+ return newIter
+};
+goog.iter.range = function(startOrStop, opt_stop, opt_step) {
+ var start = 0;
+ var stop = startOrStop;
+ var step = opt_step || 1;
+ if(arguments.length > 1) {
+ start = startOrStop;
+ stop = opt_stop
+ }
+ if(step == 0) {
+ throw Error("Range step argument must not be zero");
+ }
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ if(step > 0 && start >= stop || step < 0 && start <= stop) {
+ throw goog.iter.StopIteration;
+ }
+ var rv = start;
+ start += step;
+ return rv
+ };
+ return newIter
+};
+goog.iter.join = function(iterable, deliminator) {
+ return goog.iter.toArray(iterable).join(deliminator)
+};
+goog.iter.map = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ while(true) {
+ var val = iterable.next();
+ return f.call(opt_obj, val, undefined, iterable)
+ }
+ };
+ return newIter
+};
+goog.iter.reduce = function(iterable, f, val, opt_obj) {
+ var rval = val;
+ goog.iter.forEach(iterable, function(val) {
+ rval = f.call(opt_obj, rval, val)
+ });
+ return rval
+};
+goog.iter.some = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ try {
+ while(true) {
+ if(f.call(opt_obj, iterable.next(), undefined, iterable)) {
+ return true
+ }
+ }
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }
+ }
+ return false
+};
+goog.iter.every = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ try {
+ while(true) {
+ if(!f.call(opt_obj, iterable.next(), undefined, iterable)) {
+ return false
+ }
+ }
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }
+ }
+ return true
+};
+goog.iter.chain = function(var_args) {
+ var args = arguments;
+ var length = args.length;
+ var i = 0;
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ try {
+ if(i >= length) {
+ throw goog.iter.StopIteration;
+ }
+ var current = goog.iter.toIterator(args[i]);
+ return current.next()
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration || i >= length) {
+ throw ex;
+ }else {
+ i++;
+ return this.next()
+ }
+ }
+ };
+ return newIter
+};
+goog.iter.dropWhile = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ var newIter = new goog.iter.Iterator;
+ var dropping = true;
+ newIter.next = function() {
+ while(true) {
+ var val = iterable.next();
+ if(dropping && f.call(opt_obj, val, undefined, iterable)) {
+ continue
+ }else {
+ dropping = false
+ }
+ return val
+ }
+ };
+ return newIter
+};
+goog.iter.takeWhile = function(iterable, f, opt_obj) {
+ iterable = goog.iter.toIterator(iterable);
+ var newIter = new goog.iter.Iterator;
+ var taking = true;
+ newIter.next = function() {
+ while(true) {
+ if(taking) {
+ var val = iterable.next();
+ if(f.call(opt_obj, val, undefined, iterable)) {
+ return val
+ }else {
+ taking = false
+ }
+ }else {
+ throw goog.iter.StopIteration;
+ }
+ }
+ };
+ return newIter
+};
+goog.iter.toArray = function(iterable) {
+ if(goog.isArrayLike(iterable)) {
+ return goog.array.toArray(iterable)
+ }
+ iterable = goog.iter.toIterator(iterable);
+ var array = [];
+ goog.iter.forEach(iterable, function(val) {
+ array.push(val)
+ });
+ return array
+};
+goog.iter.equals = function(iterable1, iterable2) {
+ iterable1 = goog.iter.toIterator(iterable1);
+ iterable2 = goog.iter.toIterator(iterable2);
+ var b1, b2;
+ try {
+ while(true) {
+ b1 = b2 = false;
+ var val1 = iterable1.next();
+ b1 = true;
+ var val2 = iterable2.next();
+ b2 = true;
+ if(val1 != val2) {
+ return false
+ }
+ }
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration) {
+ throw ex;
+ }else {
+ if(b1 && !b2) {
+ return false
+ }
+ if(!b2) {
+ try {
+ val2 = iterable2.next();
+ return false
+ }catch(ex1) {
+ if(ex1 !== goog.iter.StopIteration) {
+ throw ex1;
+ }
+ return true
+ }
+ }
+ }
+ }
+ return false
+};
+goog.iter.nextOrValue = function(iterable, defaultValue) {
+ try {
+ return goog.iter.toIterator(iterable).next()
+ }catch(e) {
+ if(e != goog.iter.StopIteration) {
+ throw e;
+ }
+ return defaultValue
+ }
+};
+goog.provide("goog.structs.Map");
+goog.require("goog.iter.Iterator");
+goog.require("goog.iter.StopIteration");
+goog.require("goog.object");
+goog.require("goog.structs");
+goog.structs.Map = function(opt_map, var_args) {
+ this.map_ = {};
+ this.keys_ = [];
+ var argLength = arguments.length;
+ if(argLength > 1) {
+ if(argLength % 2) {
+ throw Error("Uneven number of arguments");
+ }
+ for(var i = 0;i < argLength;i += 2) {
+ this.set(arguments[i], arguments[i + 1])
+ }
+ }else {
+ if(opt_map) {
+ this.addAll(opt_map)
+ }
+ }
+};
+goog.structs.Map.prototype.count_ = 0;
+goog.structs.Map.prototype.version_ = 0;
+goog.structs.Map.prototype.getCount = function() {
+ return this.count_
+};
+goog.structs.Map.prototype.getValues = function() {
+ this.cleanupKeysArray_();
+ var rv = [];
+ for(var i = 0;i < this.keys_.length;i++) {
+ var key = this.keys_[i];
+ rv.push(this.map_[key])
+ }
+ return rv
+};
+goog.structs.Map.prototype.getKeys = function() {
+ this.cleanupKeysArray_();
+ return this.keys_.concat()
+};
+goog.structs.Map.prototype.containsKey = function(key) {
+ return goog.structs.Map.hasKey_(this.map_, key)
+};
+goog.structs.Map.prototype.containsValue = function(val) {
+ for(var i = 0;i < this.keys_.length;i++) {
+ var key = this.keys_[i];
+ if(goog.structs.Map.hasKey_(this.map_, key) && this.map_[key] == val) {
+ return true
+ }
+ }
+ return false
+};
+goog.structs.Map.prototype.equals = function(otherMap, opt_equalityFn) {
+ if(this === otherMap) {
+ return true
+ }
+ if(this.count_ != otherMap.getCount()) {
+ return false
+ }
+ var equalityFn = opt_equalityFn || goog.structs.Map.defaultEquals;
+ this.cleanupKeysArray_();
+ for(var key, i = 0;key = this.keys_[i];i++) {
+ if(!equalityFn(this.get(key), otherMap.get(key))) {
+ return false
+ }
+ }
+ return true
+};
+goog.structs.Map.defaultEquals = function(a, b) {
+ return a === b
+};
+goog.structs.Map.prototype.isEmpty = function() {
+ return this.count_ == 0
+};
+goog.structs.Map.prototype.clear = function() {
+ this.map_ = {};
+ this.keys_.length = 0;
+ this.count_ = 0;
+ this.version_ = 0
+};
+goog.structs.Map.prototype.remove = function(key) {
+ if(goog.structs.Map.hasKey_(this.map_, key)) {
+ delete this.map_[key];
+ this.count_--;
+ this.version_++;
+ if(this.keys_.length > 2 * this.count_) {
+ this.cleanupKeysArray_()
+ }
+ return true
+ }
+ return false
+};
+goog.structs.Map.prototype.cleanupKeysArray_ = function() {
+ if(this.count_ != this.keys_.length) {
+ var srcIndex = 0;
+ var destIndex = 0;
+ while(srcIndex < this.keys_.length) {
+ var key = this.keys_[srcIndex];
+ if(goog.structs.Map.hasKey_(this.map_, key)) {
+ this.keys_[destIndex++] = key
+ }
+ srcIndex++
+ }
+ this.keys_.length = destIndex
+ }
+ if(this.count_ != this.keys_.length) {
+ var seen = {};
+ var srcIndex = 0;
+ var destIndex = 0;
+ while(srcIndex < this.keys_.length) {
+ var key = this.keys_[srcIndex];
+ if(!goog.structs.Map.hasKey_(seen, key)) {
+ this.keys_[destIndex++] = key;
+ seen[key] = 1
+ }
+ srcIndex++
+ }
+ this.keys_.length = destIndex
+ }
+};
+goog.structs.Map.prototype.get = function(key, opt_val) {
+ if(goog.structs.Map.hasKey_(this.map_, key)) {
+ return this.map_[key]
+ }
+ return opt_val
+};
+goog.structs.Map.prototype.set = function(key, value) {
+ if(!goog.structs.Map.hasKey_(this.map_, key)) {
+ this.count_++;
+ this.keys_.push(key);
+ this.version_++
+ }
+ this.map_[key] = value
+};
+goog.structs.Map.prototype.addAll = function(map) {
+ var keys, values;
+ if(map instanceof goog.structs.Map) {
+ keys = map.getKeys();
+ values = map.getValues()
+ }else {
+ keys = goog.object.getKeys(map);
+ values = goog.object.getValues(map)
+ }
+ for(var i = 0;i < keys.length;i++) {
+ this.set(keys[i], values[i])
+ }
+};
+goog.structs.Map.prototype.clone = function() {
+ return new goog.structs.Map(this)
+};
+goog.structs.Map.prototype.transpose = function() {
+ var transposed = new goog.structs.Map;
+ for(var i = 0;i < this.keys_.length;i++) {
+ var key = this.keys_[i];
+ var value = this.map_[key];
+ transposed.set(value, key)
+ }
+ return transposed
+};
+goog.structs.Map.prototype.toObject = function() {
+ this.cleanupKeysArray_();
+ var obj = {};
+ for(var i = 0;i < this.keys_.length;i++) {
+ var key = this.keys_[i];
+ obj[key] = this.map_[key]
+ }
+ return obj
+};
+goog.structs.Map.prototype.getKeyIterator = function() {
+ return this.__iterator__(true)
+};
+goog.structs.Map.prototype.getValueIterator = function() {
+ return this.__iterator__(false)
+};
+goog.structs.Map.prototype.__iterator__ = function(opt_keys) {
+ this.cleanupKeysArray_();
+ var i = 0;
+ var keys = this.keys_;
+ var map = this.map_;
+ var version = this.version_;
+ var selfObj = this;
+ var newIter = new goog.iter.Iterator;
+ newIter.next = function() {
+ while(true) {
+ if(version != selfObj.version_) {
+ throw Error("The map has changed since the iterator was created");
+ }
+ if(i >= keys.length) {
+ throw goog.iter.StopIteration;
+ }
+ var key = keys[i++];
+ return opt_keys ? key : map[key]
+ }
+ };
+ return newIter
+};
+goog.structs.Map.hasKey_ = function(obj, key) {
+ return Object.prototype.hasOwnProperty.call(obj, key)
+};
+goog.provide("goog.structs.Set");
+goog.require("goog.structs");
+goog.require("goog.structs.Map");
+goog.structs.Set = function(opt_values) {
+ this.map_ = new goog.structs.Map;
+ if(opt_values) {
+ this.addAll(opt_values)
+ }
+};
+goog.structs.Set.getKey_ = function(val) {
+ var type = typeof val;
+ if(type == "object" && val || type == "function") {
+ return"o" + goog.getUid(val)
+ }else {
+ return type.substr(0, 1) + val
+ }
+};
+goog.structs.Set.prototype.getCount = function() {
+ return this.map_.getCount()
+};
+goog.structs.Set.prototype.add = function(element) {
+ this.map_.set(goog.structs.Set.getKey_(element), element)
+};
+goog.structs.Set.prototype.addAll = function(col) {
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ this.add(values[i])
+ }
+};
+goog.structs.Set.prototype.removeAll = function(col) {
+ var values = goog.structs.getValues(col);
+ var l = values.length;
+ for(var i = 0;i < l;i++) {
+ this.remove(values[i])
+ }
+};
+goog.structs.Set.prototype.remove = function(element) {
+ return this.map_.remove(goog.structs.Set.getKey_(element))
+};
+goog.structs.Set.prototype.clear = function() {
+ this.map_.clear()
+};
+goog.structs.Set.prototype.isEmpty = function() {
+ return this.map_.isEmpty()
+};
+goog.structs.Set.prototype.contains = function(element) {
+ return this.map_.containsKey(goog.structs.Set.getKey_(element))
+};
+goog.structs.Set.prototype.containsAll = function(col) {
+ return goog.structs.every(col, this.contains, this)
+};
+goog.structs.Set.prototype.intersection = function(col) {
+ var result = new goog.structs.Set;
+ var values = goog.structs.getValues(col);
+ for(var i = 0;i < values.length;i++) {
+ var value = values[i];
+ if(this.contains(value)) {
+ result.add(value)
+ }
+ }
+ return result
+};
+goog.structs.Set.prototype.getValues = function() {
+ return this.map_.getValues()
+};
+goog.structs.Set.prototype.clone = function() {
+ return new goog.structs.Set(this)
+};
+goog.structs.Set.prototype.equals = function(col) {
+ return this.getCount() == goog.structs.getCount(col) && this.isSubsetOf(col)
+};
+goog.structs.Set.prototype.isSubsetOf = function(col) {
+ var colCount = goog.structs.getCount(col);
+ if(this.getCount() > colCount) {
+ return false
+ }
+ if(!(col instanceof goog.structs.Set) && colCount > 5) {
+ col = new goog.structs.Set(col)
+ }
+ return goog.structs.every(this, function(value) {
+ return goog.structs.contains(col, value)
+ })
+};
+goog.structs.Set.prototype.__iterator__ = function(opt_keys) {
+ return this.map_.__iterator__(false)
+};
+goog.provide("goog.debug");
+goog.require("goog.array");
+goog.require("goog.string");
+goog.require("goog.structs.Set");
+goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) {
+ var target = opt_target || goog.global;
+ var oldErrorHandler = target.onerror;
+ target.onerror = function(message, url, line) {
+ if(oldErrorHandler) {
+ oldErrorHandler(message, url, line)
+ }
+ logFunc({message:message, fileName:url, line:line});
+ return Boolean(opt_cancel)
+ }
+};
+goog.debug.expose = function(obj, opt_showFn) {
+ if(typeof obj == "undefined") {
+ return"undefined"
+ }
+ if(obj == null) {
+ return"NULL"
+ }
+ var str = [];
+ for(var x in obj) {
+ if(!opt_showFn && goog.isFunction(obj[x])) {
+ continue
+ }
+ var s = x + " = ";
+ try {
+ s += obj[x]
+ }catch(e) {
+ s += "*** " + e + " ***"
+ }
+ str.push(s)
+ }
+ return str.join("\n")
+};
+goog.debug.deepExpose = function(obj, opt_showFn) {
+ var previous = new goog.structs.Set;
+ var str = [];
+ var helper = function(obj, space) {
+ var nestspace = space + " ";
+ var indentMultiline = function(str) {
+ return str.replace(/\n/g, "\n" + space)
+ };
+ try {
+ if(!goog.isDef(obj)) {
+ str.push("undefined")
+ }else {
+ if(goog.isNull(obj)) {
+ str.push("NULL")
+ }else {
+ if(goog.isString(obj)) {
+ str.push('"' + indentMultiline(obj) + '"')
+ }else {
+ if(goog.isFunction(obj)) {
+ str.push(indentMultiline(String(obj)))
+ }else {
+ if(goog.isObject(obj)) {
+ if(previous.contains(obj)) {
+ str.push("*** reference loop detected ***")
+ }else {
+ previous.add(obj);
+ str.push("{");
+ for(var x in obj) {
+ if(!opt_showFn && goog.isFunction(obj[x])) {
+ continue
+ }
+ str.push("\n");
+ str.push(nestspace);
+ str.push(x + " = ");
+ helper(obj[x], nestspace)
+ }
+ str.push("\n" + space + "}")
+ }
+ }else {
+ str.push(obj)
+ }
+ }
+ }
+ }
+ }
+ }catch(e) {
+ str.push("*** " + e + " ***")
+ }
+ };
+ helper(obj, "");
+ return str.join("")
+};
+goog.debug.exposeArray = function(arr) {
+ var str = [];
+ for(var i = 0;i < arr.length;i++) {
+ if(goog.isArray(arr[i])) {
+ str.push(goog.debug.exposeArray(arr[i]))
+ }else {
+ str.push(arr[i])
+ }
+ }
+ return"[ " + str.join(", ") + " ]"
+};
+goog.debug.exposeException = function(err, opt_fn) {
+ try {
+ var e = goog.debug.normalizeErrorObject(err);
+ var error = "Message: " + goog.string.htmlEscape(e.message) + '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' + e.fileName + "</a>\nLine: " + e.lineNumber + "\n\nBrowser stack:\n" + goog.string.htmlEscape(e.stack + "-> ") + "[end]\n\nJS stack traversal:\n" + goog.string.htmlEscape(goog.debug.getStacktrace(opt_fn) + "-> ");
+ return error
+ }catch(e2) {
+ return"Exception trying to expose exception! You win, we lose. " + e2
+ }
+};
+goog.debug.normalizeErrorObject = function(err) {
+ var href = goog.getObjectByName("window.location.href");
+ return typeof err == "string" ? {message:err, name:"Unknown error", lineNumber:"Not available", fileName:href, stack:"Not available"} : !err.lineNumber || !err.fileName || !err.stack ? {message:err.message, name:err.name, lineNumber:err.lineNumber || err.line || "Not available", fileName:err.fileName || err.filename || err.sourceURL || href, stack:err.stack || "Not available"} : err
+};
+goog.debug.enhanceError = function(err, opt_message) {
+ var error = typeof err == "string" ? Error(err) : err;
+ if(!error.stack) {
+ error.stack = goog.debug.getStacktrace(arguments.callee.caller)
+ }
+ if(opt_message) {
+ var x = 0;
+ while(error["message" + x]) {
+ ++x
+ }
+ error["message" + x] = String(opt_message)
+ }
+ return error
+};
+goog.debug.getStacktraceSimple = function(opt_depth) {
+ var sb = [];
+ var fn = arguments.callee.caller;
+ var depth = 0;
+ while(fn && (!opt_depth || depth < opt_depth)) {
+ sb.push(goog.debug.getFunctionName(fn));
+ sb.push("()\n");
+ try {
+ fn = fn.caller
+ }catch(e) {
+ sb.push("[exception trying to get caller]\n");
+ break
+ }
+ depth++;
+ if(depth >= goog.debug.MAX_STACK_DEPTH) {
+ sb.push("[...long stack...]");
+ break
+ }
+ }
+ if(opt_depth && depth >= opt_depth) {
+ sb.push("[...reached max depth limit...]")
+ }else {
+ sb.push("[end]")
+ }
+ return sb.join("")
+};
+goog.debug.MAX_STACK_DEPTH = 50;
+goog.debug.getStacktrace = function(opt_fn) {
+ return goog.debug.getStacktraceHelper_(opt_fn || arguments.callee.caller, [])
+};
+goog.debug.getStacktraceHelper_ = function(fn, visited) {
+ var sb = [];
+ if(goog.array.contains(visited, fn)) {
+ sb.push("[...circular reference...]")
+ }else {
+ if(fn && visited.length < goog.debug.MAX_STACK_DEPTH) {
+ sb.push(goog.debug.getFunctionName(fn) + "(");
+ var args = fn.arguments;
+ for(var i = 0;i < args.length;i++) {
+ if(i > 0) {
+ sb.push(", ")
+ }
+ var argDesc;
+ var arg = args[i];
+ switch(typeof arg) {
+ case "object":
+ argDesc = arg ? "object" : "null";
+ break;
+ case "string":
+ argDesc = arg;
+ break;
+ case "number":
+ argDesc = String(arg);
+ break;
+ case "boolean":
+ argDesc = arg ? "true" : "false";
+ break;
+ case "function":
+ argDesc = goog.debug.getFunctionName(arg);
+ argDesc = argDesc ? argDesc : "[fn]";
+ break;
+ case "undefined":
+ ;
+ default:
+ argDesc = typeof arg;
+ break
+ }
+ if(argDesc.length > 40) {
+ argDesc = argDesc.substr(0, 40) + "..."
+ }
+ sb.push(argDesc)
+ }
+ visited.push(fn);
+ sb.push(")\n");
+ try {
+ sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited))
+ }catch(e) {
+ sb.push("[exception trying to get caller]\n")
+ }
+ }else {
+ if(fn) {
+ sb.push("[...long stack...]")
+ }else {
+ sb.push("[end]")
+ }
+ }
+ }
+ return sb.join("")
+};
+goog.debug.getFunctionName = function(fn) {
+ var functionSource = String(fn);
+ if(!goog.debug.fnNameCache_[functionSource]) {
+ var matches = /function ([^\(]+)/.exec(functionSource);
+ if(matches) {
+ var method = matches[1];
+ goog.debug.fnNameCache_[functionSource] = method
+ }else {
+ goog.debug.fnNameCache_[functionSource] = "[Anonymous]"
+ }
+ }
+ return goog.debug.fnNameCache_[functionSource]
+};
+goog.debug.makeWhitespaceVisible = function(string) {
+ return string.replace(/ /g, "[_]").replace(/\f/g, "[f]").replace(/\n/g, "[n]\n").replace(/\r/g, "[r]").replace(/\t/g, "[t]")
+};
+goog.debug.fnNameCache_ = {};
+goog.provide("goog.debug.LogRecord");
+goog.debug.LogRecord = function(level, msg, loggerName, opt_time, opt_sequenceNumber) {
+ this.reset(level, msg, loggerName, opt_time, opt_sequenceNumber)
+};
+goog.debug.LogRecord.prototype.time_;
+goog.debug.LogRecord.prototype.level_;
+goog.debug.LogRecord.prototype.msg_;
+goog.debug.LogRecord.prototype.loggerName_;
+goog.debug.LogRecord.prototype.sequenceNumber_ = 0;
+goog.debug.LogRecord.prototype.exception_ = null;
+goog.debug.LogRecord.prototype.exceptionText_ = null;
+goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS = true;
+goog.debug.LogRecord.nextSequenceNumber_ = 0;
+goog.debug.LogRecord.prototype.reset = function(level, msg, loggerName, opt_time, opt_sequenceNumber) {
+ if(goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS) {
+ this.sequenceNumber_ = typeof opt_sequenceNumber == "number" ? opt_sequenceNumber : goog.debug.LogRecord.nextSequenceNumber_++
+ }
+ this.time_ = opt_time || goog.now();
+ this.level_ = level;
+ this.msg_ = msg;
+ this.loggerName_ = loggerName;
+ delete this.exception_;
+ delete this.exceptionText_
+};
+goog.debug.LogRecord.prototype.getLoggerName = function() {
+ return this.loggerName_
+};
+goog.debug.LogRecord.prototype.getException = function() {
+ return this.exception_
+};
+goog.debug.LogRecord.prototype.setException = function(exception) {
+ this.exception_ = exception
+};
+goog.debug.LogRecord.prototype.getExceptionText = function() {
+ return this.exceptionText_
+};
+goog.debug.LogRecord.prototype.setExceptionText = function(text) {
+ this.exceptionText_ = text
+};
+goog.debug.LogRecord.prototype.setLoggerName = function(loggerName) {
+ this.loggerName_ = loggerName
+};
+goog.debug.LogRecord.prototype.getLevel = function() {
+ return this.level_
+};
+goog.debug.LogRecord.prototype.setLevel = function(level) {
+ this.level_ = level
+};
+goog.debug.LogRecord.prototype.getMessage = function() {
+ return this.msg_
+};
+goog.debug.LogRecord.prototype.setMessage = function(msg) {
+ this.msg_ = msg
+};
+goog.debug.LogRecord.prototype.getMillis = function() {
+ return this.time_
+};
+goog.debug.LogRecord.prototype.setMillis = function(time) {
+ this.time_ = time
+};
+goog.debug.LogRecord.prototype.getSequenceNumber = function() {
+ return this.sequenceNumber_
+};
+goog.provide("goog.debug.LogBuffer");
+goog.require("goog.asserts");
+goog.require("goog.debug.LogRecord");
+goog.debug.LogBuffer = function() {
+ goog.asserts.assert(goog.debug.LogBuffer.isBufferingEnabled(), "Cannot use goog.debug.LogBuffer without defining " + "goog.debug.LogBuffer.CAPACITY.");
+ this.clear()
+};
+goog.debug.LogBuffer.getInstance = function() {
+ if(!goog.debug.LogBuffer.instance_) {
+ goog.debug.LogBuffer.instance_ = new goog.debug.LogBuffer
+ }
+ return goog.debug.LogBuffer.instance_
+};
+goog.debug.LogBuffer.CAPACITY = 0;
+goog.debug.LogBuffer.prototype.buffer_;
+goog.debug.LogBuffer.prototype.curIndex_;
+goog.debug.LogBuffer.prototype.isFull_;
+goog.debug.LogBuffer.prototype.addRecord = function(level, msg, loggerName) {
+ var curIndex = (this.curIndex_ + 1) % goog.debug.LogBuffer.CAPACITY;
+ this.curIndex_ = curIndex;
+ if(this.isFull_) {
+ var ret = this.buffer_[curIndex];
+ ret.reset(level, msg, loggerName);
+ return ret
+ }
+ this.isFull_ = curIndex == goog.debug.LogBuffer.CAPACITY - 1;
+ return this.buffer_[curIndex] = new goog.debug.LogRecord(level, msg, loggerName)
+};
+goog.debug.LogBuffer.isBufferingEnabled = function() {
+ return goog.debug.LogBuffer.CAPACITY > 0
+};
+goog.debug.LogBuffer.prototype.clear = function() {
+ this.buffer_ = new Array(goog.debug.LogBuffer.CAPACITY);
+ this.curIndex_ = -1;
+ this.isFull_ = false
+};
+goog.debug.LogBuffer.prototype.forEachRecord = function(func) {
+ var buffer = this.buffer_;
+ if(!buffer[0]) {
+ return
+ }
+ var curIndex = this.curIndex_;
+ var i = this.isFull_ ? curIndex : -1;
+ do {
+ i = (i + 1) % goog.debug.LogBuffer.CAPACITY;
+ func(buffer[i])
+ }while(i != curIndex)
+};
+goog.provide("goog.debug.LogManager");
+goog.provide("goog.debug.Logger");
+goog.provide("goog.debug.Logger.Level");
+goog.require("goog.array");
+goog.require("goog.asserts");
+goog.require("goog.debug");
+goog.require("goog.debug.LogBuffer");
+goog.require("goog.debug.LogRecord");
+goog.debug.Logger = function(name) {
+ this.name_ = name
+};
+goog.debug.Logger.prototype.parent_ = null;
+goog.debug.Logger.prototype.level_ = null;
+goog.debug.Logger.prototype.children_ = null;
+goog.debug.Logger.prototype.handlers_ = null;
+goog.debug.Logger.ENABLE_HIERARCHY = true;
+if(!goog.debug.Logger.ENABLE_HIERARCHY) {
+ goog.debug.Logger.rootHandlers_ = [];
+ goog.debug.Logger.rootLevel_
+}
+goog.debug.Logger.Level = function(name, value) {
+ this.name = name;
+ this.value = value
+};
+goog.debug.Logger.Level.prototype.toString = function() {
+ return this.name
+};
+goog.debug.Logger.Level.OFF = new goog.debug.Logger.Level("OFF", Infinity);
+goog.debug.Logger.Level.SHOUT = new goog.debug.Logger.Level("SHOUT", 1200);
+goog.debug.Logger.Level.SEVERE = new goog.debug.Logger.Level("SEVERE", 1E3);
+goog.debug.Logger.Level.WARNING = new goog.debug.Logger.Level("WARNING", 900);
+goog.debug.Logger.Level.INFO = new goog.debug.Logger.Level("INFO", 800);
+goog.debug.Logger.Level.CONFIG = new goog.debug.Logger.Level("CONFIG", 700);
+goog.debug.Logger.Level.FINE = new goog.debug.Logger.Level("FINE", 500);
+goog.debug.Logger.Level.FINER = new goog.debug.Logger.Level("FINER", 400);
+goog.debug.Logger.Level.FINEST = new goog.debug.Logger.Level("FINEST", 300);
+goog.debug.Logger.Level.ALL = new goog.debug.Logger.Level("ALL", 0);
+goog.debug.Logger.Level.PREDEFINED_LEVELS = [goog.debug.Logger.Level.OFF, goog.debug.Logger.Level.SHOUT, goog.debug.Logger.Level.SEVERE, goog.debug.Logger.Level.WARNING, goog.debug.Logger.Level.INFO, goog.debug.Logger.Level.CONFIG, goog.debug.Logger.Level.FINE, goog.debug.Logger.Level.FINER, goog.debug.Logger.Level.FINEST, goog.debug.Logger.Level.ALL];
+goog.debug.Logger.Level.predefinedLevelsCache_ = null;
+goog.debug.Logger.Level.createPredefinedLevelsCache_ = function() {
+ goog.debug.Logger.Level.predefinedLevelsCache_ = {};
+ for(var i = 0, level;level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i];i++) {
+ goog.debug.Logger.Level.predefinedLevelsCache_[level.value] = level;
+ goog.debug.Logger.Level.predefinedLevelsCache_[level.name] = level
+ }
+};
+goog.debug.Logger.Level.getPredefinedLevel = function(name) {
+ if(!goog.debug.Logger.Level.predefinedLevelsCache_) {
+ goog.debug.Logger.Level.createPredefinedLevelsCache_()
+ }
+ return goog.debug.Logger.Level.predefinedLevelsCache_[name] || null
+};
+goog.debug.Logger.Level.getPredefinedLevelByValue = function(value) {
+ if(!goog.debug.Logger.Level.predefinedLevelsCache_) {
+ goog.debug.Logger.Level.createPredefinedLevelsCache_()
+ }
+ if(value in goog.debug.Logger.Level.predefinedLevelsCache_) {
+ return goog.debug.Logger.Level.predefinedLevelsCache_[value]
+ }
+ for(var i = 0;i < goog.debug.Logger.Level.PREDEFINED_LEVELS.length;++i) {
+ var level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i];
+ if(level.value <= value) {
+ return level
+ }
+ }
+ return null
+};
+goog.debug.Logger.getLogger = function(name) {
+ return goog.debug.LogManager.getLogger(name)
+};
+goog.debug.Logger.prototype.getName = function() {
+ return this.name_
+};
+goog.debug.Logger.prototype.addHandler = function(handler) {
+ if(goog.debug.Logger.ENABLE_HIERARCHY) {
+ if(!this.handlers_) {
+ this.handlers_ = []
+ }
+ this.handlers_.push(handler)
+ }else {
+ goog.asserts.assert(!this.name_, "Cannot call addHandler on a non-root logger when " + "goog.debug.Logger.ENABLE_HIERARCHY is false.");
+ goog.debug.Logger.rootHandlers_.push(handler)
+ }
+};
+goog.debug.Logger.prototype.removeHandler = function(handler) {
+ var handlers = goog.debug.Logger.ENABLE_HIERARCHY ? this.handlers_ : goog.debug.Logger.rootHandlers_;
+ return!!handlers && goog.array.remove(handlers, handler)
+};
+goog.debug.Logger.prototype.getParent = function() {
+ return this.parent_
+};
+goog.debug.Logger.prototype.getChildren = function() {
+ if(!this.children_) {
+ this.children_ = {}
+ }
+ return this.children_
+};
+goog.debug.Logger.prototype.setLevel = function(level) {
+ if(goog.debug.Logger.ENABLE_HIERARCHY) {
+ this.level_ = level
+ }else {
+ goog.asserts.assert(!this.name_, "Cannot call setLevel() on a non-root logger when " + "goog.debug.Logger.ENABLE_HIERARCHY is false.");
+ goog.debug.Logger.rootLevel_ = level
+ }
+};
+goog.debug.Logger.prototype.getLevel = function() {
+ return this.level_
+};
+goog.debug.Logger.prototype.getEffectiveLevel = function() {
+ if(!goog.debug.Logger.ENABLE_HIERARCHY) {
+ return goog.debug.Logger.rootLevel_
+ }
+ if(this.level_) {
+ return this.level_
+ }
+ if(this.parent_) {
+ return this.parent_.getEffectiveLevel()
+ }
+ goog.asserts.fail("Root logger has no level set.");
+ return null
+};
+goog.debug.Logger.prototype.isLoggable = function(level) {
+ return level.value >= this.getEffectiveLevel().value
+};
+goog.debug.Logger.prototype.log = function(level, msg, opt_exception) {
+ if(this.isLoggable(level)) {
+ this.doLogRecord_(this.getLogRecord(level, msg, opt_exception))
+ }
+};
+goog.debug.Logger.prototype.getLogRecord = function(level, msg, opt_exception) {
+ if(goog.debug.LogBuffer.isBufferingEnabled()) {
+ var logRecord = goog.debug.LogBuffer.getInstance().addRecord(level, msg, this.name_)
+ }else {
+ logRecord = new goog.debug.LogRecord(level, String(msg), this.name_)
+ }
+ if(opt_exception) {
+ logRecord.setException(opt_exception);
+ logRecord.setExceptionText(goog.debug.exposeException(opt_exception, arguments.callee.caller))
+ }
+ return logRecord
+};
+goog.debug.Logger.prototype.shout = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.SHOUT, msg, opt_exception)
+};
+goog.debug.Logger.prototype.severe = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.SEVERE, msg, opt_exception)
+};
+goog.debug.Logger.prototype.warning = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.WARNING, msg, opt_exception)
+};
+goog.debug.Logger.prototype.info = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.INFO, msg, opt_exception)
+};
+goog.debug.Logger.prototype.config = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.CONFIG, msg, opt_exception)
+};
+goog.debug.Logger.prototype.fine = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.FINE, msg, opt_exception)
+};
+goog.debug.Logger.prototype.finer = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.FINER, msg, opt_exception)
+};
+goog.debug.Logger.prototype.finest = function(msg, opt_exception) {
+ this.log(goog.debug.Logger.Level.FINEST, msg, opt_exception)
+};
+goog.debug.Logger.prototype.logRecord = function(logRecord) {
+ if(this.isLoggable(logRecord.getLevel())) {
+ this.doLogRecord_(logRecord)
+ }
+};
+goog.debug.Logger.prototype.doLogRecord_ = function(logRecord) {
+ if(goog.debug.Logger.ENABLE_HIERARCHY) {
+ var target = this;
+ while(target) {
+ target.callPublish_(logRecord);
+ target = target.getParent()
+ }
+ }else {
+ for(var i = 0, handler;handler = goog.debug.Logger.rootHandlers_[i++];) {
+ handler(logRecord)
+ }
+ }
+};
+goog.debug.Logger.prototype.callPublish_ = function(logRecord) {
+ if(this.handlers_) {
+ for(var i = 0, handler;handler = this.handlers_[i];i++) {
+ handler(logRecord)
+ }
+ }
+};
+goog.debug.Logger.prototype.setParent_ = function(parent) {
+ this.parent_ = parent
+};
+goog.debug.Logger.prototype.addChild_ = function(name, logger) {
+ this.getChildren()[name] = logger
+};
+goog.debug.LogManager = {};
+goog.debug.LogManager.loggers_ = {};
+goog.debug.LogManager.rootLogger_ = null;
+goog.debug.LogManager.initialize = function() {
+ if(!goog.debug.LogManager.rootLogger_) {
+ goog.debug.LogManager.rootLogger_ = new goog.debug.Logger("");
+ goog.debug.LogManager.loggers_[""] = goog.debug.LogManager.rootLogger_;
+ goog.debug.LogManager.rootLogger_.setLevel(goog.debug.Logger.Level.CONFIG)
+ }
+};
+goog.debug.LogManager.getLoggers = function() {
+ return goog.debug.LogManager.loggers_
+};
+goog.debug.LogManager.getRoot = function() {
+ goog.debug.LogManager.initialize();
+ return goog.debug.LogManager.rootLogger_
+};
+goog.debug.LogManager.getLogger = function(name) {
+ goog.debug.LogManager.initialize();
+ var ret = goog.debug.LogManager.loggers_[name];
+ return ret || goog.debug.LogManager.createLogger_(name)
+};
+goog.debug.LogManager.createFunctionForCatchErrors = function(opt_logger) {
+ return function(info) {
+ var logger = opt_logger || goog.debug.LogManager.getRoot();
+ logger.severe("Error: " + info.message + " (" + info.fileName + " @ Line: " + info.line + ")")
+ }
+};
+goog.debug.LogManager.createLogger_ = function(name) {
+ var logger = new goog.debug.Logger(name);
+ if(goog.debug.Logger.ENABLE_HIERARCHY) {
+ var lastDotIndex = name.lastIndexOf(".");
+ var parentName = name.substr(0, lastDotIndex);
+ var leafName = name.substr(lastDotIndex + 1);
+ var parentLogger = goog.debug.LogManager.getLogger(parentName);
+ parentLogger.addChild_(leafName, logger);
+ logger.setParent_(parentLogger)
+ }
+ goog.debug.LogManager.loggers_[name] = logger;
+ return logger
+};
+goog.provide("goog.dom.SavedRange");
+goog.require("goog.Disposable");
+goog.require("goog.debug.Logger");
+goog.dom.SavedRange = function() {
+ goog.Disposable.call(this)
+};
+goog.inherits(goog.dom.SavedRange, goog.Disposable);
+goog.dom.SavedRange.logger_ = goog.debug.Logger.getLogger("goog.dom.SavedRange");
+goog.dom.SavedRange.prototype.restore = function(opt_stayAlive) {
+ if(this.isDisposed()) {
+ goog.dom.SavedRange.logger_.severe("Disposed SavedRange objects cannot be restored.")
+ }
+ var range = this.restoreInternal();
+ if(!opt_stayAlive) {
+ this.dispose()
+ }
+ return range
+};
+goog.dom.SavedRange.prototype.restoreInternal = goog.abstractMethod;
+goog.provide("goog.dom.SavedCaretRange");
+goog.require("goog.array");
+goog.require("goog.dom");
+goog.require("goog.dom.SavedRange");
+goog.require("goog.dom.TagName");
+goog.require("goog.string");
+goog.dom.SavedCaretRange = function(range) {
+ goog.dom.SavedRange.call(this);
+ this.startCaretId_ = goog.string.createUniqueString();
+ this.endCaretId_ = goog.string.createUniqueString();
+ this.dom_ = goog.dom.getDomHelper(range.getDocument());
+ range.surroundWithNodes(this.createCaret_(true), this.createCaret_(false))
+};
+goog.inherits(goog.dom.SavedCaretRange, goog.dom.SavedRange);
+goog.dom.SavedCaretRange.prototype.toAbstractRange = function() {
+ var range = null;
+ var startCaret = this.getCaret(true);
+ var endCaret = this.getCaret(false);
+ if(startCaret && endCaret) {
+ range = goog.dom.Range.createFromNodes(startCaret, 0, endCaret, 0)
+ }
+ return range
+};
+goog.dom.SavedCaretRange.prototype.getCaret = function(start) {
+ return this.dom_.getElement(start ? this.startCaretId_ : this.endCaretId_)
+};
+goog.dom.SavedCaretRange.prototype.removeCarets = function(opt_range) {
+ goog.dom.removeNode(this.getCaret(true));
+ goog.dom.removeNode(this.getCaret(false));
+ return opt_range
+};
+goog.dom.SavedCaretRange.prototype.setRestorationDocument = function(doc) {
+ this.dom_.setDocument(doc)
+};
+goog.dom.SavedCaretRange.prototype.restoreInternal = function() {
+ var range = null;
+ var startCaret = this.getCaret(true);
+ var endCaret = this.getCaret(false);
+ if(startCaret && endCaret) {
+ var startNode = startCaret.parentNode;
+ var startOffset = goog.array.indexOf(startNode.childNodes, startCaret);
+ var endNode = endCaret.parentNode;
+ var endOffset = goog.array.indexOf(endNode.childNodes, endCaret);
+ if(endNode == startNode) {
+ endOffset -= 1
+ }
+ range = goog.dom.Range.createFromNodes(startNode, startOffset, endNode, endOffset);
+ range = this.removeCarets(range);
+ range.select()
+ }else {
+ this.removeCarets()
+ }
+ return range
+};
+goog.dom.SavedCaretRange.prototype.disposeInternal = function() {
+ this.removeCarets();
+ this.dom_ = null
+};
+goog.dom.SavedCaretRange.prototype.createCaret_ = function(start) {
+ return this.dom_.createDom(goog.dom.TagName.SPAN, {id:start ? this.startCaretId_ : this.endCaretId_})
+};
+goog.dom.SavedCaretRange.CARET_REGEX = /<span\s+id="?goog_\d+"?><\/span>/ig;
+goog.dom.SavedCaretRange.htmlEqual = function(str1, str2) {
+ return str1 == str2 || str1.replace(goog.dom.SavedCaretRange.CARET_REGEX, "") == str2.replace(goog.dom.SavedCaretRange.CARET_REGEX, "")
+};
+goog.provide("goog.dom.TagIterator");
+goog.provide("goog.dom.TagWalkType");
+goog.require("goog.dom.NodeType");
+goog.require("goog.iter.Iterator");
+goog.require("goog.iter.StopIteration");
+goog.dom.TagWalkType = {START_TAG:1, OTHER:0, END_TAG:-1};
+goog.dom.TagIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_tagType, opt_depth) {
+ this.reversed = !!opt_reversed;
+ if(opt_node) {
+ this.setPosition(opt_node, opt_tagType)
+ }
+ this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0;
+ if(this.reversed) {
+ this.depth *= -1
+ }
+ this.constrained = !opt_unconstrained
+};
+goog.inherits(goog.dom.TagIterator, goog.iter.Iterator);
+goog.dom.TagIterator.prototype.node = null;
+goog.dom.TagIterator.prototype.tagType = goog.dom.TagWalkType.OTHER;
+goog.dom.TagIterator.prototype.depth;
+goog.dom.TagIterator.prototype.reversed;
+goog.dom.TagIterator.prototype.constrained;
+goog.dom.TagIterator.prototype.started_ = false;
+goog.dom.TagIterator.prototype.setPosition = function(node, opt_tagType, opt_depth) {
+ this.node = node;
+ if(node) {
+ if(goog.isNumber(opt_tagType)) {
+ this.tagType = opt_tagType
+ }else {
+ this.tagType = this.node.nodeType != goog.dom.NodeType.ELEMENT ? goog.dom.TagWalkType.OTHER : this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG
+ }
+ }
+ if(goog.isNumber(opt_depth)) {
+ this.depth = opt_depth
+ }
+};
+goog.dom.TagIterator.prototype.copyFrom = function(other) {
+ this.node = other.node;
+ this.tagType = other.tagType;
+ this.depth = other.depth;
+ this.reversed = other.reversed;
+ this.constrained = other.constrained
+};
+goog.dom.TagIterator.prototype.clone = function() {
+ return new goog.dom.TagIterator(this.node, this.reversed, !this.constrained, this.tagType, this.depth)
+};
+goog.dom.TagIterator.prototype.skipTag = function() {
+ var check = this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG;
+ if(this.tagType == check) {
+ this.tagType = check * -1;
+ this.depth += this.tagType * (this.reversed ? -1 : 1)
+ }
+};
+goog.dom.TagIterator.prototype.restartTag = function() {
+ var check = this.reversed ? goog.dom.TagWalkType.START_TAG : goog.dom.TagWalkType.END_TAG;
+ if(this.tagType == check) {
+ this.tagType = check * -1;
+ this.depth += this.tagType * (this.reversed ? -1 : 1)
+ }
+};
+goog.dom.TagIterator.prototype.next = function() {
+ var node;
+ if(this.started_) {
+ if(!this.node || this.constrained && this.depth == 0) {
+ throw goog.iter.StopIteration;
+ }
+ node = this.node;
+ var startType = this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG;
+ if(this.tagType == startType) {
+ var child = this.reversed ? node.lastChild : node.firstChild;
+ if(child) {
+ this.setPosition(child)
+ }else {
+ this.setPosition(node, startType * -1)
+ }
+ }else {
+ var sibling = this.reversed ? node.previousSibling : node.nextSibling;
+ if(sibling) {
+ this.setPosition(sibling)
+ }else {
+ this.setPosition(node.parentNode, startType * -1)
+ }
+ }
+ this.depth += this.tagType * (this.reversed ? -1 : 1)
+ }else {
+ this.started_ = true
+ }
+ node = this.node;
+ if(!this.node) {
+ throw goog.iter.StopIteration;
+ }
+ return node
+};
+goog.dom.TagIterator.prototype.isStarted = function() {
+ return this.started_
+};
+goog.dom.TagIterator.prototype.isStartTag = function() {
+ return this.tagType == goog.dom.TagWalkType.START_TAG
+};
+goog.dom.TagIterator.prototype.isEndTag = function() {
+ return this.tagType == goog.dom.TagWalkType.END_TAG
+};
+goog.dom.TagIterator.prototype.isNonElement = function() {
+ return this.tagType == goog.dom.TagWalkType.OTHER
+};
+goog.dom.TagIterator.prototype.equals = function(other) {
+ return other.node == this.node && (!this.node || other.tagType == this.tagType)
+};
+goog.dom.TagIterator.prototype.splice = function(var_args) {
+ var node = this.node;
+ this.restartTag();
+ this.reversed = !this.reversed;
+ goog.dom.TagIterator.prototype.next.call(this);
+ this.reversed = !this.reversed;
+ var arr = goog.isArrayLike(arguments[0]) ? arguments[0] : arguments;
+ for(var i = arr.length - 1;i >= 0;i--) {
+ goog.dom.insertSiblingAfter(arr[i], node)
+ }
+ goog.dom.removeNode(node)
+};
+goog.provide("goog.dom.AbstractRange");
+goog.provide("goog.dom.RangeIterator");
+goog.provide("goog.dom.RangeType");
+goog.require("goog.dom");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.SavedCaretRange");
+goog.require("goog.dom.TagIterator");
+goog.require("goog.userAgent");
+goog.dom.RangeType = {TEXT:"text", CONTROL:"control", MULTI:"mutli"};
+goog.dom.AbstractRange = function() {
+};
+goog.dom.AbstractRange.getBrowserSelectionForWindow = function(win) {
+ if(win.getSelection) {
+ return win.getSelection()
+ }else {
+ var doc = win.document;
+ var sel = doc.selection;
+ if(sel) {
+ try {
+ var range = sel.createRange();
+ if(range.parentElement) {
+ if(range.parentElement().document != doc) {
+ return null
+ }
+ }else {
+ if(!range.length || range.item(0).document != doc) {
+ return null
+ }
+ }
+ }catch(e) {
+ return null
+ }
+ return sel
+ }
+ return null
+ }
+};
+goog.dom.AbstractRange.isNativeControlRange = function(range) {
+ return!!range && !!range.addElement
+};
+goog.dom.AbstractRange.prototype.clone = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getType = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getBrowserRangeObject = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.setBrowserRangeObject = function(nativeRange) {
+ return false
+};
+goog.dom.AbstractRange.prototype.getTextRangeCount = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getTextRange = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getTextRanges = function() {
+ var output = [];
+ for(var i = 0, len = this.getTextRangeCount();i < len;i++) {
+ output.push(this.getTextRange(i))
+ }
+ return output
+};
+goog.dom.AbstractRange.prototype.getContainer = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getContainerElement = function() {
+ var node = this.getContainer();
+ return node.nodeType == goog.dom.NodeType.ELEMENT ? node : node.parentNode
+};
+goog.dom.AbstractRange.prototype.getStartNode = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getStartOffset = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getEndNode = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getEndOffset = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getAnchorNode = function() {
+ return this.isReversed() ? this.getEndNode() : this.getStartNode()
+};
+goog.dom.AbstractRange.prototype.getAnchorOffset = function() {
+ return this.isReversed() ? this.getEndOffset() : this.getStartOffset()
+};
+goog.dom.AbstractRange.prototype.getFocusNode = function() {
+ return this.isReversed() ? this.getStartNode() : this.getEndNode()
+};
+goog.dom.AbstractRange.prototype.getFocusOffset = function() {
+ return this.isReversed() ? this.getStartOffset() : this.getEndOffset()
+};
+goog.dom.AbstractRange.prototype.isReversed = function() {
+ return false
+};
+goog.dom.AbstractRange.prototype.getDocument = function() {
+ return goog.dom.getOwnerDocument(goog.userAgent.IE ? this.getContainer() : this.getStartNode())
+};
+goog.dom.AbstractRange.prototype.getWindow = function() {
+ return goog.dom.getWindow(this.getDocument())
+};
+goog.dom.AbstractRange.prototype.containsRange = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+ return this.containsRange(goog.dom.Range.createFromNodeContents(node), opt_allowPartial)
+};
+goog.dom.AbstractRange.prototype.isRangeInDocument = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.isCollapsed = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getText = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getHtmlFragment = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getValidHtml = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.getPastableHtml = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.__iterator__ = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.select = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.removeContents = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.insertNode = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.replaceContentsWithNode = function(node) {
+ if(!this.isCollapsed()) {
+ this.removeContents()
+ }
+ return this.insertNode(node, true)
+};
+goog.dom.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.saveUsingDom = goog.abstractMethod;
+goog.dom.AbstractRange.prototype.saveUsingCarets = function() {
+ return this.getStartNode() && this.getEndNode() ? new goog.dom.SavedCaretRange(this) : null
+};
+goog.dom.AbstractRange.prototype.collapse = goog.abstractMethod;
+goog.dom.RangeIterator = function(node, opt_reverse) {
+ goog.dom.TagIterator.call(this, node, opt_reverse, true)
+};
+goog.inherits(goog.dom.RangeIterator, goog.dom.TagIterator);
+goog.dom.RangeIterator.prototype.getStartTextOffset = goog.abstractMethod;
+goog.dom.RangeIterator.prototype.getEndTextOffset = goog.abstractMethod;
+goog.dom.RangeIterator.prototype.getStartNode = goog.abstractMethod;
+goog.dom.RangeIterator.prototype.getEndNode = goog.abstractMethod;
+goog.dom.RangeIterator.prototype.isLast = goog.abstractMethod;
+goog.provide("goog.dom.AbstractMultiRange");
+goog.require("goog.array");
+goog.require("goog.dom");
+goog.require("goog.dom.AbstractRange");
+goog.dom.AbstractMultiRange = function() {
+};
+goog.inherits(goog.dom.AbstractMultiRange, goog.dom.AbstractRange);
+goog.dom.AbstractMultiRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+ var ranges = this.getTextRanges();
+ var otherRanges = otherRange.getTextRanges();
+ var fn = opt_allowPartial ? goog.array.some : goog.array.every;
+ return fn(otherRanges, function(otherRange) {
+ return goog.array.some(ranges, function(range) {
+ return range.containsRange(otherRange, opt_allowPartial)
+ })
+ })
+};
+goog.dom.AbstractMultiRange.prototype.insertNode = function(node, before) {
+ if(before) {
+ goog.dom.insertSiblingBefore(node, this.getStartNode())
+ }else {
+ goog.dom.insertSiblingAfter(node, this.getEndNode())
+ }
+ return node
+};
+goog.dom.AbstractMultiRange.prototype.surroundWithNodes = function(startNode, endNode) {
+ this.insertNode(startNode, true);
+ this.insertNode(endNode, false)
+};
+goog.provide("goog.dom.TextRangeIterator");
+goog.require("goog.array");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.RangeIterator");
+goog.require("goog.dom.TagName");
+goog.require("goog.iter.StopIteration");
+goog.dom.TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) {
+ var goNext;
+ if(startNode) {
+ this.startNode_ = startNode;
+ this.startOffset_ = startOffset;
+ this.endNode_ = endNode;
+ this.endOffset_ = endOffset;
+ if(startNode.nodeType == goog.dom.NodeType.ELEMENT && startNode.tagName != goog.dom.TagName.BR) {
+ var startChildren = startNode.childNodes;
+ var candidate = startChildren[startOffset];
+ if(candidate) {
+ this.startNode_ = candidate;
+ this.startOffset_ = 0
+ }else {
+ if(startChildren.length) {
+ this.startNode_ = goog.array.peek(startChildren)
+ }
+ goNext = true
+ }
+ }
+ if(endNode.nodeType == goog.dom.NodeType.ELEMENT) {
+ this.endNode_ = endNode.childNodes[endOffset];
+ if(this.endNode_) {
+ this.endOffset_ = 0
+ }else {
+ this.endNode_ = endNode
+ }
+ }
+ }
+ goog.dom.RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse);
+ if(goNext) {
+ try {
+ this.next()
+ }catch(e) {
+ if(e != goog.iter.StopIteration) {
+ throw e;
+ }
+ }
+ }
+};
+goog.inherits(goog.dom.TextRangeIterator, goog.dom.RangeIterator);
+goog.dom.TextRangeIterator.prototype.startNode_ = null;
+goog.dom.TextRangeIterator.prototype.endNode_ = null;
+goog.dom.TextRangeIterator.prototype.startOffset_ = 0;
+goog.dom.TextRangeIterator.prototype.endOffset_ = 0;
+goog.dom.TextRangeIterator.prototype.getStartTextOffset = function() {
+ return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.startNode_ ? this.startOffset_ : 0
+};
+goog.dom.TextRangeIterator.prototype.getEndTextOffset = function() {
+ return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.endNode_ ? this.endOffset_ : this.node.nodeValue.length
+};
+goog.dom.TextRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog.dom.TextRangeIterator.prototype.setStartNode = function(node) {
+ if(!this.isStarted()) {
+ this.setPosition(node)
+ }
+ this.startNode_ = node;
+ this.startOffset_ = 0
+};
+goog.dom.TextRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog.dom.TextRangeIterator.prototype.setEndNode = function(node) {
+ this.endNode_ = node;
+ this.endOffset_ = 0
+};
+goog.dom.TextRangeIterator.prototype.isLast = function() {
+ return this.isStarted() && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag())
+};
+goog.dom.TextRangeIterator.prototype.next = function() {
+ if(this.isLast()) {
+ throw goog.iter.StopIteration;
+ }
+ return goog.dom.TextRangeIterator.superClass_.next.call(this)
+};
+goog.dom.TextRangeIterator.prototype.skipTag = function() {
+ goog.dom.TextRangeIterator.superClass_.skipTag.apply(this);
+ if(goog.dom.contains(this.node, this.endNode_)) {
+ throw goog.iter.StopIteration;
+ }
+};
+goog.dom.TextRangeIterator.prototype.copyFrom = function(other) {
+ this.startNode_ = other.startNode_;
+ this.endNode_ = other.endNode_;
+ this.startOffset_ = other.startOffset_;
+ this.endOffset_ = other.endOffset_;
+ this.isReversed_ = other.isReversed_;
+ goog.dom.TextRangeIterator.superClass_.copyFrom.call(this, other)
+};
+goog.dom.TextRangeIterator.prototype.clone = function() {
+ var copy = new goog.dom.TextRangeIterator(this.startNode_, this.startOffset_, this.endNode_, this.endOffset_, this.isReversed_);
+ copy.copyFrom(this);
+ return copy
+};
+goog.provide("goog.dom.RangeEndpoint");
+goog.dom.RangeEndpoint = {START:1, END:0};
+goog.provide("goog.userAgent.jscript");
+goog.require("goog.string");
+goog.userAgent.jscript.ASSUME_NO_JSCRIPT = false;
+goog.userAgent.jscript.init_ = function() {
+ var hasScriptEngine = "ScriptEngine" in goog.global;
+ goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_ = hasScriptEngine && goog.global["ScriptEngine"]() == "JScript";
+ goog.userAgent.jscript.DETECTED_VERSION_ = goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_ ? goog.global["ScriptEngineMajorVersion"]() + "." + goog.global["ScriptEngineMinorVersion"]() + "." + goog.global["ScriptEngineBuildVersion"]() : "0"
+};
+if(!goog.userAgent.jscript.ASSUME_NO_JSCRIPT) {
+ goog.userAgent.jscript.init_()
+}
+goog.userAgent.jscript.HAS_JSCRIPT = goog.userAgent.jscript.ASSUME_NO_JSCRIPT ? false : goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_;
+goog.userAgent.jscript.VERSION = goog.userAgent.jscript.ASSUME_NO_JSCRIPT ? "0" : goog.userAgent.jscript.DETECTED_VERSION_;
+goog.userAgent.jscript.isVersion = function(version) {
+ return goog.string.compareVersions(goog.userAgent.jscript.VERSION, version) >= 0
+};
+goog.provide("goog.string.StringBuffer");
+goog.require("goog.userAgent.jscript");
+goog.string.StringBuffer = function(opt_a1, var_args) {
+ this.buffer_ = goog.userAgent.jscript.HAS_JSCRIPT ? [] : "";
+ if(opt_a1 != null) {
+ this.append.apply(this, arguments)
+ }
+};
+goog.string.StringBuffer.prototype.set = function(s) {
+ this.clear();
+ this.append(s)
+};
+if(goog.userAgent.jscript.HAS_JSCRIPT) {
+ goog.string.StringBuffer.prototype.bufferLength_ = 0;
+ goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) {
+ if(opt_a2 == null) {
+ this.buffer_[this.bufferLength_++] = a1
+ }else {
+ this.buffer_.push.apply(this.buffer_, arguments);
+ this.bufferLength_ = this.buffer_.length
+ }
+ return this
+ }
+}else {
+ goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) {
+ this.buffer_ += a1;
+ if(opt_a2 != null) {
+ for(var i = 1;i < arguments.length;i++) {
+ this.buffer_ += arguments[i]
+ }
+ }
+ return this
+ }
+}
+goog.string.StringBuffer.prototype.clear = function() {
+ if(goog.userAgent.jscript.HAS_JSCRIPT) {
+ this.buffer_.length = 0;
+ this.bufferLength_ = 0
+ }else {
+ this.buffer_ = ""
+ }
+};
+goog.string.StringBuffer.prototype.getLength = function() {
+ return this.toString().length
+};
+goog.string.StringBuffer.prototype.toString = function() {
+ if(goog.userAgent.jscript.HAS_JSCRIPT) {
+ var str = this.buffer_.join("");
+ this.clear();
+ if(str) {
+ this.append(str)
+ }
+ return str
+ }else {
+ return this.buffer_
+ }
+};
+goog.provide("goog.dom.browserrange.AbstractRange");
+goog.require("goog.dom");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.RangeEndpoint");
+goog.require("goog.dom.TagName");
+goog.require("goog.dom.TextRangeIterator");
+goog.require("goog.iter");
+goog.require("goog.string");
+goog.require("goog.string.StringBuffer");
+goog.require("goog.userAgent");
+goog.dom.browserrange.AbstractRange = function() {
+};
+goog.dom.browserrange.AbstractRange.prototype.clone = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getBrowserRange = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getContainer = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getStartNode = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getStartOffset = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getEndNode = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getEndOffset = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.compareBrowserRangeEndpoints = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.containsRange = function(abstractRange, opt_allowPartial) {
+ var checkPartial = opt_allowPartial && !abstractRange.isCollapsed();
+ var range = abstractRange.getBrowserRange();
+ var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;
+ try {
+ if(checkPartial) {
+ return this.compareBrowserRangeEndpoints(range, end, start) >= 0 && this.compareBrowserRangeEndpoints(range, start, end) <= 0
+ }else {
+ return this.compareBrowserRangeEndpoints(range, end, end) >= 0 && this.compareBrowserRangeEndpoints(range, start, start) <= 0
+ }
+ }catch(e) {
+ if(!goog.userAgent.IE) {
+ throw e;
+ }
+ return false
+ }
+};
+goog.dom.browserrange.AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+ return this.containsRange(goog.dom.browserrange.createRangeFromNodeContents(node), opt_allowPartial)
+};
+goog.dom.browserrange.AbstractRange.prototype.isCollapsed = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getText = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.getHtmlFragment = function() {
+ var output = new goog.string.StringBuffer;
+ goog.iter.forEach(this, function(node, ignore, it) {
+ if(node.nodeType == goog.dom.NodeType.TEXT) {
+ output.append(goog.string.htmlEscape(node.nodeValue.substring(it.getStartTextOffset(), it.getEndTextOffset())))
+ }else {
+ if(node.nodeType == goog.dom.NodeType.ELEMENT) {
+ if(it.isEndTag()) {
+ if(goog.dom.canHaveChildren(node)) {
+ output.append("</" + node.tagName + ">")
+ }
+ }else {
+ var shallow = node.cloneNode(false);
+ var html = goog.dom.getOuterHtml(shallow);
+ if(goog.userAgent.IE && node.tagName == goog.dom.TagName.LI) {
+ output.append(html)
+ }else {
+ var index = html.lastIndexOf("<");
+ output.append(index ? html.substr(0, index) : html)
+ }
+ }
+ }
+ }
+ }, this);
+ return output.toString()
+};
+goog.dom.browserrange.AbstractRange.prototype.getValidHtml = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.__iterator__ = function(opt_keys) {
+ return new goog.dom.TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};
+goog.dom.browserrange.AbstractRange.prototype.select = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.removeContents = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.surroundContents = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.insertNode = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod;
+goog.dom.browserrange.AbstractRange.prototype.collapse = goog.abstractMethod;
+goog.provide("goog.dom.browserrange.W3cRange");
+goog.require("goog.dom");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.RangeEndpoint");
+goog.require("goog.dom.browserrange.AbstractRange");
+goog.require("goog.string");
+goog.dom.browserrange.W3cRange = function(range) {
+ this.range_ = range
+};
+goog.inherits(goog.dom.browserrange.W3cRange, goog.dom.browserrange.AbstractRange);
+goog.dom.browserrange.W3cRange.getBrowserRangeForNode = function(node) {
+ var nodeRange = goog.dom.getOwnerDocument(node).createRange();
+ if(node.nodeType == goog.dom.NodeType.TEXT) {
+ nodeRange.setStart(node, 0);
+ nodeRange.setEnd(node, node.length)
+ }else {
+ if(!goog.dom.browserrange.canContainRangeEndpoint(node)) {
+ var rangeParent = node.parentNode;
+ var rangeStartOffset = goog.array.indexOf(rangeParent.childNodes, node);
+ nodeRange.setStart(rangeParent, rangeStartOffset);
+ nodeRange.setEnd(rangeParent, rangeStartOffset + 1)
+ }else {
+ var tempNode, leaf = node;
+ while((tempNode = leaf.firstChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
+ leaf = tempNode
+ }
+ nodeRange.setStart(leaf, 0);
+ leaf = node;
+ while((tempNode = leaf.lastChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
+ leaf = tempNode
+ }
+ nodeRange.setEnd(leaf, leaf.nodeType == goog.dom.NodeType.ELEMENT ? leaf.childNodes.length : leaf.length)
+ }
+ }
+ return nodeRange
+};
+goog.dom.browserrange.W3cRange.getBrowserRangeForNodes = function(startNode, startOffset, endNode, endOffset) {
+ var nodeRange = goog.dom.getOwnerDocument(startNode).createRange();
+ nodeRange.setStart(startNode, startOffset);
+ nodeRange.setEnd(endNode, endOffset);
+ return nodeRange
+};
+goog.dom.browserrange.W3cRange.createFromNodeContents = function(node) {
+ return new goog.dom.browserrange.W3cRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node))
+};
+goog.dom.browserrange.W3cRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return new goog.dom.browserrange.W3cRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset))
+};
+goog.dom.browserrange.W3cRange.prototype.clone = function() {
+ return new this.constructor(this.range_.cloneRange())
+};
+goog.dom.browserrange.W3cRange.prototype.getBrowserRange = function() {
+ return this.range_
+};
+goog.dom.browserrange.W3cRange.prototype.getContainer = function() {
+ return this.range_.commonAncestorContainer
+};
+goog.dom.browserrange.W3cRange.prototype.getStartNode = function() {
+ return this.range_.startContainer
+};
+goog.dom.browserrange.W3cRange.prototype.getStartOffset = function() {
+ return this.range_.startOffset
+};
+goog.dom.browserrange.W3cRange.prototype.getEndNode = function() {
+ return this.range_.endContainer
+};
+goog.dom.browserrange.W3cRange.prototype.getEndOffset = function() {
+ return this.range_.endOffset
+};
+goog.dom.browserrange.W3cRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ return this.range_.compareBoundaryPoints(otherEndpoint == goog.dom.RangeEndpoint.START ? thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_START : goog.global["Range"].START_TO_END : thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].END_TO_START : goog.global["Range"].END_TO_END, range)
+};
+goog.dom.browserrange.W3cRange.prototype.isCollapsed = function() {
+ return this.range_.collapsed
+};
+goog.dom.browserrange.W3cRange.prototype.getText = function() {
+ return this.range_.toString()
+};
+goog.dom.browserrange.W3cRange.prototype.getValidHtml = function() {
+ var div = goog.dom.getDomHelper(this.range_.startContainer).createDom("div");
+ div.appendChild(this.range_.cloneContents());
+ var result = div.innerHTML;
+ if(goog.string.startsWith(result, "<") || !this.isCollapsed() && !goog.string.contains(result, "<")) {
+ return result
+ }
+ var container = this.getContainer();
+ container = container.nodeType == goog.dom.NodeType.ELEMENT ? container : container.parentNode;
+ var html = goog.dom.getOuterHtml(container.cloneNode(false));
+ return html.replace(">", ">" + result)
+};
+goog.dom.browserrange.W3cRange.prototype.select = function(reverse) {
+ var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));
+ this.selectInternal(win.getSelection(), reverse)
+};
+goog.dom.browserrange.W3cRange.prototype.selectInternal = function(selection, reverse) {
+ selection.removeAllRanges();
+ selection.addRange(this.range_)
+};
+goog.dom.browserrange.W3cRange.prototype.removeContents = function() {
+ var range = this.range_;
+ range.extractContents();
+ if(range.startContainer.hasChildNodes()) {
+ var rangeStartContainer = range.startContainer.childNodes[range.startOffset];
+ if(rangeStartContainer) {
+ var rangePrevious = rangeStartContainer.previousSibling;
+ if(goog.dom.getRawTextContent(rangeStartContainer) == "") {
+ goog.dom.removeNode(rangeStartContainer)
+ }
+ if(rangePrevious && goog.dom.getRawTextContent(rangePrevious) == "") {
+ goog.dom.removeNode(rangePrevious)
+ }
+ }
+ }
+};
+goog.dom.browserrange.W3cRange.prototype.surroundContents = function(element) {
+ this.range_.surroundContents(element);
+ return element
+};
+goog.dom.browserrange.W3cRange.prototype.insertNode = function(node, before) {
+ var range = this.range_.cloneRange();
+ range.collapse(before);
+ range.insertNode(node);
+ range.detach();
+ return node
+};
+goog.dom.browserrange.W3cRange.prototype.surroundWithNodes = function(startNode, endNode) {
+ var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode()));
+ var selectionRange = goog.dom.Range.createFromWindow(win);
+ if(selectionRange) {
+ var sNode = selectionRange.getStartNode();
+ var eNode = selectionRange.getEndNode();
+ var sOffset = selectionRange.getStartOffset();
+ var eOffset = selectionRange.getEndOffset()
+ }
+ var clone1 = this.range_.cloneRange();
+ var clone2 = this.range_.cloneRange();
+ clone1.collapse(false);
+ clone2.collapse(true);
+ clone1.insertNode(endNode);
+ clone2.insertNode(startNode);
+ clone1.detach();
+ clone2.detach();
+ if(selectionRange) {
+ var isInsertedNode = function(n) {
+ return n == startNode || n == endNode
+ };
+ if(sNode.nodeType == goog.dom.NodeType.TEXT) {
+ while(sOffset > sNode.length) {
+ sOffset -= sNode.length;
+ do {
+ sNode = sNode.nextSibling
+ }while(isInsertedNode(sNode))
+ }
+ }
+ if(eNode.nodeType == goog.dom.NodeType.TEXT) {
+ while(eOffset > eNode.length) {
+ eOffset -= eNode.length;
+ do {
+ eNode = eNode.nextSibling
+ }while(isInsertedNode(eNode))
+ }
+ }
+ goog.dom.Range.createFromNodes(sNode, sOffset, eNode, eOffset).select()
+ }
+};
+goog.dom.browserrange.W3cRange.prototype.collapse = function(toStart) {
+ this.range_.collapse(toStart)
+};
+goog.provide("goog.dom.browserrange.GeckoRange");
+goog.require("goog.dom.browserrange.W3cRange");
+goog.dom.browserrange.GeckoRange = function(range) {
+ goog.dom.browserrange.W3cRange.call(this, range)
+};
+goog.inherits(goog.dom.browserrange.GeckoRange, goog.dom.browserrange.W3cRange);
+goog.dom.browserrange.GeckoRange.createFromNodeContents = function(node) {
+ return new goog.dom.browserrange.GeckoRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node))
+};
+goog.dom.browserrange.GeckoRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return new goog.dom.browserrange.GeckoRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset))
+};
+goog.dom.browserrange.GeckoRange.prototype.selectInternal = function(selection, reversed) {
+ var anchorNode = reversed ? this.getEndNode() : this.getStartNode();
+ var anchorOffset = reversed ? this.getEndOffset() : this.getStartOffset();
+ var focusNode = reversed ? this.getStartNode() : this.getEndNode();
+ var focusOffset = reversed ? this.getStartOffset() : this.getEndOffset();
+ selection.collapse(anchorNode, anchorOffset);
+ if(anchorNode != focusNode || anchorOffset != focusOffset) {
+ selection.extend(focusNode, focusOffset)
+ }
+};
+goog.provide("goog.dom.NodeIterator");
+goog.require("goog.dom.TagIterator");
+goog.dom.NodeIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_depth) {
+ goog.dom.TagIterator.call(this, opt_node, opt_reversed, opt_unconstrained, null, opt_depth)
+};
+goog.inherits(goog.dom.NodeIterator, goog.dom.TagIterator);
+goog.dom.NodeIterator.prototype.next = function() {
+ do {
+ goog.dom.NodeIterator.superClass_.next.call(this)
+ }while(this.isEndTag());
+ return this.node
+};
+goog.provide("goog.dom.browserrange.IeRange");
+goog.require("goog.array");
+goog.require("goog.debug.Logger");
+goog.require("goog.dom");
+goog.require("goog.dom.NodeIterator");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.RangeEndpoint");
+goog.require("goog.dom.TagName");
+goog.require("goog.dom.browserrange.AbstractRange");
+goog.require("goog.iter");
+goog.require("goog.iter.StopIteration");
+goog.require("goog.string");
+goog.dom.browserrange.IeRange = function(range, doc) {
+ this.range_ = range;
+ this.doc_ = doc
+};
+goog.inherits(goog.dom.browserrange.IeRange, goog.dom.browserrange.AbstractRange);
+goog.dom.browserrange.IeRange.logger_ = goog.debug.Logger.getLogger("goog.dom.browserrange.IeRange");
+goog.dom.browserrange.IeRange.getBrowserRangeForNode_ = function(node) {
+ var nodeRange = goog.dom.getOwnerDocument(node).body.createTextRange();
+ if(node.nodeType == goog.dom.NodeType.ELEMENT) {
+ nodeRange.moveToElementText(node);
+ if(goog.dom.browserrange.canContainRangeEndpoint(node) && !node.childNodes.length) {
+ nodeRange.collapse(false)
+ }
+ }else {
+ var offset = 0;
+ var sibling = node;
+ while(sibling = sibling.previousSibling) {
+ var nodeType = sibling.nodeType;
+ if(nodeType == goog.dom.NodeType.TEXT) {
+ offset += sibling.length
+ }else {
+ if(nodeType == goog.dom.NodeType.ELEMENT) {
+ nodeRange.moveToElementText(sibling);
+ break
+ }
+ }
+ }
+ if(!sibling) {
+ nodeRange.moveToElementText(node.parentNode)
+ }
+ nodeRange.collapse(!sibling);
+ if(offset) {
+ nodeRange.move("character", offset)
+ }
+ nodeRange.moveEnd("character", node.length)
+ }
+ return nodeRange
+};
+goog.dom.browserrange.IeRange.getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) {
+ var child, collapse = false;
+ if(startNode.nodeType == goog.dom.NodeType.ELEMENT) {
+ if(startOffset > startNode.childNodes.length) {
+ goog.dom.browserrange.IeRange.logger_.severe("Cannot have startOffset > startNode child count")
+ }
+ child = startNode.childNodes[startOffset];
+ collapse = !child;
+ startNode = child || startNode.lastChild || startNode;
+ startOffset = 0
+ }
+ var leftRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(startNode);
+ if(startOffset) {
+ leftRange.move("character", startOffset)
+ }
+ if(startNode == endNode && startOffset == endOffset) {
+ leftRange.collapse(true);
+ return leftRange
+ }
+ if(collapse) {
+ leftRange.collapse(false)
+ }
+ collapse = false;
+ if(endNode.nodeType == goog.dom.NodeType.ELEMENT) {
+ if(endOffset > endNode.childNodes.length) {
+ goog.dom.browserrange.IeRange.logger_.severe("Cannot have endOffset > endNode child count")
+ }
+ child = endNode.childNodes[endOffset];
+ endNode = child || endNode.lastChild || endNode;
+ endOffset = 0;
+ collapse = !child
+ }
+ var rightRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(endNode);
+ rightRange.collapse(!collapse);
+ if(endOffset) {
+ rightRange.moveEnd("character", endOffset)
+ }
+ leftRange.setEndPoint("EndToEnd", rightRange);
+ return leftRange
+};
+goog.dom.browserrange.IeRange.createFromNodeContents = function(node) {
+ var range = new goog.dom.browserrange.IeRange(goog.dom.browserrange.IeRange.getBrowserRangeForNode_(node), goog.dom.getOwnerDocument(node));
+ if(!goog.dom.browserrange.canContainRangeEndpoint(node)) {
+ range.startNode_ = range.endNode_ = range.parentNode_ = node.parentNode;
+ range.startOffset_ = goog.array.indexOf(range.parentNode_.childNodes, node);
+ range.endOffset_ = range.startOffset_ + 1
+ }else {
+ var tempNode, leaf = node;
+ while((tempNode = leaf.firstChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
+ leaf = tempNode
+ }
+ range.startNode_ = leaf;
+ range.startOffset_ = 0;
+ leaf = node;
+ while((tempNode = leaf.lastChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) {
+ leaf = tempNode
+ }
+ range.endNode_ = leaf;
+ range.endOffset_ = leaf.nodeType == goog.dom.NodeType.ELEMENT ? leaf.childNodes.length : leaf.length;
+ range.parentNode_ = node
+ }
+ return range
+};
+goog.dom.browserrange.IeRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ var range = new goog.dom.browserrange.IeRange(goog.dom.browserrange.IeRange.getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset), goog.dom.getOwnerDocument(startNode));
+ range.startNode_ = startNode;
+ range.startOffset_ = startOffset;
+ range.endNode_ = endNode;
+ range.endOffset_ = endOffset;
+ return range
+};
+goog.dom.browserrange.IeRange.prototype.parentNode_ = null;
+goog.dom.browserrange.IeRange.prototype.startNode_ = null;
+goog.dom.browserrange.IeRange.prototype.endNode_ = null;
+goog.dom.browserrange.IeRange.prototype.startOffset_ = -1;
+goog.dom.browserrange.IeRange.prototype.endOffset_ = -1;
+goog.dom.browserrange.IeRange.prototype.clone = function() {
+ var range = new goog.dom.browserrange.IeRange(this.range_.duplicate(), this.doc_);
+ range.parentNode_ = this.parentNode_;
+ range.startNode_ = this.startNode_;
+ range.endNode_ = this.endNode_;
+ return range
+};
+goog.dom.browserrange.IeRange.prototype.getBrowserRange = function() {
+ return this.range_
+};
+goog.dom.browserrange.IeRange.prototype.clearCachedValues_ = function() {
+ this.parentNode_ = this.startNode_ = this.endNode_ = null;
+ this.startOffset_ = this.endOffset_ = -1
+};
+goog.dom.browserrange.IeRange.prototype.getContainer = function() {
+ if(!this.parentNode_) {
+ var selectText = this.range_.text;
+ var range = this.range_.duplicate();
+ var rightTrimmedSelectText = selectText.replace(/ +$/, "");
+ var numSpacesAtEnd = selectText.length - rightTrimmedSelectText.length;
+ if(numSpacesAtEnd) {
+ range.moveEnd("character", -numSpacesAtEnd)
+ }
+ var parent = range.parentElement();
+ var htmlText = range.htmlText;
+ var htmlTextLen = goog.string.stripNewlines(htmlText).length;
+ if(this.isCollapsed() && htmlTextLen > 0) {
+ return this.parentNode_ = parent
+ }
+ while(htmlTextLen > goog.string.stripNewlines(parent.outerHTML).length) {
+ parent = parent.parentNode
+ }
+ while(parent.childNodes.length == 1 && parent.innerText == goog.dom.browserrange.IeRange.getNodeText_(parent.firstChild)) {
+ if(!goog.dom.browserrange.canContainRangeEndpoint(parent.firstChild)) {
+ break
+ }
+ parent = parent.firstChild
+ }
+ if(selectText.length == 0) {
+ parent = this.findDeepestContainer_(parent)
+ }
+ this.parentNode_ = parent
+ }
+ return this.parentNode_
+};
+goog.dom.browserrange.IeRange.prototype.findDeepestContainer_ = function(node) {
+ var childNodes = node.childNodes;
+ for(var i = 0, len = childNodes.length;i < len;i++) {
+ var child = childNodes[i];
+ if(goog.dom.browserrange.canContainRangeEndpoint(child)) {
+ var childRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(child);
+ var start = goog.dom.RangeEndpoint.START;
+ var end = goog.dom.RangeEndpoint.END;
+ var isChildRangeErratic = childRange.htmlText != child.outerHTML;
+ var isNativeInRangeErratic = this.isCollapsed() && isChildRangeErratic;
+ var inChildRange = isNativeInRangeErratic ? this.compareBrowserRangeEndpoints(childRange, start, start) >= 0 && this.compareBrowserRangeEndpoints(childRange, start, end) <= 0 : this.range_.inRange(childRange);
+ if(inChildRange) {
+ return this.findDeepestContainer_(child)
+ }
+ }
+ }
+ return node
+};
+goog.dom.browserrange.IeRange.prototype.getStartNode = function() {
+ if(!this.startNode_) {
+ this.startNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.START);
+ if(this.isCollapsed()) {
+ this.endNode_ = this.startNode_
+ }
+ }
+ return this.startNode_
+};
+goog.dom.browserrange.IeRange.prototype.getStartOffset = function() {
+ if(this.startOffset_ < 0) {
+ this.startOffset_ = this.getOffset_(goog.dom.RangeEndpoint.START);
+ if(this.isCollapsed()) {
+ this.endOffset_ = this.startOffset_
+ }
+ }
+ return this.startOffset_
+};
+goog.dom.browserrange.IeRange.prototype.getEndNode = function() {
+ if(this.isCollapsed()) {
+ return this.getStartNode()
+ }
+ if(!this.endNode_) {
+ this.endNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.END)
+ }
+ return this.endNode_
+};
+goog.dom.browserrange.IeRange.prototype.getEndOffset = function() {
+ if(this.isCollapsed()) {
+ return this.getStartOffset()
+ }
+ if(this.endOffset_ < 0) {
+ this.endOffset_ = this.getOffset_(goog.dom.RangeEndpoint.END);
+ if(this.isCollapsed()) {
+ this.startOffset_ = this.endOffset_
+ }
+ }
+ return this.endOffset_
+};
+goog.dom.browserrange.IeRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ return this.range_.compareEndPoints((thisEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End") + "To" + (otherEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End"), range)
+};
+goog.dom.browserrange.IeRange.prototype.getEndpointNode_ = function(endpoint, opt_node) {
+ var node = opt_node || this.getContainer();
+ if(!node || !node.firstChild) {
+ return node
+ }
+ var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END;
+ var isStartEndpoint = endpoint == start;
+ for(var j = 0, length = node.childNodes.length;j < length;j++) {
+ var i = isStartEndpoint ? j : length - j - 1;
+ var child = node.childNodes[i];
+ var childRange;
+ try {
+ childRange = goog.dom.browserrange.createRangeFromNodeContents(child)
+ }catch(e) {
+ continue
+ }
+ var ieRange = childRange.getBrowserRange();
+ if(this.isCollapsed()) {
+ if(!goog.dom.browserrange.canContainRangeEndpoint(child)) {
+ if(this.compareBrowserRangeEndpoints(ieRange, start, start) == 0) {
+ this.startOffset_ = this.endOffset_ = i;
+ return node
+ }
+ }else {
+ if(childRange.containsRange(this)) {
+ return this.getEndpointNode_(endpoint, child)
+ }
+ }
+ }else {
+ if(this.containsRange(childRange)) {
+ if(!goog.dom.browserrange.canContainRangeEndpoint(child)) {
+ if(isStartEndpoint) {
+ this.startOffset_ = i
+ }else {
+ this.endOffset_ = i + 1
+ }
+ return node
+ }
+ return this.getEndpointNode_(endpoint, child)
+ }else {
+ if(this.compareBrowserRangeEndpoints(ieRange, start, end) < 0 && this.compareBrowserRangeEndpoints(ieRange, end, start) > 0) {
+ return this.getEndpointNode_(endpoint, child)
+ }
+ }
+ }
+ }
+ return node
+};
+goog.dom.browserrange.IeRange.prototype.compareNodeEndpoints_ = function(node, thisEndpoint, otherEndpoint) {
+ return this.range_.compareEndPoints((thisEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End") + "To" + (otherEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End"), goog.dom.browserrange.createRangeFromNodeContents(node).getBrowserRange())
+};
+goog.dom.browserrange.IeRange.prototype.getOffset_ = function(endpoint, opt_container) {
+ var isStartEndpoint = endpoint == goog.dom.RangeEndpoint.START;
+ var container = opt_container || (isStartEndpoint ? this.getStartNode() : this.getEndNode());
+ if(container.nodeType == goog.dom.NodeType.ELEMENT) {
+ var children = container.childNodes;
+ var len = children.length;
+ var edge = isStartEndpoint ? 0 : len - 1;
+ var sign = isStartEndpoint ? 1 : -1;
+ for(var i = edge;i >= 0 && i < len;i += sign) {
+ var child = children[i];
+ if(goog.dom.browserrange.canContainRangeEndpoint(child)) {
+ continue
+ }
+ var endPointCompare = this.compareNodeEndpoints_(child, endpoint, endpoint);
+ if(endPointCompare == 0) {
+ return isStartEndpoint ? i : i + 1
+ }
+ }
+ return i == -1 ? 0 : i
+ }else {
+ var range = this.range_.duplicate();
+ var nodeRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(container);
+ range.setEndPoint(isStartEndpoint ? "EndToEnd" : "StartToStart", nodeRange);
+ var rangeLength = range.text.length;
+ return isStartEndpoint ? container.length - rangeLength : rangeLength
+ }
+};
+goog.dom.browserrange.IeRange.getNodeText_ = function(node) {
+ return node.nodeType == goog.dom.NodeType.TEXT ? node.nodeValue : node.innerText
+};
+goog.dom.browserrange.IeRange.prototype.isRangeInDocument = function() {
+ var range = this.doc_.body.createTextRange();
+ range.moveToElementText(this.doc_.body);
+ return this.containsRange(new goog.dom.browserrange.IeRange(range, this.doc_), true)
+};
+goog.dom.browserrange.IeRange.prototype.isCollapsed = function() {
+ return this.range_.compareEndPoints("StartToEnd", this.range_) == 0
+};
+goog.dom.browserrange.IeRange.prototype.getText = function() {
+ return this.range_.text
+};
+goog.dom.browserrange.IeRange.prototype.getValidHtml = function() {
+ return this.range_.htmlText
+};
+goog.dom.browserrange.IeRange.prototype.select = function(opt_reverse) {
+ this.range_.select()
+};
+goog.dom.browserrange.IeRange.prototype.removeContents = function() {
+ if(this.range_.htmlText) {
+ var startNode = this.getStartNode();
+ var endNode = this.getEndNode();
+ var oldText = this.range_.text;
+ var clone = this.range_.duplicate();
+ clone.moveStart("character", 1);
+ clone.moveStart("character", -1);
+ if(clone.text != oldText) {
+ var iter = new goog.dom.NodeIterator(startNode, false, true);
+ var toDelete = [];
+ goog.iter.forEach(iter, function(node) {
+ if(node.nodeType != goog.dom.NodeType.TEXT && this.containsNode(node)) {
+ toDelete.push(node);
+ iter.skipTag()
+ }
+ if(node == endNode) {
+ throw goog.iter.StopIteration;
+ }
+ });
+ this.collapse(true);
+ goog.array.forEach(toDelete, goog.dom.removeNode);
+ this.clearCachedValues_();
+ return
+ }
+ this.range_ = clone;
+ this.range_.text = "";
+ this.clearCachedValues_();
+ var newStartNode = this.getStartNode();
+ var newStartOffset = this.getStartOffset();
+ try {
+ var sibling = startNode.nextSibling;
+ if(startNode == endNode && startNode.parentNode && startNode.nodeType == goog.dom.NodeType.TEXT && sibling && sibling.nodeType == goog.dom.NodeType.TEXT) {
+ startNode.nodeValue += sibling.nodeValue;
+ goog.dom.removeNode(sibling);
+ this.range_ = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(newStartNode);
+ this.range_.move("character", newStartOffset);
+ this.clearCachedValues_()
+ }
+ }catch(e) {
+ }
+ }
+};
+goog.dom.browserrange.IeRange.getDomHelper_ = function(range) {
+ return goog.dom.getDomHelper(range.parentElement())
+};
+goog.dom.browserrange.IeRange.pasteElement_ = function(range, element, opt_domHelper) {
+ opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(range);
+ var id;
+ var originalId = id = element.id;
+ if(!id) {
+ id = element.id = goog.string.createUniqueString()
+ }
+ range.pasteHTML(element.outerHTML);
+ element = opt_domHelper.getElement(id);
+ if(element) {
+ if(!originalId) {
+ element.removeAttribute("id")
+ }
+ }
+ return element
+};
+goog.dom.browserrange.IeRange.prototype.surroundContents = function(element) {
+ goog.dom.removeNode(element);
+ element.innerHTML = this.range_.htmlText;
+ element = goog.dom.browserrange.IeRange.pasteElement_(this.range_, element);
+ if(element) {
+ this.range_.moveToElementText(element)
+ }
+ this.clearCachedValues_();
+ return element
+};
+goog.dom.browserrange.IeRange.insertNode_ = function(clone, node, before, opt_domHelper) {
+ opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(clone);
+ var isNonElement;
+ if(node.nodeType != goog.dom.NodeType.ELEMENT) {
+ isNonElement = true;
+ node = opt_domHelper.createDom(goog.dom.TagName.DIV, null, node)
+ }
+ clone.collapse(before);
+ node = goog.dom.browserrange.IeRange.pasteElement_(clone, node, opt_domHelper);
+ if(isNonElement) {
+ var newNonElement = node.firstChild;
+ opt_domHelper.flattenElement(node);
+ node = newNonElement
+ }
+ return node
+};
+goog.dom.browserrange.IeRange.prototype.insertNode = function(node, before) {
+ var output = goog.dom.browserrange.IeRange.insertNode_(this.range_.duplicate(), node, before);
+ this.clearCachedValues_();
+ return output
+};
+goog.dom.browserrange.IeRange.prototype.surroundWithNodes = function(startNode, endNode) {
+ var clone1 = this.range_.duplicate();
+ var clone2 = this.range_.duplicate();
+ goog.dom.browserrange.IeRange.insertNode_(clone1, startNode, true);
+ goog.dom.browserrange.IeRange.insertNode_(clone2, endNode, false);
+ this.clearCachedValues_()
+};
+goog.dom.browserrange.IeRange.prototype.collapse = function(toStart) {
+ this.range_.collapse(toStart);
+ if(toStart) {
+ this.endNode_ = this.startNode_;
+ this.endOffset_ = this.startOffset_
+ }else {
+ this.startNode_ = this.endNode_;
+ this.startOffset_ = this.endOffset_
+ }
+};
+goog.provide("goog.dom.browserrange.OperaRange");
+goog.require("goog.dom.browserrange.W3cRange");
+goog.dom.browserrange.OperaRange = function(range) {
+ goog.dom.browserrange.W3cRange.call(this, range)
+};
+goog.inherits(goog.dom.browserrange.OperaRange, goog.dom.browserrange.W3cRange);
+goog.dom.browserrange.OperaRange.createFromNodeContents = function(node) {
+ return new goog.dom.browserrange.OperaRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node))
+};
+goog.dom.browserrange.OperaRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return new goog.dom.browserrange.OperaRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset))
+};
+goog.dom.browserrange.OperaRange.prototype.selectInternal = function(selection, reversed) {
+ selection.collapse(this.getStartNode(), this.getStartOffset());
+ if(this.getEndNode() != this.getStartNode() || this.getEndOffset() != this.getStartOffset()) {
+ selection.extend(this.getEndNode(), this.getEndOffset())
+ }
+ if(selection.rangeCount == 0) {
+ selection.addRange(this.range_)
+ }
+};
+goog.provide("goog.dom.browserrange.WebKitRange");
+goog.require("goog.dom.RangeEndpoint");
+goog.require("goog.dom.browserrange.W3cRange");
+goog.require("goog.userAgent");
+goog.dom.browserrange.WebKitRange = function(range) {
+ goog.dom.browserrange.W3cRange.call(this, range)
+};
+goog.inherits(goog.dom.browserrange.WebKitRange, goog.dom.browserrange.W3cRange);
+goog.dom.browserrange.WebKitRange.createFromNodeContents = function(node) {
+ return new goog.dom.browserrange.WebKitRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node))
+};
+goog.dom.browserrange.WebKitRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return new goog.dom.browserrange.WebKitRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset))
+};
+goog.dom.browserrange.WebKitRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+ if(goog.userAgent.isVersion("528")) {
+ return goog.dom.browserrange.WebKitRange.superClass_.compareBrowserRangeEndpoints.call(this, range, thisEndpoint, otherEndpoint)
+ }
+ return this.range_.compareBoundaryPoints(otherEndpoint == goog.dom.RangeEndpoint.START ? thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_START : goog.global["Range"].END_TO_START : thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_END : goog.global["Range"].END_TO_END, range)
+};
+goog.dom.browserrange.WebKitRange.prototype.selectInternal = function(selection, reversed) {
+ selection.removeAllRanges();
+ if(reversed) {
+ selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(), this.getStartNode(), this.getStartOffset())
+ }else {
+ selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+ }
+};
+goog.provide("goog.dom.browserrange");
+goog.provide("goog.dom.browserrange.Error");
+goog.require("goog.dom");
+goog.require("goog.dom.browserrange.GeckoRange");
+goog.require("goog.dom.browserrange.IeRange");
+goog.require("goog.dom.browserrange.OperaRange");
+goog.require("goog.dom.browserrange.W3cRange");
+goog.require("goog.dom.browserrange.WebKitRange");
+goog.require("goog.userAgent");
+goog.dom.browserrange.Error = {NOT_IMPLEMENTED:"Not Implemented"};
+goog.dom.browserrange.createRange = function(range) {
+ if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) {
+ return new goog.dom.browserrange.IeRange(range, goog.dom.getOwnerDocument(range.parentElement()))
+ }else {
+ if(goog.userAgent.WEBKIT) {
+ return new goog.dom.browserrange.WebKitRange(range)
+ }else {
+ if(goog.userAgent.GECKO) {
+ return new goog.dom.browserrange.GeckoRange(range)
+ }else {
+ if(goog.userAgent.OPERA) {
+ return new goog.dom.browserrange.OperaRange(range)
+ }else {
+ return new goog.dom.browserrange.W3cRange(range)
+ }
+ }
+ }
+ }
+};
+goog.dom.browserrange.createRangeFromNodeContents = function(node) {
+ if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) {
+ return goog.dom.browserrange.IeRange.createFromNodeContents(node)
+ }else {
+ if(goog.userAgent.WEBKIT) {
+ return goog.dom.browserrange.WebKitRange.createFromNodeContents(node)
+ }else {
+ if(goog.userAgent.GECKO) {
+ return goog.dom.browserrange.GeckoRange.createFromNodeContents(node)
+ }else {
+ if(goog.userAgent.OPERA) {
+ return goog.dom.browserrange.OperaRange.createFromNodeContents(node)
+ }else {
+ return goog.dom.browserrange.W3cRange.createFromNodeContents(node)
+ }
+ }
+ }
+ }
+};
+goog.dom.browserrange.createRangeFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) {
+ return goog.dom.browserrange.IeRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }else {
+ if(goog.userAgent.WEBKIT) {
+ return goog.dom.browserrange.WebKitRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }else {
+ if(goog.userAgent.GECKO) {
+ return goog.dom.browserrange.GeckoRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }else {
+ if(goog.userAgent.OPERA) {
+ return goog.dom.browserrange.OperaRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }else {
+ return goog.dom.browserrange.W3cRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+ }
+ }
+ }
+ }
+};
+goog.dom.browserrange.canContainRangeEndpoint = function(node) {
+ return goog.dom.canHaveChildren(node) || node.nodeType == goog.dom.NodeType.TEXT
+};
+goog.provide("goog.dom.TextRange");
+goog.require("goog.array");
+goog.require("goog.dom");
+goog.require("goog.dom.AbstractRange");
+goog.require("goog.dom.RangeType");
+goog.require("goog.dom.SavedRange");
+goog.require("goog.dom.TagName");
+goog.require("goog.dom.TextRangeIterator");
+goog.require("goog.dom.browserrange");
+goog.require("goog.string");
+goog.require("goog.userAgent");
+goog.dom.TextRange = function() {
+};
+goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange);
+goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) {
+ return goog.dom.TextRange.createFromBrowserRangeWrapper_(goog.dom.browserrange.createRange(range), opt_isReversed)
+};
+goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(browserRange, opt_isReversed) {
+ var range = new goog.dom.TextRange;
+ range.browserRangeWrapper_ = browserRange;
+ range.isReversed_ = !!opt_isReversed;
+ return range
+};
+goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) {
+ return goog.dom.TextRange.createFromBrowserRangeWrapper_(goog.dom.browserrange.createRangeFromNodeContents(node), opt_isReversed)
+};
+goog.dom.TextRange.createFromNodes = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+ var range = new goog.dom.TextRange;
+ range.isReversed_ = goog.dom.Range.isReversed(anchorNode, anchorOffset, focusNode, focusOffset);
+ if(anchorNode.tagName == "BR") {
+ var parent = anchorNode.parentNode;
+ anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode);
+ anchorNode = parent
+ }
+ if(focusNode.tagName == "BR") {
+ var parent = focusNode.parentNode;
+ focusOffset = goog.array.indexOf(parent.childNodes, focusNode);
+ focusNode = parent
+ }
+ if(range.isReversed_) {
+ range.startNode_ = focusNode;
+ range.startOffset_ = focusOffset;
+ range.endNode_ = anchorNode;
+ range.endOffset_ = anchorOffset
+ }else {
+ range.startNode_ = anchorNode;
+ range.startOffset_ = anchorOffset;
+ range.endNode_ = focusNode;
+ range.endOffset_ = focusOffset
+ }
+ return range
+};
+goog.dom.TextRange.prototype.browserRangeWrapper_ = null;
+goog.dom.TextRange.prototype.startNode_ = null;
+goog.dom.TextRange.prototype.startOffset_ = null;
+goog.dom.TextRange.prototype.endNode_ = null;
+goog.dom.TextRange.prototype.endOffset_ = null;
+goog.dom.TextRange.prototype.isReversed_ = false;
+goog.dom.TextRange.prototype.clone = function() {
+ var range = new goog.dom.TextRange;
+ range.browserRangeWrapper_ = this.browserRangeWrapper_;
+ range.startNode_ = this.startNode_;
+ range.startOffset_ = this.startOffset_;
+ range.endNode_ = this.endNode_;
+ range.endOffset_ = this.endOffset_;
+ range.isReversed_ = this.isReversed_;
+ return range
+};
+goog.dom.TextRange.prototype.getType = function() {
+ return goog.dom.RangeType.TEXT
+};
+goog.dom.TextRange.prototype.getBrowserRangeObject = function() {
+ return this.getBrowserRangeWrapper_().getBrowserRange()
+};
+goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) {
+ if(goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
+ return false
+ }
+ this.browserRangeWrapper_ = goog.dom.browserrange.createRange(nativeRange);
+ this.clearCachedValues_();
+ return true
+};
+goog.dom.TextRange.prototype.clearCachedValues_ = function() {
+ this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null
+};
+goog.dom.TextRange.prototype.getTextRangeCount = function() {
+ return 1
+};
+goog.dom.TextRange.prototype.getTextRange = function(i) {
+ return this
+};
+goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() {
+ return this.browserRangeWrapper_ || (this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()))
+};
+goog.dom.TextRange.prototype.getContainer = function() {
+ return this.getBrowserRangeWrapper_().getContainer()
+};
+goog.dom.TextRange.prototype.getStartNode = function() {
+ return this.startNode_ || (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode())
+};
+goog.dom.TextRange.prototype.getStartOffset = function() {
+ return this.startOffset_ != null ? this.startOffset_ : this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset()
+};
+goog.dom.TextRange.prototype.getEndNode = function() {
+ return this.endNode_ || (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode())
+};
+goog.dom.TextRange.prototype.getEndOffset = function() {
+ return this.endOffset_ != null ? this.endOffset_ : this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset()
+};
+goog.dom.TextRange.prototype.moveToNodes = function(startNode, startOffset, endNode, endOffset, isReversed) {
+ this.startNode_ = startNode;
+ this.startOffset_ = startOffset;
+ this.endNode_ = endNode;
+ this.endOffset_ = endOffset;
+ this.isReversed_ = isReversed;
+ this.browserRangeWrapper_ = null
+};
+goog.dom.TextRange.prototype.isReversed = function() {
+ return this.isReversed_
+};
+goog.dom.TextRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+ var otherRangeType = otherRange.getType();
+ if(otherRangeType == goog.dom.RangeType.TEXT) {
+ return this.getBrowserRangeWrapper_().containsRange(otherRange.getBrowserRangeWrapper_(), opt_allowPartial)
+ }else {
+ if(otherRangeType == goog.dom.RangeType.CONTROL) {
+ var elements = otherRange.getElements();
+ var fn = opt_allowPartial ? goog.array.some : goog.array.every;
+ return fn(elements, function(el) {
+ return this.containsNode(el, opt_allowPartial)
+ }, this)
+ }
+ }
+ return false
+};
+goog.dom.TextRange.isAttachedNode = function(node) {
+ if(goog.userAgent.IE) {
+ var returnValue = false;
+ try {
+ returnValue = node.parentNode
+ }catch(e) {
+ }
+ return!!returnValue
+ }else {
+ return goog.dom.contains(node.ownerDocument.body, node)
+ }
+};
+goog.dom.TextRange.prototype.isRangeInDocument = function() {
+ return(!this.startNode_ || goog.dom.TextRange.isAttachedNode(this.startNode_)) && (!this.endNode_ || goog.dom.TextRange.isAttachedNode(this.endNode_)) && (!goog.userAgent.IE || this.getBrowserRangeWrapper_().isRangeInDocument())
+};
+goog.dom.TextRange.prototype.isCollapsed = function() {
+ return this.getBrowserRangeWrapper_().isCollapsed()
+};
+goog.dom.TextRange.prototype.getText = function() {
+ return this.getBrowserRangeWrapper_().getText()
+};
+goog.dom.TextRange.prototype.getHtmlFragment = function() {
+ return this.getBrowserRangeWrapper_().getHtmlFragment()
+};
+goog.dom.TextRange.prototype.getValidHtml = function() {
+ return this.getBrowserRangeWrapper_().getValidHtml()
+};
+goog.dom.TextRange.prototype.getPastableHtml = function() {
+ var html = this.getValidHtml();
+ if(html.match(/^\s*<td\b/i)) {
+ html = "<table><tbody><tr>" + html + "</tr></tbody></table>"
+ }else {
+ if(html.match(/^\s*<tr\b/i)) {
+ html = "<table><tbody>" + html + "</tbody></table>"
+ }else {
+ if(html.match(/^\s*<tbody\b/i)) {
+ html = "<table>" + html + "</table>"
+ }else {
+ if(html.match(/^\s*<li\b/i)) {
+ var container = this.getContainer();
+ var tagType = goog.dom.TagName.UL;
+ while(container) {
+ if(container.tagName == goog.dom.TagName.OL) {
+ tagType = goog.dom.TagName.OL;
+ break
+ }else {
+ if(container.tagName == goog.dom.TagName.UL) {
+ break
+ }
+ }
+ container = container.parentNode
+ }
+ html = goog.string.buildString("<", tagType, ">", html, "</", tagType, ">")
+ }
+ }
+ }
+ }
+ return html
+};
+goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) {
+ return new goog.dom.TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};
+goog.dom.TextRange.prototype.select = function() {
+ this.getBrowserRangeWrapper_().select(this.isReversed_)
+};
+goog.dom.TextRange.prototype.removeContents = function() {
+ this.getBrowserRangeWrapper_().removeContents();
+ this.clearCachedValues_()
+};
+goog.dom.TextRange.prototype.surroundContents = function(element) {
+ var output = this.getBrowserRangeWrapper_().surroundContents(element);
+ this.clearCachedValues_();
+ return output
+};
+goog.dom.TextRange.prototype.insertNode = function(node, before) {
+ var output = this.getBrowserRangeWrapper_().insertNode(node, before);
+ this.clearCachedValues_();
+ return output
+};
+goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) {
+ this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode);
+ this.clearCachedValues_()
+};
+goog.dom.TextRange.prototype.saveUsingDom = function() {
+ return new goog.dom.DomSavedTextRange_(this)
+};
+goog.dom.TextRange.prototype.collapse = function(toAnchor) {
+ var toStart = this.isReversed() ? !toAnchor : toAnchor;
+ if(this.browserRangeWrapper_) {
+ this.browserRangeWrapper_.collapse(toStart)
+ }
+ if(toStart) {
+ this.endNode_ = this.startNode_;
+ this.endOffset_ = this.startOffset_
+ }else {
+ this.startNode_ = this.endNode_;
+ this.startOffset_ = this.endOffset_
+ }
+ this.isReversed_ = false
+};
+goog.dom.DomSavedTextRange_ = function(range) {
+ this.anchorNode_ = range.getAnchorNode();
+ this.anchorOffset_ = range.getAnchorOffset();
+ this.focusNode_ = range.getFocusNode();
+ this.focusOffset_ = range.getFocusOffset()
+};
+goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange);
+goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() {
+ return goog.dom.Range.createFromNodes(this.anchorNode_, this.anchorOffset_, this.focusNode_, this.focusOffset_)
+};
+goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() {
+ goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this);
+ this.anchorNode_ = null;
+ this.focusNode_ = null
+};
+goog.provide("goog.dom.ControlRange");
+goog.provide("goog.dom.ControlRangeIterator");
+goog.require("goog.array");
+goog.require("goog.dom");
+goog.require("goog.dom.AbstractMultiRange");
+goog.require("goog.dom.AbstractRange");
+goog.require("goog.dom.RangeIterator");
+goog.require("goog.dom.RangeType");
+goog.require("goog.dom.SavedRange");
+goog.require("goog.dom.TagWalkType");
+goog.require("goog.dom.TextRange");
+goog.require("goog.iter.StopIteration");
+goog.require("goog.userAgent");
+goog.dom.ControlRange = function() {
+};
+goog.inherits(goog.dom.ControlRange, goog.dom.AbstractMultiRange);
+goog.dom.ControlRange.createFromBrowserRange = function(controlRange) {
+ var range = new goog.dom.ControlRange;
+ range.range_ = controlRange;
+ return range
+};
+goog.dom.ControlRange.createFromElements = function(var_args) {
+ var range = goog.dom.getOwnerDocument(arguments[0]).body.createControlRange();
+ for(var i = 0, len = arguments.length;i < len;i++) {
+ range.addElement(arguments[i])
+ }
+ return goog.dom.ControlRange.createFromBrowserRange(range)
+};
+goog.dom.ControlRange.prototype.range_ = null;
+goog.dom.ControlRange.prototype.elements_ = null;
+goog.dom.ControlRange.prototype.sortedElements_ = null;
+goog.dom.ControlRange.prototype.clearCachedValues_ = function() {
+ this.elements_ = null;
+ this.sortedElements_ = null
+};
+goog.dom.ControlRange.prototype.clone = function() {
+ return goog.dom.ControlRange.createFromElements.apply(this, this.getElements())
+};
+goog.dom.ControlRange.prototype.getType = function() {
+ return goog.dom.RangeType.CONTROL
+};
+goog.dom.ControlRange.prototype.getBrowserRangeObject = function() {
+ return this.range_ || document.body.createControlRange()
+};
+goog.dom.ControlRange.prototype.setBrowserRangeObject = function(nativeRange) {
+ if(!goog.dom.AbstractRange.isNativeControlRange(nativeRange)) {
+ return false
+ }
+ this.range_ = nativeRange;
+ return true
+};
+goog.dom.ControlRange.prototype.getTextRangeCount = function() {
+ return this.range_ ? this.range_.length : 0
+};
+goog.dom.ControlRange.prototype.getTextRange = function(i) {
+ return goog.dom.TextRange.createFromNodeContents(this.range_.item(i))
+};
+goog.dom.ControlRange.prototype.getContainer = function() {
+ return goog.dom.findCommonAncestor.apply(null, this.getElements())
+};
+goog.dom.ControlRange.prototype.getStartNode = function() {
+ return this.getSortedElements()[0]
+};
+goog.dom.ControlRange.prototype.getStartOffset = function() {
+ return 0
+};
+goog.dom.ControlRange.prototype.getEndNode = function() {
+ var sorted = this.getSortedElements();
+ var startsLast = goog.array.peek(sorted);
+ return goog.array.find(sorted, function(el) {
+ return goog.dom.contains(el, startsLast)
+ })
+};
+goog.dom.ControlRange.prototype.getEndOffset = function() {
+ return this.getEndNode().childNodes.length
+};
+goog.dom.ControlRange.prototype.getElements = function() {
+ if(!this.elements_) {
+ this.elements_ = [];
+ if(this.range_) {
+ for(var i = 0;i < this.range_.length;i++) {
+ this.elements_.push(this.range_.item(i))
+ }
+ }
+ }
+ return this.elements_
+};
+goog.dom.ControlRange.prototype.getSortedElements = function() {
+ if(!this.sortedElements_) {
+ this.sortedElements_ = this.getElements().concat();
+ this.sortedElements_.sort(function(a, b) {
+ return a.sourceIndex - b.sourceIndex
+ })
+ }
+ return this.sortedElements_
+};
+goog.dom.ControlRange.prototype.isRangeInDocument = function() {
+ var returnValue = false;
+ try {
+ returnValue = goog.array.every(this.getElements(), function(element) {
+ return goog.userAgent.IE ? element.parentNode : goog.dom.contains(element.ownerDocument.body, element)
+ })
+ }catch(e) {
+ }
+ return returnValue
+};
+goog.dom.ControlRange.prototype.isCollapsed = function() {
+ return!this.range_ || !this.range_.length
+};
+goog.dom.ControlRange.prototype.getText = function() {
+ return""
+};
+goog.dom.ControlRange.prototype.getHtmlFragment = function() {
+ return goog.array.map(this.getSortedElements(), goog.dom.getOuterHtml).join("")
+};
+goog.dom.ControlRange.prototype.getValidHtml = function() {
+ return this.getHtmlFragment()
+};
+goog.dom.ControlRange.prototype.getPastableHtml = goog.dom.ControlRange.prototype.getValidHtml;
+goog.dom.ControlRange.prototype.__iterator__ = function(opt_keys) {
+ return new goog.dom.ControlRangeIterator(this)
+};
+goog.dom.ControlRange.prototype.select = function() {
+ if(this.range_) {
+ this.range_.select()
+ }
+};
+goog.dom.ControlRange.prototype.removeContents = function() {
+ if(this.range_) {
+ var nodes = [];
+ for(var i = 0, len = this.range_.length;i < len;i++) {
+ nodes.push(this.range_.item(i))
+ }
+ goog.array.forEach(nodes, goog.dom.removeNode);
+ this.collapse(false)
+ }
+};
+goog.dom.ControlRange.prototype.replaceContentsWithNode = function(node) {
+ var result = this.insertNode(node, true);
+ if(!this.isCollapsed()) {
+ this.removeContents()
+ }
+ return result
+};
+goog.dom.ControlRange.prototype.saveUsingDom = function() {
+ return new goog.dom.DomSavedControlRange_(this)
+};
+goog.dom.ControlRange.prototype.collapse = function(toAnchor) {
+ this.range_ = null;
+ this.clearCachedValues_()
+};
+goog.dom.DomSavedControlRange_ = function(range) {
+ this.elements_ = range.getElements()
+};
+goog.inherits(goog.dom.DomSavedControlRange_, goog.dom.SavedRange);
+goog.dom.DomSavedControlRange_.prototype.restoreInternal = function() {
+ var doc = this.elements_.length ? goog.dom.getOwnerDocument(this.elements_[0]) : document;
+ var controlRange = doc.body.createControlRange();
+ for(var i = 0, len = this.elements_.length;i < len;i++) {
+ controlRange.addElement(this.elements_[i])
+ }
+ return goog.dom.ControlRange.createFromBrowserRange(controlRange)
+};
+goog.dom.DomSavedControlRange_.prototype.disposeInternal = function() {
+ goog.dom.DomSavedControlRange_.superClass_.disposeInternal.call(this);
+ delete this.elements_
+};
+goog.dom.ControlRangeIterator = function(range) {
+ if(range) {
+ this.elements_ = range.getSortedElements();
+ this.startNode_ = this.elements_.shift();
+ this.endNode_ = goog.array.peek(this.elements_) || this.startNode_
+ }
+ goog.dom.RangeIterator.call(this, this.startNode_, false)
+};
+goog.inherits(goog.dom.ControlRangeIterator, goog.dom.RangeIterator);
+goog.dom.ControlRangeIterator.prototype.startNode_ = null;
+goog.dom.ControlRangeIterator.prototype.endNode_ = null;
+goog.dom.ControlRangeIterator.prototype.elements_ = null;
+goog.dom.ControlRangeIterator.prototype.getStartTextOffset = function() {
+ return 0
+};
+goog.dom.ControlRangeIterator.prototype.getEndTextOffset = function() {
+ return 0
+};
+goog.dom.ControlRangeIterator.prototype.getStartNode = function() {
+ return this.startNode_
+};
+goog.dom.ControlRangeIterator.prototype.getEndNode = function() {
+ return this.endNode_
+};
+goog.dom.ControlRangeIterator.prototype.isLast = function() {
+ return!this.depth && !this.elements_.length
+};
+goog.dom.ControlRangeIterator.prototype.next = function() {
+ if(this.isLast()) {
+ throw goog.iter.StopIteration;
+ }else {
+ if(!this.depth) {
+ var el = this.elements_.shift();
+ this.setPosition(el, goog.dom.TagWalkType.START_TAG, goog.dom.TagWalkType.START_TAG);
+ return el
+ }
+ }
+ return goog.dom.ControlRangeIterator.superClass_.next.call(this)
+};
+goog.dom.ControlRangeIterator.prototype.copyFrom = function(other) {
+ this.elements_ = other.elements_;
+ this.startNode_ = other.startNode_;
+ this.endNode_ = other.endNode_;
+ goog.dom.ControlRangeIterator.superClass_.copyFrom.call(this, other)
+};
+goog.dom.ControlRangeIterator.prototype.clone = function() {
+ var copy = new goog.dom.ControlRangeIterator(null);
+ copy.copyFrom(this);
+ return copy
+};
+goog.provide("goog.dom.MultiRange");
+goog.provide("goog.dom.MultiRangeIterator");
+goog.require("goog.array");
+goog.require("goog.debug.Logger");
+goog.require("goog.dom.AbstractMultiRange");
+goog.require("goog.dom.AbstractRange");
+goog.require("goog.dom.RangeIterator");
+goog.require("goog.dom.RangeType");
+goog.require("goog.dom.SavedRange");
+goog.require("goog.dom.TextRange");
+goog.require("goog.iter.StopIteration");
+goog.dom.MultiRange = function() {
+ this.browserRanges_ = [];
+ this.ranges_ = [];
+ this.sortedRanges_ = null;
+ this.container_ = null
+};
+goog.inherits(goog.dom.MultiRange, goog.dom.AbstractMultiRange);
+goog.dom.MultiRange.createFromBrowserSelection = function(selection) {
+ var range = new goog.dom.MultiRange;
+ for(var i = 0, len = selection.rangeCount;i < len;i++) {
+ range.browserRanges_.push(selection.getRangeAt(i))
+ }
+ return range
+};
+goog.dom.MultiRange.createFromBrowserRanges = function(browserRanges) {
+ var range = new goog.dom.MultiRange;
+ range.browserRanges_ = goog.array.clone(browserRanges);
+ return range
+};
+goog.dom.MultiRange.createFromTextRanges = function(textRanges) {
+ var range = new goog.dom.MultiRange;
+ range.ranges_ = textRanges;
+ range.browserRanges_ = goog.array.map(textRanges, function(range) {
+ return range.getBrowserRangeObject()
+ });
+ return range
+};
+goog.dom.MultiRange.prototype.logger_ = goog.debug.Logger.getLogger("goog.dom.MultiRange");
+goog.dom.MultiRange.prototype.clearCachedValues_ = function() {
+ this.ranges_ = [];
+ this.sortedRanges_ = null;
+ this.container_ = null
+};
+goog.dom.MultiRange.prototype.clone = function() {
+ return goog.dom.MultiRange.createFromBrowserRanges(this.browserRanges_)
+};
+goog.dom.MultiRange.prototype.getType = function() {
+ return goog.dom.RangeType.MULTI
+};
+goog.dom.MultiRange.prototype.getBrowserRangeObject = function() {
+ if(this.browserRanges_.length > 1) {
+ this.logger_.warning("getBrowserRangeObject called on MultiRange with more than 1 range")
+ }
+ return this.browserRanges_[0]
+};
+goog.dom.MultiRange.prototype.setBrowserRangeObject = function(nativeRange) {
+ return false
+};
+goog.dom.MultiRange.prototype.getTextRangeCount = function() {
+ return this.browserRanges_.length
+};
+goog.dom.MultiRange.prototype.getTextRange = function(i) {
+ if(!this.ranges_[i]) {
+ this.ranges_[i] = goog.dom.TextRange.createFromBrowserRange(this.browserRanges_[i])
+ }
+ return this.ranges_[i]
+};
+goog.dom.MultiRange.prototype.getContainer = function() {
+ if(!this.container_) {
+ var nodes = [];
+ for(var i = 0, len = this.getTextRangeCount();i < len;i++) {
+ nodes.push(this.getTextRange(i).getContainer())
+ }
+ this.container_ = goog.dom.findCommonAncestor.apply(null, nodes)
+ }
+ return this.container_
+};
+goog.dom.MultiRange.prototype.getSortedRanges = function() {
+ if(!this.sortedRanges_) {
+ this.sortedRanges_ = this.getTextRanges();
+ this.sortedRanges_.sort(function(a, b) {
+ var aStartNode = a.getStartNode();
+ var aStartOffset = a.getStartOffset();
+ var bStartNode = b.getStartNode();
+ var bStartOffset = b.getStartOffset();
+ if(aStartNode == bStartNode && aStartOffset == bStartOffset) {
+ return 0
+ }
+ return goog.dom.Range.isReversed(aStartNode, aStartOffset, bStartNode, bStartOffset) ? 1 : -1
+ })
+ }
+ return this.sortedRanges_
+};
+goog.dom.MultiRange.prototype.getStartNode = function() {
+ return this.getSortedRanges()[0].getStartNode()
+};
+goog.dom.MultiRange.prototype.getStartOffset = function() {
+ return this.getSortedRanges()[0].getStartOffset()
+};
+goog.dom.MultiRange.prototype.getEndNode = function() {
+ return goog.array.peek(this.getSortedRanges()).getEndNode()
+};
+goog.dom.MultiRange.prototype.getEndOffset = function() {
+ return goog.array.peek(this.getSortedRanges()).getEndOffset()
+};
+/*
+goog.dom.MultiRange.prototype.isRangeInDocument = function() {
+ return goog.array.every(this.getTextRanges(), function(range) {
+ return range.isRangeInDocument()
+ })
+};
+*/
+goog.dom.MultiRange.prototype.isCollapsed = function() {
+ return this.browserRanges_.length == 0 || this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed()
+};
+goog.dom.MultiRange.prototype.getText = function() {
+ return goog.array.map(this.getTextRanges(), function(range) {
+ return range.getText()
+ }).join("")
+};
+goog.dom.MultiRange.prototype.getHtmlFragment = function() {
+ return this.getValidHtml()
+};
+goog.dom.MultiRange.prototype.getValidHtml = function() {
+ return goog.array.map(this.getTextRanges(), function(range) {
+ return range.getValidHtml()
+ }).join("")
+};
+goog.dom.MultiRange.prototype.getPastableHtml = function() {
+ return this.getValidHtml()
+};
+goog.dom.MultiRange.prototype.__iterator__ = function(opt_keys) {
+ return new goog.dom.MultiRangeIterator(this)
+};
+goog.dom.MultiRange.prototype.select = function() {
+ var selection = goog.dom.AbstractRange.getBrowserSelectionForWindow(this.getWindow());
+ selection.removeAllRanges();
+ for(var i = 0, len = this.getTextRangeCount();i < len;i++) {
+ selection.addRange(this.getTextRange(i).getBrowserRangeObject())
+ }
+};
+goog.dom.MultiRange.prototype.removeContents = function() {
+ goog.array.forEach(this.getTextRanges(), function(range) {
+ range.removeContents()
+ })
+};
+goog.dom.MultiRange.prototype.saveUsingDom = function() {
+ return new goog.dom.DomSavedMultiRange_(this)
+};
+goog.dom.MultiRange.prototype.collapse = function(toAnchor) {
+ if(!this.isCollapsed()) {
+ var range = toAnchor ? this.getTextRange(0) : this.getTextRange(this.getTextRangeCount() - 1);
+ this.clearCachedValues_();
+ range.collapse(toAnchor);
+ this.ranges_ = [range];
+ this.sortedRanges_ = [range];
+ this.browserRanges_ = [range.getBrowserRangeObject()]
+ }
+};
+goog.dom.DomSavedMultiRange_ = function(range) {
+ this.savedRanges_ = goog.array.map(range.getTextRanges(), function(range) {
+ return range.saveUsingDom()
+ })
+};
+goog.inherits(goog.dom.DomSavedMultiRange_, goog.dom.SavedRange);
+goog.dom.DomSavedMultiRange_.prototype.restoreInternal = function() {
+ var ranges = goog.array.map(this.savedRanges_, function(savedRange) {
+ return savedRange.restore()
+ });
+ return goog.dom.MultiRange.createFromTextRanges(ranges)
+};
+goog.dom.DomSavedMultiRange_.prototype.disposeInternal = function() {
+ goog.dom.DomSavedMultiRange_.superClass_.disposeInternal.call(this);
+ goog.array.forEach(this.savedRanges_, function(savedRange) {
+ savedRange.dispose()
+ });
+ delete this.savedRanges_
+};
+goog.dom.MultiRangeIterator = function(range) {
+ if(range) {
+ this.iterators_ = goog.array.map(range.getSortedRanges(), function(r) {
+ return goog.iter.toIterator(r)
+ })
+ }
+ goog.dom.RangeIterator.call(this, range ? this.getStartNode() : null, false)
+};
+goog.inherits(goog.dom.MultiRangeIterator, goog.dom.RangeIterator);
+goog.dom.MultiRangeIterator.prototype.iterators_ = null;
+goog.dom.MultiRangeIterator.prototype.currentIdx_ = 0;
+goog.dom.MultiRangeIterator.prototype.getStartTextOffset = function() {
+ return this.iterators_[this.currentIdx_].getStartTextOffset()
+};
+goog.dom.MultiRangeIterator.prototype.getEndTextOffset = function() {
+ return this.iterators_[this.currentIdx_].getEndTextOffset()
+};
+goog.dom.MultiRangeIterator.prototype.getStartNode = function() {
+ return this.iterators_[0].getStartNode()
+};
+goog.dom.MultiRangeIterator.prototype.getEndNode = function() {
+ return goog.array.peek(this.iterators_).getEndNode()
+};
+goog.dom.MultiRangeIterator.prototype.isLast = function() {
+ return this.iterators_[this.currentIdx_].isLast()
+};
+goog.dom.MultiRangeIterator.prototype.next = function() {
+ try {
+ var it = this.iterators_[this.currentIdx_];
+ var next = it.next();
+ this.setPosition(it.node, it.tagType, it.depth);
+ return next
+ }catch(ex) {
+ if(ex !== goog.iter.StopIteration || this.iterators_.length - 1 == this.currentIdx_) {
+ throw ex;
+ }else {
+ this.currentIdx_++;
+ return this.next()
+ }
+ }
+};
+goog.dom.MultiRangeIterator.prototype.copyFrom = function(other) {
+ this.iterators_ = goog.array.clone(other.iterators_);
+ goog.dom.MultiRangeIterator.superClass_.copyFrom.call(this, other)
+};
+goog.dom.MultiRangeIterator.prototype.clone = function() {
+ var copy = new goog.dom.MultiRangeIterator(null);
+ copy.copyFrom(this);
+ return copy
+};
+goog.provide("goog.dom.Range");
+goog.require("goog.dom");
+goog.require("goog.dom.AbstractRange");
+goog.require("goog.dom.ControlRange");
+goog.require("goog.dom.MultiRange");
+goog.require("goog.dom.NodeType");
+goog.require("goog.dom.TextRange");
+goog.require("goog.userAgent");
+goog.dom.Range.createFromWindow = function(opt_win) {
+ var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window);
+ return sel && goog.dom.Range.createFromBrowserSelection(sel)
+};
+goog.dom.Range.createFromBrowserSelection = function(selection) {
+ var range;
+ var isReversed = false;
+ if(selection.createRange) {
+ try {
+ range = selection.createRange()
+ }catch(e) {
+ return null
+ }
+ }else {
+ if(selection.rangeCount) {
+ if(selection.rangeCount > 1) {
+ return goog.dom.MultiRange.createFromBrowserSelection(selection)
+ }else {
+ range = selection.getRangeAt(0);
+ isReversed = goog.dom.Range.isReversed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset)
+ }
+ }else {
+ return null
+ }
+ }
+ return goog.dom.Range.createFromBrowserRange(range, isReversed)
+};
+goog.dom.Range.createFromBrowserRange = function(range, opt_isReversed) {
+ return goog.dom.AbstractRange.isNativeControlRange(range) ? goog.dom.ControlRange.createFromBrowserRange(range) : goog.dom.TextRange.createFromBrowserRange(range, opt_isReversed)
+};
+goog.dom.Range.createFromNodeContents = function(node, opt_isReversed) {
+ return goog.dom.TextRange.createFromNodeContents(node, opt_isReversed)
+};
+goog.dom.Range.createCaret = function(node, offset) {
+ return goog.dom.TextRange.createFromNodes(node, offset, node, offset)
+};
+goog.dom.Range.createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+ return goog.dom.TextRange.createFromNodes(startNode, startOffset, endNode, endOffset)
+};
+goog.dom.Range.clearSelection = function(opt_win) {
+ var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window);
+ if(!sel) {
+ return
+ }
+ if(sel.empty) {
+ try {
+ sel.empty()
+ }catch(e) {
+ }
+ }else {
+ sel.removeAllRanges()
+ }
+};
+goog.dom.Range.hasSelection = function(opt_win) {
+ var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window);
+ return!!sel && (goog.userAgent.IE ? sel.type != "None" : !!sel.rangeCount)
+};
+goog.dom.Range.isReversed = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+ if(anchorNode == focusNode) {
+ return focusOffset < anchorOffset
+ }
+ var child;
+ if(anchorNode.nodeType == goog.dom.NodeType.ELEMENT && anchorOffset) {
+ child = anchorNode.childNodes[anchorOffset];
+ if(child) {
+ anchorNode = child;
+ anchorOffset = 0
+ }else {
+ if(goog.dom.contains(anchorNode, focusNode)) {
+ return true
+ }
+ }
+ }
+ if(focusNode.nodeType == goog.dom.NodeType.ELEMENT && focusOffset) {
+ child = focusNode.childNodes[focusOffset];
+ if(child) {
+ focusNode = child;
+ focusOffset = 0
+ }else {
+ if(goog.dom.contains(focusNode, anchorNode)) {
+ return false
+ }
+ }
+ }
+ return(goog.dom.compareNodeOrder(anchorNode, focusNode) || anchorOffset - focusOffset) > 0
+};
+window.createFromWindow = goog.dom.Range.createFromWindow;
+window.createFromNodes = goog.dom.Range.createFromNodes;
+window.createCaret = goog.dom.Range.createCaret; \ No newline at end of file
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js
new file mode 100644
index 0000000000..6e2acd937c
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js
@@ -0,0 +1,383 @@
+/**
+ * @fileoverview
+ * Main functions used in running the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+/**
+ * Info function: returns true if the suite (mainly) tests the result HTML/Text.
+ *
+ * @param suite {String} the test suite
+ * @return {boolean} Whether the suite main focus is the output HTML/Text
+ */
+function suiteChecksHTMLOrText(suite) {
+ return suite.id[0] != 'S';
+}
+
+/**
+ * Info function: returns true if the suite checks the result selection.
+ *
+ * @param suite {String} the test suite
+ * @return {boolean} Whether the suite checks the selection
+ */
+function suiteChecksSelection(suite) {
+ return suite.id[0] != 'Q';
+}
+
+/**
+ * Helper function returning the effective value of a test parameter.
+ *
+ * @param suite {Object} the test suite
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test
+ * @param param {String} the test parameter to be checked
+ * @return {Any} the effective value of the parameter (can be undefined)
+ */
+function getTestParameter(suite, group, test, param) {
+ var val = test[param];
+ if (val === undefined) {
+ val = group[param];
+ }
+ if (val === undefined) {
+ val = suite[param];
+ }
+ return val;
+}
+
+/**
+ * Helper function returning the effective value of a container/test parameter.
+ *
+ * @param suite {Object} the test suite
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} the test
+ * @param container {Object} the container descriptor object
+ * @param param {String} the test parameter to be checked
+ * @return {Any} the effective value of the parameter (can be undefined)
+ */
+function getContainerParameter(suite, group, test, container, param) {
+ var val = undefined;
+ if (test[container.id]) {
+ val = test[container.id][param];
+ }
+ if (val === undefined) {
+ val = test[param];
+ }
+ if (val === undefined) {
+ val = group[param];
+ }
+ if (val === undefined) {
+ val = suite[param];
+ }
+ return val;
+}
+
+/**
+ * Initializes the global variables before any tests are run.
+ */
+function initVariables() {
+ results = {
+ count: 0,
+ valscore: 0,
+ selscore: 0
+ };
+}
+
+/**
+ * Runs a single test - outputs and sets the result variables.
+ *
+ * @param suite {Object} suite that test originates in as object reference
+ * @param group {Object} group of tests within the suite the test belongs to
+ * @param test {Object} test to be run as object reference
+ * @param container {Object} container descriptor as object reference
+ * @see variables.js for RESULT... values
+ */
+function runSingleTest(suite, group, test, container) {
+ var result = {
+ valscore: 0,
+ selscore: 0,
+ valresult: VALRESULT_NOT_RUN,
+ selresult: SELRESULT_NOT_RUN,
+ output: ''
+ };
+
+ // 1.) Populate the editor element with the initial test setup HTML.
+ try {
+ initContainer(suite, group, test, container);
+ } catch(ex) {
+ result.valresult = VALRESULT_SETUP_EXCEPTION;
+ result.selresult = SELRESULT_NA;
+ result.output = SETUP_EXCEPTION + ex.toString();
+ return result;
+ }
+
+ // 2.) Run the test command, general function or query function.
+ var isHTMLTest = false;
+
+ try {
+ var cmd = undefined;
+
+ if (cmd = getTestParameter(suite, group, test, PARAM_EXECCOMMAND)) {
+ isHTMLTest = true;
+ // Note: "getTestParameter(suite, group, test, PARAM_VALUE) || null"
+ // doesn't work, since value might be the empty string, e.g., for 'insertText'!
+ var value = getTestParameter(suite, group, test, PARAM_VALUE);
+ if (value === undefined) {
+ value = null;
+ }
+ container.doc.execCommand(cmd, false, value);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_FUNCTION)) {
+ isHTMLTest = true;
+ eval(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSUPPORTED)) {
+ result.output = container.doc.queryCommandSupported(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDENABLED)) {
+ result.output = container.doc.queryCommandEnabled(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDINDETERM)) {
+ result.output = container.doc.queryCommandIndeterm(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSTATE)) {
+ result.output = container.doc.queryCommandState(cmd);
+ } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) {
+ result.output = container.doc.queryCommandValue(cmd);
+ if (result.output === false) {
+ // A return value of boolean 'false' for queryCommandValue means 'not supported'.
+ result.valresult = VALRESULT_UNSUPPORTED;
+ result.selresult = SELRESULT_NA;
+ result.output = UNSUPPORTED;
+ return result;
+ }
+ } else {
+ result.valresult = VALRESULT_SETUP_EXCEPTION;
+ result.selresult = SELRESULT_NA;
+ result.output = SETUP_EXCEPTION + SETUP_NOCOMMAND;
+ return result;
+ }
+ } catch (ex) {
+ result.valresult = VALRESULT_EXECUTION_EXCEPTION;
+ result.selresult = SELRESULT_NA;
+ result.output = EXECUTION_EXCEPTION + ex.toString();
+ return result;
+ }
+
+ // 4.) Verify test result
+ try {
+ if (isHTMLTest) {
+ // First, retrieve HTML from container
+ prepareHTMLTestResult(container, result);
+
+ // Compare result to expectations
+ compareHTMLTestResult(suite, group, test, container, result);
+
+ result.valscore = (result.valresult === VALRESULT_EQUAL) ? 1 : 0;
+ result.selscore = (result.selresult === SELRESULT_EQUAL) ? 1 : 0;
+ } else {
+ compareTextTestResult(suite, group, test, result);
+
+ result.selresult = SELRESULT_NA;
+ result.valscore = (result.valresult === VALRESULT_EQUAL) ? 1 : 0;
+ }
+ } catch (ex) {
+ result.valresult = VALRESULT_VERIFICATION_EXCEPTION;
+ result.selresult = SELRESULT_NA;
+ result.output = VERIFICATION_EXCEPTION + ex.toString();
+ return result;
+ }
+
+ return result;
+}
+
+/**
+ * Initializes the results dictionary for a given test suite.
+ * (for all classes -> tests -> containers)
+ *
+ * @param {Object} suite as object reference
+ */
+function initTestSuiteResults(suite) {
+ var suiteID = suite.id;
+
+ // Initialize results entries for this suite
+ results[suiteID] = {
+ count: 0,
+ valscore: 0,
+ selscore: 0,
+ time: 0
+ };
+ var totalTestCount = 0;
+
+ for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) {
+ var clsID = testClassIDs[clsIdx];
+ var cls = suite[clsID];
+ if (!cls)
+ continue;
+
+ results[suiteID][clsID] = {
+ count: 0,
+ valscore: 0,
+ selscore: 0
+ };
+ var clsTestCount = 0;
+
+ var groupCount = cls.length;
+ for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) {
+ var group = cls[groupIdx];
+ var testCount = group.tests.length;
+
+ clsTestCount += testCount;
+ totalTestCount += testCount;
+
+ for (var testIdx = 0; testIdx < testCount; ++testIdx) {
+ var test = group.tests[testIdx];
+
+ results[suiteID][clsID ][test.id] = {
+ valscore: 0,
+ selscore: 0,
+ valresult: VALRESULT_NOT_RUN,
+ selresult: SELRESULT_NOT_RUN
+ };
+ for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) {
+ var cntID = containers[cntIdx].id;
+
+ results[suiteID][clsID][test.id][cntID] = {
+ valscore: 0,
+ selscore: 0,
+ valresult: VALRESULT_NOT_RUN,
+ selresult: SELRESULT_NOT_RUN,
+ output: ''
+ }
+ }
+ }
+ }
+ results[suiteID][clsID].count = clsTestCount;
+ }
+ results[suiteID].count = totalTestCount;
+}
+
+/**
+ * Runs a single test suite (such as DELETE tests or INSERT tests).
+ *
+ * @param suite {Object} suite as object reference
+ */
+function runTestSuite(suite) {
+ var suiteID = suite.id;
+ var suiteStartTime = new Date().getTime();
+
+ initTestSuiteResults(suite);
+
+ for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) {
+ var clsID = testClassIDs[clsIdx];
+ var cls = suite[clsID];
+ if (!cls)
+ continue;
+
+ var groupCount = cls.length;
+
+ for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) {
+ var group = cls[groupIdx];
+ var testCount = group.tests.length;
+
+ for (var testIdx = 0; testIdx < testCount; ++testIdx) {
+ var test = group.tests[testIdx];
+
+ var valscore = 1;
+ var selscore = 1;
+ var valresult = VALRESULT_EQUAL;
+ var selresult = SELRESULT_EQUAL;
+
+ for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) {
+ var container = containers[cntIdx];
+ var cntID = container.id;
+
+ var result = runSingleTest(suite, group, test, container);
+
+ results[suiteID][clsID][test.id][cntID] = result;
+
+ valscore = Math.min(valscore, result.valscore);
+ selscore = Math.min(selscore, result.selscore);
+ valresult = Math.min(valresult, result.valresult);
+ selresult = Math.min(selresult, result.selresult);
+
+ resetContainer(container);
+ }
+
+ results[suiteID][clsID][test.id].valscore = valscore;
+ results[suiteID][clsID][test.id].selscore = selscore;
+ results[suiteID][clsID][test.id].valresult = valresult;
+ results[suiteID][clsID][test.id].selresult = selresult;
+
+ results[suiteID][clsID].valscore += valscore;
+ results[suiteID][clsID].selscore += selscore;
+ results[suiteID].valscore += valscore;
+ results[suiteID].selscore += selscore;
+ results.valscore += valscore;
+ results.selscore += selscore;
+ }
+ }
+ }
+
+ results[suiteID].time = new Date().getTime() - suiteStartTime;
+}
+
+/**
+ * Runs a single test suite (such as DELETE tests or INSERT tests)
+ * and updates the output HTML.
+ *
+ * @param {Object} suite as object reference
+ */
+function runAndOutputTestSuite(suite) {
+ runTestSuite(suite);
+ outputTestSuiteResults(suite);
+}
+
+/**
+ * Fills the beacon with the test results.
+ */
+function fillResults() {
+ // Result totals of the individual categories
+ categoryTotals = [
+ 'selection=' + results['S'].selscore,
+ 'apply=' + results['A'].valscore,
+ 'applyCSS=' + results['AC'].valscore,
+ 'change=' + results['C'].valscore,
+ 'changeCSS=' + results['CC'].valscore,
+ 'unapply=' + results['U'].valscore,
+ 'unapplyCSS=' + results['UC'].valscore,
+ 'delete=' + results['D'].valscore,
+ 'forwarddelete=' + results['FD'].valscore,
+ 'insert=' + results['I'].valscore,
+ 'selectionResult=' + (results['A'].selscore +
+ results['AC'].selscore +
+ results['C'].selscore +
+ results['CC'].selscore +
+ results['U'].selscore +
+ results['UC'].selscore +
+ results['D'].selscore +
+ results['FD'].selscore +
+ results['I'].selscore),
+ 'querySupported=' + results['Q'].valscore,
+ 'queryEnabled=' + results['QE'].valscore,
+ 'queryIndeterm=' + results['QI'].valscore,
+ 'queryState=' + results['QS'].valscore,
+ 'queryStateCSS=' + results['QSC'].valscore,
+ 'queryValue=' + results['QV'].valscore,
+ 'queryValueCSS=' + results['QVC'].valscore
+ ];
+
+ // Beacon copies category results
+ beacon = categoryTotals.slice(0);
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js
new file mode 100644
index 0000000000..f2c23fbe50
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js
@@ -0,0 +1,416 @@
+/**
+ * @fileoverview
+ * Common constants and variables used in the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+// All colors defined in CSS3.
+var colorChart = {
+ 'aliceblue': {red: 0xF0, green: 0xF8, blue: 0xFF},
+ 'antiquewhite': {red: 0xFA, green: 0xEB, blue: 0xD7},
+ 'aqua': {red: 0x00, green: 0xFF, blue: 0xFF},
+ 'aquamarine': {red: 0x7F, green: 0xFF, blue: 0xD4},
+ 'azure': {red: 0xF0, green: 0xFF, blue: 0xFF},
+ 'beige': {red: 0xF5, green: 0xF5, blue: 0xDC},
+ 'bisque': {red: 0xFF, green: 0xE4, blue: 0xC4},
+ 'black': {red: 0x00, green: 0x00, blue: 0x00},
+ 'blanchedalmond': {red: 0xFF, green: 0xEB, blue: 0xCD},
+ 'blue': {red: 0x00, green: 0x00, blue: 0xFF},
+ 'blueviolet': {red: 0x8A, green: 0x2B, blue: 0xE2},
+ 'brown': {red: 0xA5, green: 0x2A, blue: 0x2A},
+ 'burlywood': {red: 0xDE, green: 0xB8, blue: 0x87},
+ 'cadetblue': {red: 0x5F, green: 0x9E, blue: 0xA0},
+ 'chartreuse': {red: 0x7F, green: 0xFF, blue: 0x00},
+ 'chocolate': {red: 0xD2, green: 0x69, blue: 0x1E},
+ 'coral': {red: 0xFF, green: 0x7F, blue: 0x50},
+ 'cornflowerblue': {red: 0x64, green: 0x95, blue: 0xED},
+ 'cornsilk': {red: 0xFF, green: 0xF8, blue: 0xDC},
+ 'crimson': {red: 0xDC, green: 0x14, blue: 0x3C},
+ 'cyan': {red: 0x00, green: 0xFF, blue: 0xFF},
+ 'darkblue': {red: 0x00, green: 0x00, blue: 0x8B},
+ 'darkcyan': {red: 0x00, green: 0x8B, blue: 0x8B},
+ 'darkgoldenrod': {red: 0xB8, green: 0x86, blue: 0x0B},
+ 'darkgray': {red: 0xA9, green: 0xA9, blue: 0xA9},
+ 'darkgreen': {red: 0x00, green: 0x64, blue: 0x00},
+ 'darkgrey': {red: 0xA9, green: 0xA9, blue: 0xA9},
+ 'darkkhaki': {red: 0xBD, green: 0xB7, blue: 0x6B},
+ 'darkmagenta': {red: 0x8B, green: 0x00, blue: 0x8B},
+ 'darkolivegreen': {red: 0x55, green: 0x6B, blue: 0x2F},
+ 'darkorange': {red: 0xFF, green: 0x8C, blue: 0x00},
+ 'darkorchid': {red: 0x99, green: 0x32, blue: 0xCC},
+ 'darkred': {red: 0x8B, green: 0x00, blue: 0x00},
+ 'darksalmon': {red: 0xE9, green: 0x96, blue: 0x7A},
+ 'darkseagreen': {red: 0x8F, green: 0xBC, blue: 0x8F},
+ 'darkslateblue': {red: 0x48, green: 0x3D, blue: 0x8B},
+ 'darkslategray': {red: 0x2F, green: 0x4F, blue: 0x4F},
+ 'darkslategrey': {red: 0x2F, green: 0x4F, blue: 0x4F},
+ 'darkturquoise': {red: 0x00, green: 0xCE, blue: 0xD1},
+ 'darkviolet': {red: 0x94, green: 0x00, blue: 0xD3},
+ 'deeppink': {red: 0xFF, green: 0x14, blue: 0x93},
+ 'deepskyblue': {red: 0x00, green: 0xBF, blue: 0xFF},
+ 'dimgray': {red: 0x69, green: 0x69, blue: 0x69},
+ 'dimgrey': {red: 0x69, green: 0x69, blue: 0x69},
+ 'dodgerblue': {red: 0x1E, green: 0x90, blue: 0xFF},
+ 'firebrick': {red: 0xB2, green: 0x22, blue: 0x22},
+ 'floralwhite': {red: 0xFF, green: 0xFA, blue: 0xF0},
+ 'forestgreen': {red: 0x22, green: 0x8B, blue: 0x22},
+ 'fuchsia': {red: 0xFF, green: 0x00, blue: 0xFF},
+ 'gainsboro': {red: 0xDC, green: 0xDC, blue: 0xDC},
+ 'ghostwhite': {red: 0xF8, green: 0xF8, blue: 0xFF},
+ 'gold': {red: 0xFF, green: 0xD7, blue: 0x00},
+ 'goldenrod': {red: 0xDA, green: 0xA5, blue: 0x20},
+ 'gray': {red: 0x80, green: 0x80, blue: 0x80},
+ 'green': {red: 0x00, green: 0x80, blue: 0x00},
+ 'greenyellow': {red: 0xAD, green: 0xFF, blue: 0x2F},
+ 'grey': {red: 0x80, green: 0x80, blue: 0x80},
+ 'honeydew': {red: 0xF0, green: 0xFF, blue: 0xF0},
+ 'hotpink': {red: 0xFF, green: 0x69, blue: 0xB4},
+ 'indianred': {red: 0xCD, green: 0x5C, blue: 0x5C},
+ 'indigo': {red: 0x4B, green: 0x00, blue: 0x82},
+ 'ivory': {red: 0xFF, green: 0xFF, blue: 0xF0},
+ 'khaki': {red: 0xF0, green: 0xE6, blue: 0x8C},
+ 'lavender': {red: 0xE6, green: 0xE6, blue: 0xFA},
+ 'lavenderblush': {red: 0xFF, green: 0xF0, blue: 0xF5},
+ 'lawngreen': {red: 0x7C, green: 0xFC, blue: 0x00},
+ 'lemonchiffon': {red: 0xFF, green: 0xFA, blue: 0xCD},
+ 'lightblue': {red: 0xAD, green: 0xD8, blue: 0xE6},
+ 'lightcoral': {red: 0xF0, green: 0x80, blue: 0x80},
+ 'lightcyan': {red: 0xE0, green: 0xFF, blue: 0xFF},
+ 'lightgoldenrodyellow': {red: 0xFA, green: 0xFA, blue: 0xD2},
+ 'lightgray': {red: 0xD3, green: 0xD3, blue: 0xD3},
+ 'lightgreen': {red: 0x90, green: 0xEE, blue: 0x90},
+ 'lightgrey': {red: 0xD3, green: 0xD3, blue: 0xD3},
+ 'lightpink': {red: 0xFF, green: 0xB6, blue: 0xC1},
+ 'lightsalmon': {red: 0xFF, green: 0xA0, blue: 0x7A},
+ 'lightseagreen': {red: 0x20, green: 0xB2, blue: 0xAA},
+ 'lightskyblue': {red: 0x87, green: 0xCE, blue: 0xFA},
+ 'lightslategray': {red: 0x77, green: 0x88, blue: 0x99},
+ 'lightslategrey': {red: 0x77, green: 0x88, blue: 0x99},
+ 'lightsteelblue': {red: 0xB0, green: 0xC4, blue: 0xDE},
+ 'lightyellow': {red: 0xFF, green: 0xFF, blue: 0xE0},
+ 'lime': {red: 0x00, green: 0xFF, blue: 0x00},
+ 'limegreen': {red: 0x32, green: 0xCD, blue: 0x32},
+ 'linen': {red: 0xFA, green: 0xF0, blue: 0xE6},
+ 'magenta': {red: 0xFF, green: 0x00, blue: 0xFF},
+ 'maroon': {red: 0x80, green: 0x00, blue: 0x00},
+ 'mediumaquamarine': {red: 0x66, green: 0xCD, blue: 0xAA},
+ 'mediumblue': {red: 0x00, green: 0x00, blue: 0xCD},
+ 'mediumorchid': {red: 0xBA, green: 0x55, blue: 0xD3},
+ 'mediumpurple': {red: 0x93, green: 0x70, blue: 0xDB},
+ 'mediumseagreen': {red: 0x3C, green: 0xB3, blue: 0x71},
+ 'mediumslateblue': {red: 0x7B, green: 0x68, blue: 0xEE},
+ 'mediumspringgreen': {red: 0x00, green: 0xFA, blue: 0x9A},
+ 'mediumturquoise': {red: 0x48, green: 0xD1, blue: 0xCC},
+ 'mediumvioletred': {red: 0xC7, green: 0x15, blue: 0x85},
+ 'midnightblue': {red: 0x19, green: 0x19, blue: 0x70},
+ 'mintcream': {red: 0xF5, green: 0xFF, blue: 0xFA},
+ 'mistyrose': {red: 0xFF, green: 0xE4, blue: 0xE1},
+ 'moccasin': {red: 0xFF, green: 0xE4, blue: 0xB5},
+ 'navajowhite': {red: 0xFF, green: 0xDE, blue: 0xAD},
+ 'navy': {red: 0x00, green: 0x00, blue: 0x80},
+ 'oldlace': {red: 0xFD, green: 0xF5, blue: 0xE6},
+ 'olive': {red: 0x80, green: 0x80, blue: 0x00},
+ 'olivedrab': {red: 0x6B, green: 0x8E, blue: 0x23},
+ 'orange': {red: 0xFF, green: 0xA5, blue: 0x00},
+ 'orangered': {red: 0xFF, green: 0x45, blue: 0x00},
+ 'orchid': {red: 0xDA, green: 0x70, blue: 0xD6},
+ 'palegoldenrod': {red: 0xEE, green: 0xE8, blue: 0xAA},
+ 'palegreen': {red: 0x98, green: 0xFB, blue: 0x98},
+ 'paleturquoise': {red: 0xAF, green: 0xEE, blue: 0xEE},
+ 'palevioletred': {red: 0xDB, green: 0x70, blue: 0x93},
+ 'papayawhip': {red: 0xFF, green: 0xEF, blue: 0xD5},
+ 'peachpuff': {red: 0xFF, green: 0xDA, blue: 0xB9},
+ 'peru': {red: 0xCD, green: 0x85, blue: 0x3F},
+ 'pink': {red: 0xFF, green: 0xC0, blue: 0xCB},
+ 'plum': {red: 0xDD, green: 0xA0, blue: 0xDD},
+ 'powderblue': {red: 0xB0, green: 0xE0, blue: 0xE6},
+ 'purple': {red: 0x80, green: 0x00, blue: 0x80},
+ 'red': {red: 0xFF, green: 0x00, blue: 0x00},
+ 'rosybrown': {red: 0xBC, green: 0x8F, blue: 0x8F},
+ 'royalblue': {red: 0x41, green: 0x69, blue: 0xE1},
+ 'saddlebrown': {red: 0x8B, green: 0x45, blue: 0x13},
+ 'salmon': {red: 0xFA, green: 0x80, blue: 0x72},
+ 'sandybrown': {red: 0xF4, green: 0xA4, blue: 0x60},
+ 'seagreen': {red: 0x2E, green: 0x8B, blue: 0x57},
+ 'seashell': {red: 0xFF, green: 0xF5, blue: 0xEE},
+ 'sienna': {red: 0xA0, green: 0x52, blue: 0x2D},
+ 'silver': {red: 0xC0, green: 0xC0, blue: 0xC0},
+ 'skyblue': {red: 0x87, green: 0xCE, blue: 0xEB},
+ 'slateblue': {red: 0x6A, green: 0x5A, blue: 0xCD},
+ 'slategray': {red: 0x70, green: 0x80, blue: 0x90},
+ 'slategrey': {red: 0x70, green: 0x80, blue: 0x90},
+ 'snow': {red: 0xFF, green: 0xFA, blue: 0xFA},
+ 'springgreen': {red: 0x00, green: 0xFF, blue: 0x7F},
+ 'steelblue': {red: 0x46, green: 0x82, blue: 0xB4},
+ 'tan': {red: 0xD2, green: 0xB4, blue: 0x8C},
+ 'teal': {red: 0x00, green: 0x80, blue: 0x80},
+ 'thistle': {red: 0xD8, green: 0xBF, blue: 0xD8},
+ 'tomato': {red: 0xFF, green: 0x63, blue: 0x47},
+ 'turquoise': {red: 0x40, green: 0xE0, blue: 0xD0},
+ 'violet': {red: 0xEE, green: 0x82, blue: 0xEE},
+ 'wheat': {red: 0xF5, green: 0xDE, blue: 0xB3},
+ 'white': {red: 0xFF, green: 0xFF, blue: 0xFF},
+ 'whitesmoke': {red: 0xF5, green: 0xF5, blue: 0xF5},
+ 'yellow': {red: 0xFF, green: 0xFF, blue: 0x00},
+ 'yellowgreen': {red: 0x9A, green: 0xCD, blue: 0x32},
+
+ 'transparent': {red: 0x00, green: 0x00, blue: 0x00, alpha: 0.0}
+};
+
+/**
+ * Color class allows cross-browser comparison of values, which can
+ * be returned from queryCommandValue in several formats:
+ * #ff00ff
+ * #f0f
+ * rgb(255, 0, 0)
+ * rgb(100%, 0%, 28%) // disabled for the time being (see below)
+ * rgba(127, 0, 64, 0.25)
+ * rgba(50%, 0%, 10%, 0.65) // disabled for the time being (see below)
+ * palegoldenrod
+ * transparent
+ *
+ * @constructor
+ * @param value {String} original value
+ */
+function Color(value) {
+ this.compare = function(other) {
+ if (!this.valid || !other.valid) {
+ return false;
+ }
+ if (this.alpha != other.alpha) {
+ return false;
+ }
+ if (this.alpha == 0.0) {
+ // both are fully transparent -> ignore the specific color information
+ return true;
+ }
+ // TODO(rolandsteiner): handle hsl/hsla values
+ return this.red == other.red && this.green == other.green && this.blue == other.blue;
+ }
+ this.parse = function(value) {
+ if (!value)
+ return false;
+ value = String(value).toLowerCase();
+ var match;
+ // '#' + 6 hex digits, e.g., #ff3300
+ match = value.match(/#([0-9a-f]{6})/i);
+ if (match) {
+ this.red = parseInt(match[1].substring(0, 2), 16);
+ this.green = parseInt(match[1].substring(2, 4), 16);
+ this.blue = parseInt(match[1].substring(4, 6), 16);
+ this.alpha = 1.0;
+ return true;
+ }
+ // '#' + 3 hex digits, e.g., #f30
+ match = value.match(/#([0-9a-f]{3})/i);
+ if (match) {
+ this.red = parseInt(match[1].substring(0, 1), 16) * 16;
+ this.green = parseInt(match[1].substring(1, 2), 16) * 16;
+ this.blue = parseInt(match[1].substring(2, 3), 16) * 16;
+ this.alpha = 1.0;
+ return true;
+ }
+ // a color name, e.g., springgreen
+ match = colorChart[value];
+ if (match) {
+ this.red = match.red;
+ this.green = match.green;
+ this.blue = match.blue;
+ this.alpha = (match.alpha === undefined) ? 1.0 : match.alpha;
+ return true;
+ }
+ // rgb(r, g, b), e.g., rgb(128, 12, 217)
+ match = value.match(/rgb\(([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/i);
+ if (match) {
+ this.red = Number(match[1]);
+ this.green = Number(match[2]);
+ this.blue = Number(match[3]);
+ this.alpha = 1.0;
+ return true;
+ }
+ // rgb(r%, g%, b%), e.g., rgb(100%, 0%, 50%)
+// Commented out for the time being, since it seems likely that the resulting
+// decimal values will create false negatives when compared with non-% values.
+//
+// => store as separate percent values and do exact matching when compared with % values
+// and fuzzy matching when compared with non-% values?
+//
+// match = value.match(/rgb\(([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*\)/i);
+// if (match) {
+// this.red = Number(match[1]) * 255 / 100;
+// this.green = Number(match[2]) * 255 / 100;
+// this.blue = Number(match[3]) * 255 / 100;
+// this.alpha = 1.0;
+// return true;
+// }
+ // rgba(r, g, b, a), e.g., rgb(128, 12, 217, 0.2)
+ match = value.match(/rgba\(([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/i);
+ if (match) {
+ this.red = Number(match[1]);
+ this.green = Number(match[2]);
+ this.blue = Number(match[3]);
+ this.alpha = Number(match[4]);
+ return true;
+ }
+ // rgba(r%, g%, b%, a), e.g., rgb(100%, 0%, 50%, 0.3)
+// Commented out for the time being (cf. rgb() matching above)
+// match = value.match(/rgba\(([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/i);
+// if (match) {
+// this.red = Number(match[1]) * 255 / 100;
+// this.green = Number(match[2]) * 255 / 100;
+// this.blue = Number(match[3]) * 255 / 100;
+// this.alpha = Number(match[4]);
+// return true;
+// }
+ // TODO(rolandsteiner): handle "hsl(h, s, l)" and "hsla(h, s, l, a)" notation
+ return false;
+ }
+ this.toString = function() {
+ return this.valid ? this.red + ',' + this.green + ',' + this.blue : '(invalid)';
+ }
+ this.toHexString = function() {
+ if (!this.valid)
+ return '(invalid)';
+ return ((this.red < 16) ? '0' : '') + this.red.toString(16) +
+ ((this.green < 16) ? '0' : '') + this.green.toString(16) +
+ ((this.blue < 16) ? '0' : '') + this.blue.toString(16);
+ }
+ this.valid = this.parse(value);
+}
+
+/**
+ * Utility class for converting font sizes to the size
+ * attribute in a font tag. Currently only converts px because
+ * only the sizes and px ever come from queryCommandValue.
+ *
+ * @constructor
+ * @param value {String} original value
+ */
+function FontSize(value) {
+ this.parse = function(str) {
+ if (!str)
+ this.valid = false;
+ var match;
+ if (match = String(str).match(/([0-9]+)px/)) {
+ var px = Number(match[1]);
+ if (px <= 0 || px > 47)
+ return false;
+ if (px <= 10) {
+ this.size = '1';
+ } else if (px <= 13) {
+ this.size = '2';
+ } else if (px <= 16) {
+ this.size = '3';
+ } else if (px <= 18) {
+ this.size = '4';
+ } else if (px <= 24) {
+ this.size = '5';
+ } else if (px <= 32) {
+ this.size = '6';
+ } else {
+ this.size = '7';
+ }
+ return true;
+ }
+ if (match = String(str).match(/([+-][0-9]+)/)) {
+ this.size = match[1];
+ return this.size >= 1 && this.size <= 7;
+ }
+ if (Number(str)) {
+ this.size = String(Number(str));
+ return this.size >= 1 && this.size <= 7;
+ }
+ switch (str) {
+ case 'x-small':
+ this.size = '1';
+ return true;
+ case 'small':
+ this.size = '2';
+ return true;
+ case 'medium':
+ this.size = '3';
+ return true;
+ case 'large':
+ this.size = '4';
+ return true;
+ case 'x-large':
+ this.size = '5';
+ return true;
+ case 'xx-large':
+ this.size = '6';
+ return true;
+ case 'xxx-large':
+ this.size = '7';
+ return true;
+ case '-webkit-xxx-large':
+ this.size = '7';
+ return true;
+ case 'larger':
+ this.size = '+1';
+ return true;
+ case 'smaller':
+ this.size = '-1';
+ return true;
+ }
+ return false;
+ }
+ this.compare = function(other) {
+ return this.valid && other.valid && this.size === other.size;
+ }
+ this.toString = function() {
+ return this.valid ? this.size : '(invalid)';
+ }
+ this.valid = this.parse(value);
+}
+
+/**
+ * Utility class for converting & canonicalizing font names.
+ *
+ * @constructor
+ * @param value {String} original value
+ */
+function FontName(value) {
+ this.parse = function(str) {
+ if (!str)
+ return false;
+ str = String(str).toLowerCase();
+ switch (str) {
+ case 'arial new':
+ this.fontname = 'arial';
+ return true;
+ case 'courier new':
+ this.fontname = 'courier';
+ return true;
+ case 'times new':
+ case 'times roman':
+ case 'times new roman':
+ this.fontname = 'times';
+ return true;
+ }
+ this.fontname = value;
+ return true;
+ }
+ this.compare = function(other) {
+ return this.valid && other.valid && this.fontname === other.fontname;
+ }
+ this.toString = function() {
+ return this.valid ? this.fontname : '(invalid)';
+ }
+ this.valid = this.parse(value);
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js
new file mode 100644
index 0000000000..cdc6f1e929
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js
@@ -0,0 +1,227 @@
+/**
+ * @fileoverview
+ * Common constants and variables used in the RTE test suite.
+ *
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License')
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @version 0.1
+ * @author rolandsteiner@google.com
+ */
+
+// Constant for indicating a test setup is unsupported or incorrect
+// (threw exception).
+var INTERNAL_ERR = 'INTERNAL ERROR: ';
+var SETUP_EXCEPTION = 'SETUP EXCEPTION: ';
+var EXECUTION_EXCEPTION = 'EXECUTION EXCEPTION: ';
+var VERIFICATION_EXCEPTION = 'VERIFICATION EXCEPTION: ';
+
+var SETUP_CONTAINER = 'WHEN INITIALIZING TEST CONTAINER';
+var SETUP_BAD_SELECTION_SPEC = 'BAD SELECTION SPECIFICATION IN TEST OR EXPECTATION STRING';
+var SETUP_HTML = 'WHEN SETTING TEST HTML';
+var SETUP_SELECTION = 'WHEN SETTING SELECTION';
+var SETUP_NOCOMMAND = 'NO COMMAND, GENERAL FUNCTION OR QUERY FUNCTION GIVEN';
+var HTML_COMPARISON = 'WHEN COMPARING OUTPUT HTML';
+
+// Exceptiona to be thrown on unsupported selection operations
+var SELMODIFY_UNSUPPORTED = 'UNSUPPORTED selection.modify()';
+var SELALLCHILDREN_UNSUPPORTED = 'UNSUPPORTED selection.selectAllChildren()';
+
+// Output string for unsupported functions
+// (returning bool 'false' as opposed to throwing an exception)
+var UNSUPPORTED = '<i>false</i> (UNSUPPORTED)';
+
+// HTML comparison result contants.
+var VALRESULT_NOT_RUN = 0; // test hasn't been run yet
+var VALRESULT_SETUP_EXCEPTION = 1;
+var VALRESULT_EXECUTION_EXCEPTION = 2;
+var VALRESULT_VERIFICATION_EXCEPTION = 3;
+var VALRESULT_UNSUPPORTED = 4;
+var VALRESULT_CANARY = 5; // HTML changes bled into the canary.
+var VALRESULT_DIFF = 6;
+var VALRESULT_ACCEPT = 7; // HTML technically correct, but not ideal.
+var VALRESULT_EQUAL = 8;
+
+var VALOUTPUT = [ // IMPORTANT: this array MUST be coordinated with the values above!!
+ {css: 'grey', output: '???', title: 'The test has not been run yet.'}, // VALRESULT_NOT_RUN
+ {css: 'exception', output: 'EXC.', title: 'Exception was thrown during setup.'}, // VALRESULT_SETUP_EXCEPTION
+ {css: 'exception', output: 'EXC.', title: 'Exception was thrown during execution.'}, // VALRESULT_EXECUTION_EXCEPTION
+ {css: 'exception', output: 'EXC.', title: 'Exception was thrown during result verification.'}, // VALRESULT_VERIFICATION_EXCEPTION
+ {css: 'unsupported', output: 'UNS.', title: 'Unsupported command or value'}, // VALRESULT_UNSUPPORTED
+ {css: 'canary', output: 'CANARY', title: 'The command affected the contentEditable root element, or outside HTML.'}, // VALRESULT_CANARY
+ {css: 'fail', output: 'FAIL', title: 'The result differs from the expectation(s).'}, // VALRESULT_DIFF
+ {css: 'accept', output: 'ACC.', title: 'The result is technically correct, but sub-optimal.'}, // VALRESULT_ACCEPT
+ {css: 'pass', output: 'PASS', title: 'The test result matches the expectation.'} // VALRESULT_EQUAL
+]
+
+// Selection comparison result contants.
+var SELRESULT_NOT_RUN = 0; // test hasn't been run yet
+var SELRESULT_CANARY = 1; // selection escapes the contentEditable element
+var SELRESULT_DIFF = 2;
+var SELRESULT_NA = 3;
+var SELRESULT_ACCEPT = 4; // Selection is acceptable, but not ideal.
+var SELRESULT_EQUAL = 5;
+
+var SELOUTPUT = [ // IMPORTANT: this array MUST be coordinated with the values above!!
+ {css: 'grey', output: 'grey', title: 'The test has not been run yet.'}, // SELRESULT_NOT_RUN
+ {css: 'canary', output: 'CANARY', title: 'The selection escaped the contentEditable boundary!'}, // SELRESULT_CANARY
+ {css: 'fail', output: 'FAIL', title: 'The selection differs from the expectation(s).'}, // SELRESULT_DIFF
+ {css: 'na', output: 'N/A', title: 'The correctness of the selection could not be verified.'}, // SELRESULT_NA
+ {css: 'accept', output: 'ACC.', title: 'The selection is technically correct, but sub-optimal.'}, // SELRESULT_ACCEPT
+ {css: 'pass', output: 'PASS', title: 'The selection matches the expectation.'} // SELRESULT_EQUAL
+];
+
+// RegExp for selection markers
+var SELECTION_MARKERS = /[\[\]\{\}\|\^]/;
+
+// Special attributes used to mark selections within elements that otherwise
+// have no children. Important: attribute name MUST be lower case!
+var ATTRNAME_SEL_START = 'bsselstart';
+var ATTRNAME_SEL_END = 'bsselend';
+
+// DOM node type constants.
+var DOM_NODE_TYPE_ELEMENT = 1;
+var DOM_NODE_TYPE_TEXT = 3;
+var DOM_NODE_TYPE_COMMENT = 8;
+
+// Test parameter names
+var PARAM_DESCRIPTION = 'desc';
+var PARAM_PAD = 'pad';
+var PARAM_EXECCOMMAND = 'command';
+var PARAM_FUNCTION = 'function';
+var PARAM_QUERYCOMMANDSUPPORTED = 'qcsupported';
+var PARAM_QUERYCOMMANDENABLED = 'qcenabled';
+var PARAM_QUERYCOMMANDINDETERM = 'qcindeterm';
+var PARAM_QUERYCOMMANDSTATE = 'qcstate';
+var PARAM_QUERYCOMMANDVALUE = 'qcvalue';
+var PARAM_VALUE = 'value';
+var PARAM_EXPECTED = 'expected';
+var PARAM_EXPECTED_OUTER = 'expOuter';
+var PARAM_ACCEPT = 'accept';
+var PARAM_ACCEPT_OUTER = 'accOuter';
+var PARAM_CHECK_ATTRIBUTES = 'checkAttrs';
+var PARAM_CHECK_STYLE = 'checkStyle';
+var PARAM_CHECK_CLASS = 'checkClass';
+var PARAM_CHECK_ID = 'checkID';
+var PARAM_STYLE_WITH_CSS = 'styleWithCSS';
+
+// ID suffixes for the output columns
+var IDOUT_TR = '_:TR:'; // per container
+var IDOUT_TESTID = '_:tid'; // per test
+var IDOUT_COMMAND = '_:cmd'; // per test
+var IDOUT_VALUE = '_:val'; // per test
+var IDOUT_CHECKATTRS = '_:att'; // per test
+var IDOUT_CHECKSTYLE = '_:sty'; // per test
+var IDOUT_CONTAINER = '_:cnt:'; // per container
+var IDOUT_STATUSVAL = '_:sta:'; // per container
+var IDOUT_STATUSSEL = '_:sel:'; // per container
+var IDOUT_PAD = '_:pad'; // per test
+var IDOUT_EXPECTED = '_:exp'; // per test
+var IDOUT_ACTUAL = '_:act:'; // per container
+
+// Output strings to use for yes/no/NA
+var OUTSTR_YES = '&#x25CF;';
+var OUTSTR_NO = '&#x25CB;';
+var OUTSTR_NA = '-';
+
+// Tags at the start of HTML strings where they were taken from
+var HTMLTAG_BODY = 'B:';
+var HTMLTAG_OUTER = 'O:';
+var HTMLTAG_INNER = 'I:';
+
+// What to use for the canary
+var CANARY = 'CAN<br>ARY';
+
+// Containers for tests, and their associated DOM elements:
+// iframe, win, doc, body, elem
+var containers = [
+ { id: 'dM',
+ iframe: null,
+ win: null,
+ doc: null,
+ body: null,
+ editor: null,
+ tagOpen: '<body>',
+ tagClose: '</body>',
+ editorID: null,
+ canary: '',
+ },
+ { id: 'body',
+ iframe: null,
+ win: null,
+ doc: null,
+ body: null,
+ editor: null,
+ tagOpen: '<body contenteditable="true">',
+ tagClose: '</body>',
+ editorID: null,
+ canary: ''
+ },
+ { id: 'div',
+ iframe: null,
+ win: null,
+ doc: null,
+ body: null,
+ editor: null,
+ tagOpen: '<div contenteditable="true" id="editor-div">',
+ tagClose: '</div>',
+ editorID: 'editor-div',
+ canary: CANARY
+ }
+];
+
+// Helper variables to use in test functions
+var win = null; // window object to use for test functions
+var doc = null; // document object to use for test functions
+var body = null; // The <body> element of the current document
+var editor = null; // The contentEditable element (i.e., the <body> or <div>)
+var sel = null; // The current selection after the pad is set up
+
+// Canonicalization emit flags for various purposes
+var emitFlagsForCanary = {
+ emitAttrs: true,
+ emitStyle: true,
+ emitClass: true,
+ emitID: true,
+ lowercase: true,
+ canonicalizeUnits: true
+};
+var emitFlagsForOutput = {
+ emitAttrs: true,
+ emitStyle: true,
+ emitClass: true,
+ emitID: true,
+ lowercase: false,
+ canonicalizeUnits: false
+};
+
+// Shades of output colors
+var colorShades = ['Lo', 'Hi'];
+
+// Classes of tests
+var testClassIDs = ['Finalized', 'RFC', 'Proposed'];
+var testClassCount = testClassIDs.length;
+
+// Dictionary storing the detailed test results.
+var results = {
+ count: 0,
+ score: 0
+};
+
+// Results - populated by the fillResults() function.
+var beacon = [];
+
+// "compatibility" between Python and JS for test quines
+var True = true;
+var False = false;
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html
new file mode 100644
index 0000000000..62d917d697
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html
@@ -0,0 +1,138 @@
+<!-- Legend -->
+<TABLE CLASS="legend framed">
+ <THEAD>
+ <TR><TH COLSPAN=3 CLASS="legendHdr">Result Description</TH></TR>
+ <TR><TH>Status</TH><TH ALIGN="LEFT">Meaning</TH><TH ALIGN="LEFT">Explanation</TH><TH>Scoring</TH></TR>
+ </THEAD>
+ <TBODY>
+ <TR CLASS="lo"><TD CLASS="pass" ALIGN="CENTER">&nbsp;PASS&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Passed</TD><TD CLASS="legend" ROWSPAN=2>The result matches the expectation.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="pass">PASS (+1)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="pass" ALIGN="CENTER">&nbsp;PASS&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="accept" ALIGN="CENTER">&nbsp;ACC.&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Acceptable</TD><TD CLASS="legend" ROWSPAN=2>The result is technically correct, but not ideal (too verbose, deprecated usage, etc.) - for informative purposes only.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="accept" ALIGN="CENTER">&nbsp;ACC.&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="fail" ALIGN="CENTER">&nbsp;FAIL&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Failure</TD><TD CLASS="legend" ROWSPAN=2>The result does not match any given expectation.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="fail" ALIGN="CENTER">&nbsp;FAIL&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="canary" ALIGN="CENTER">&nbsp;CANARY&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Canary</TD><TD CLASS="legend" ROWSPAN=2>The result changes HTML other than children of the contentEditable element.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="canary" ALIGN="CENTER">&nbsp;CANARY&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="unsupported" ALIGN="CENTER">&nbsp;UNS.&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Unsupported</TD><TD CLASS="legend" ROWSPAN=2>The specific function or value is unsupported (returned boolean 'false').</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="unsupported" ALIGN="CENTER">&nbsp;UNS.&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="exception" ALIGN="CENTER">&nbsp;EXC.&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Exception</TD><TD CLASS="legend" ROWSPAN=2>An unexpected exception was thrown during the execution of the test.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="exception" ALIGN="CENTER">&nbsp;EXC.&nbsp;</TD></TR>
+ <TR CLASS="lo"><TD CLASS="na" ALIGN="CENTER">&nbsp;N/A&nbsp;</TD><TD CLASS="legend" ROWSPAN=2>Not Applicable</TD><TD CLASS="legend" ROWSPAN=2>The selection could not be tested, because the tested function failed to return a known result.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR>
+ <TR CLASS="hi"><TD CLASS="na" ALIGN="CENTER">&nbsp;N/A&nbsp;</TD></TR>
+ </TBODY>
+</TABLE>
+<TABLE CLASS="legend framed">
+ <THEAD>
+ <TR><TH COLSPAN=2 CLASS="legendHdr">Selection and Result Display</TH></TR>
+ <TR><TH>Character</TH><TH ALIGN="LEFT">Explanation</TH></TR>
+ </THEAD>
+ <TBODY>
+ <TR><TD CLASS="sel" ALIGN="CENTER">[</TD><TD>Start of selection - selection point is within a text node.</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">]</TD><TD>End of selection - selection point is within a text node.</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">^</TD><TD>Collapsed selection - selection point is within a text node.</TD></TR>
+ <TR><TD COLSPAN=2>&nbsp;</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">{</TD><TD>Start of selection - selection point is within an element node.</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">}</TD><TD>End of selection - selection point is within an element node.</TD></TR>
+ <TR><TD CLASS="sel" ALIGN="CENTER">|</TD><TD>Collapsed selection - selection point is within an element node.</TD></TR>
+ <TR><TD COLSPAN=2>&nbsp;</TD></TR>
+ <TR><TD ALIGN="CENTER"><SPAN CLASS="fade">foo</SPAN></TD><TD>Greyed text indicates parts of the output that are ignored for the purposes of checking the result.</TD></TR>
+ <TR><TD ALIGN="CENTER"><SPAN CLASS="txt">foo</SPAN></TD><TD>Grey border indicates extent of text nodes in the result.</TD></TR>
+ </TBODY>
+</TABLE>
+<!-- progress meter -->
+<HR ID="divider">
+<H1>Running Test Suites: {% for s in suites %}<A HREF="#{{ s.id }}" ID="{{ s.id }}-progress" STYLE="color: #eeeeee">{{ s.id }}</A> {% endfor %}<SPAN ID="done">&nbsp;</SPAN></H1>
+<HR>
+<!-- main output -->
+{% for s in suites %}
+ <H1 ID="{{ s.id }}"><A NAME="{{ s.id }}" HREF="#{{ s.id }}">{{ s.id }}</A> - {{ s.caption }}:
+ <SPAN ID="{{ s.id }}-{% ifequal s.id.0 'S' %}sel{% endifequal %}score">?/?</SPAN>
+ {% ifnotequal s.id.0 "Q" %}{% ifnotequal s.id.0 "S" %}
+ (Selection: <SPAN ID="{{ s.id }}-selscore">?/?</SPAN>)
+ {% endifnotequal %}{% endifnotequal %}
+ (time: <SPAN ID="{{ s.id }}-time">?</SPAN>&nbsp;ms)
+ </H1>
+ {% if s.comment %}
+ <DIV CLASS="comment">{{ s.comment|safe }}</DIV>
+ {% endif %}
+ {% for cls in classes %}{% for pk, pv in s.items %}{% ifequal pk cls %}
+ <H2 ID="{{ s.id }}-{{ cls }}"><A NAME="{{ s.id }}-{{ cls }}" HREF="#{{ s.id }}-{{ cls }}">{{ cls }} Tests</A>:
+ <SPAN ID="{{ s.id }}-{{ cls }}-{% ifequal s.id.0 'S' %}sel{% endifequal %}score">?/?</SPAN>
+ {% ifnotequal s.id.0 "Q" %}{% ifnotequal s.id.0 "S" %}
+ (Selection: <SPAN ID="{{ s.id }}-{{ cls }}-selscore">?/?</SPAN>)
+ {% endifnotequal %}{% endifnotequal %}
+ </H2>
+ <TABLE WIDTH=100%>
+ <THEAD>
+ <TR>
+ <TH TITLE="Unique ID of the test" ALIGN="LEFT">ID</TH>
+ <TH TITLE="Command or function used in the test" ALIGN="LEFT">Command</TH>
+ <TH TITLE="Value field for commands" ALIGN="LEFT">Value</TH>
+ {% ifnotequal s.id.0 "S" %}{% ifnotequal s.id.0 "Q" %}{% comment %} Don't output attribute and style columns for selection and "queryCommand..." tests. {% endcomment %}
+ <TH TITLE="check Atributes?">A</TH>
+ <TH TITLE="check Style">S</TH>
+ {% endifnotequal %}{% endifnotequal %}
+ <TH TITLE="Testing HTML Element">Env.</TH>
+ {% ifnotequal s.id.0 "S" %}{% comment %} Don't output HTML status column for selection tests. {% endcomment %}
+ <TH TITLE="State of the test">Status</TH>
+ {% endifnotequal %}
+ {% ifnotequal s.id.0 "Q" %}{% comment %} Don't output selection result column for "queryCommand..." tests. {% endcomment %}
+ <TH TITLE="State of the test regarding the selection">Selection</TH>
+ {% endifnotequal %}
+ <TH TITLE="Initial HTML and selection" ALIGN="LEFT">Initial</TH>
+ <TH TITLE="Expected HTML and selection" ALIGN="LEFT">Expected</TH>
+ <TH TITLE="Actual result HTML and selection" ALIGN="LEFT">Actual (lower case, canonicalized, selection marks)</TH>
+ <TH TITLE="Short description of the test" ALIGN="LEFT">Description</TH>
+ </TR>
+ </THEAD>
+ <TBODY>
+ {% for g in pv %}{% for t in g.tests %}
+ <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:dM" CLASS="{% cycle 'lo' 'lo' 'lo' 'hi' 'hi' 'hi' as shade %}">
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:tid"><A CLASS="idLabel" NAME="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}" HREF="#{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}">{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}</A></TD>
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cmd">&nbsp;</TD>
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:val">&nbsp;</TD>
+ {% ifnotequal s.id.0 "S" %}{% ifnotequal s.id.0 "Q" %}{% comment %} Don't output attribute and style columns for selection and "queryCommand..." tests. {% endcomment %}
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:att" ALIGN="CENTER">&nbsp;</TD>
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sty" ALIGN="CENTER">&nbsp;</TD>
+ {% endifnotequal %}{% endifnotequal %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:dM" TITLE="designMode=&quot;on&quot;" ALIGN="CENTER">dM</TD>
+ {% ifnotequal s.id.0 "S" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:dM" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ {% ifnotequal s.id.0 "Q" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:dM" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:pad">&nbsp;</TD>
+ <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:exp">&nbsp;</TD>
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:dM"><I>Processing...</I></TD>
+ <TD ROWSPAN=3>{{ t.desc|default:"&nbsp;" }}</TD>
+ </TR>
+ <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:body" CLASS="{% cycle shade %}">
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:body" TITLE="&lt;body contentEditable=&quot;true&quot;&gt;" ALIGN="CENTER">body</TD>
+ {% ifnotequal s.id.0 "S" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:body" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ {% ifnotequal s.id.0 "Q" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:body" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:body"><I>Processing...</I></TD>
+ </TR>
+ <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:div" CLASS="{% cycle shade %}">
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:div" TITLE="&lt;div contentEditable=&quot;true&quot;&gt;" ALIGN="CENTER">div</TD>
+ {% ifnotequal s.id.0 "S" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:div" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ {% ifnotequal s.id.0 "Q" %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:div" ALIGN="CENTER">NONE</TD>
+ {% endifnotequal %}
+ <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:div"><I>Processing...</I></TD>
+ </TR>
+ {% endfor %}{% endfor %}
+ </TBODY>
+ </TABLE>
+ {% endifequal %}{% endfor %}{% endfor %}
+{% endfor %}
+
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html
new file mode 100644
index 0000000000..98de8796da
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <title>New Rich Text Tests</title>
+
+ <link rel="stylesheet" href="static/common.css" type="text/css">
+ <link rel="stylesheet" href="static/editable.css" type="text/css">
+
+ <!-- utility scripts -->
+ <script src="static/js/variables.js"></script>
+
+ <script src="static/js/canonicalize.js"></script>
+ <script src="static/js/compare.js"></script>
+ <script src="static/js/output.js"></script>
+ <script src="static/js/pad.js"></script>
+ <script src="static/js/range.js"></script>
+ <script src="static/js/units.js"></script>
+
+ <script src="static/js/run.js"></script>
+
+ <!-- new tests -->
+ <script type="text/javascript">
+ {% autoescape off %}
+
+ var commonIDPrefix = '{{ commonIDPrefix }}';
+ {% for s in suites %}
+ var {{ s.id }}_TESTS = {{ s }};
+ {% endfor %}
+
+ /**
+ * Stuff to do after all tests are run:
+ * - write a nice "DONE!" at the end of the progress meter
+ * - beacon the results
+ * - remove the testing <iframe>s
+ */
+ function finish() {
+ var span = document.getElementById('done');
+ if (span)
+ span.innerHTML = ' ... DONE!';
+
+ fillResults();
+ parent.sendScore(beacon, categoryTotals);
+
+ cleanUp();
+ }
+
+ /**
+ * Run every individual suite, with a a brief timeout in between
+ * to allow for screen updates.
+ */
+{% for s in suites %}
+ {% if not forloop.first %}
+ setTimeout("runSuite{{ s.id }}()", 100);
+ }
+ {% endif %}
+
+ function runSuite{{ s.id }}() {
+ runAndOutputTestSuite({{ s.id }}_TESTS);
+{% endfor %}
+ finish();
+ }
+
+ /**
+ * Runs all tests in all suites.
+ */
+ function doRunTests() {
+ initVariables();
+ initEditorDocs();
+
+ // Start with the first test suite
+ runSuite{{ suites.0.id }}();
+ }
+
+ /**
+ * Runs after allowing for some time to have everything loaded
+ * (aka. horrible IE9 kludge)
+ */
+ function runTests() {
+ setTimeout("doRunTests()", 1500);
+ }
+
+ /**
+ * Removes the <iframe>s after all tests are finished
+ */
+ function cleanUp() {
+ var e = document.getElementById('iframe-dM');
+ e.parentNode.removeChild(e);
+ e = document.getElementById('iframe-body');
+ e.parentNode.removeChild(e);
+ e = document.getElementById('iframe-div');
+ e.parentNode.removeChild(e);
+ }
+ {% endautoescape %}
+ </script>
+</head>
+
+<body onload="runTests()">
+ {% include "richtext2/templates/output.html" %}
+ <hr>
+ <iframe name="iframe-dM" id="iframe-dM" src="static/editable-dM.html"></iframe>
+ <iframe name="iframe-body" id="iframe-body" src="static/editable-body.html"></iframe>
+ <iframe name="iframe-div" id="iframe-div" src="static/editable-div.html"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py
new file mode 100644
index 0000000000..a1f5279ad5
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py
@@ -0,0 +1,17 @@
+__all__ = [
+ 'apply',
+ 'applyCSS',
+ 'change',
+ 'changeCSS',
+ 'delete',
+ 'forwarddelete',
+ 'insert',
+ 'queryEnabled',
+ 'queryIndeterm',
+ 'queryState',
+ 'querySupported',
+ 'queryValue',
+ 'selection',
+ 'unapply',
+ 'unapplyCSS'
+] \ No newline at end of file
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py
new file mode 100644
index 0000000000..3eb465c84c
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py
@@ -0,0 +1,364 @@
+
+APPLY_TESTS = {
+ 'id': 'A',
+ 'caption': 'Apply Formatting Tests',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] bold',
+ 'command': 'bold',
+ 'tests': [
+ { 'id': 'B_TEXT-1_SI',
+ 'rte1-id': 'a-bold-0',
+ 'desc': 'Bold selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<b>[bar]</b>baz',
+ 'foo<strong>[bar]</strong>baz' ] },
+
+ { 'id': 'B_TEXT-1_SIR',
+ 'desc': 'Bold reversed selection',
+ 'pad': 'foo]bar[baz',
+ 'expected': [ 'foo<b>[bar]</b>baz',
+ 'foo<strong>[bar]</strong>baz' ] },
+
+ { 'id': 'B_I-1_SL',
+ 'desc': 'Bold selection, partially including italic',
+ 'pad': 'foo[bar<i>baz]qoz</i>quz',
+ 'expected': [ 'foo<b>[bar</b><i><b>baz]</b>qoz</i>quz',
+ 'foo<b>[bar<i>baz]</i></b><i>qoz</i>quz',
+ 'foo<strong>[bar</strong><i><strong>baz]</strong>qoz</i>quz',
+ 'foo<strong>[bar<i>baz]</i></strong><i>qoz</i>quz' ] }
+ ]
+ },
+
+ { 'desc': '[HTML5] italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_TEXT-1_SI',
+ 'rte1-id': 'a-italic-0',
+ 'desc': 'Italicize selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<i>[bar]</i>baz',
+ 'foo<em>[bar]</em>baz' ] }
+ ]
+ },
+
+ { 'desc': '[HTML5] underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_TEXT-1_SI',
+ 'rte1-id': 'a-underline-0',
+ 'desc': 'Underline selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<u>[bar]</u>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] strikethrough',
+ 'command': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_TEXT-1_SI',
+ 'rte1-id': 'a-strikethrough-0',
+ 'desc': 'Strike-through selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<s>[bar]</s>baz',
+ 'foo<strike>[bar]</strike>baz',
+ 'foo<del>[bar]</del>baz' ] }
+ ]
+ },
+
+ { 'desc': '[HTML5] subscript',
+ 'command': 'subscript',
+ 'tests': [
+ { 'id': 'SUB_TEXT-1_SI',
+ 'rte1-id': 'a-subscript-0',
+ 'desc': 'Change selection to subscript',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<sub>[bar]</sub>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] superscript',
+ 'command': 'superscript',
+ 'tests': [
+ { 'id': 'SUP_TEXT-1_SI',
+ 'rte1-id': 'a-superscript-0',
+ 'desc': 'Change selection to superscript',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<sup>[bar]</sup>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] createlink',
+ 'command': 'createlink',
+ 'tests': [
+ { 'id': 'CL:url_TEXT-1_SI',
+ 'rte1-id': 'a-createlink-0',
+ 'desc': 'create a link around the selection',
+ 'value': '#foo',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<a href="#foo">[bar]</a>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] formatBlock',
+ 'command': 'formatblock',
+ 'tests': [
+ { 'id': 'FB:H1_TEXT-1_SI',
+ 'rte1-id': 'a-formatblock-0',
+ 'desc': 'format the selection into a block: use <h1>',
+ 'value': 'h1',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<h1>foo[bar]baz</h1>' },
+
+ { 'id': 'FB:P_TEXT-1_SI',
+ 'desc': 'format the selection into a block: use <p>',
+ 'value': 'p',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<p>foo[bar]baz</p>' },
+
+ { 'id': 'FB:PRE_TEXT-1_SI',
+ 'desc': 'format the selection into a block: use <pre>',
+ 'value': 'pre',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<pre>foo[bar]baz</pre>' },
+
+ { 'id': 'FB:ADDRESS_TEXT-1_SI',
+ 'desc': 'format the selection into a block: use <address>',
+ 'value': 'address',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<address>foo[bar]baz</address>' },
+
+ { 'id': 'FB:BQ_TEXT-1_SI',
+ 'desc': 'format the selection into a block: use <blockquote>',
+ 'value': 'blockquote',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<blockquote>foo[bar]baz</blockquote>' },
+
+ { 'id': 'FB:BQ_BR.BR-1_SM',
+ 'desc': 'format a multi-line selection into a block: use <blockquote>',
+ 'command': 'formatblock',
+ 'value': 'blockquote',
+ 'pad': 'fo[o<br>bar<br>b]az',
+ 'expected': '<blockquote>fo[o<br>bar<br>b]az</blockquote>' }
+ ]
+ },
+
+
+ { 'desc': '[MIDAS] backcolor',
+ 'command': 'backcolor',
+ 'tests': [
+ { 'id': 'BC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-backcolor-0',
+ 'desc': 'Change background color (note: no non-CSS variant available)',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz',
+ 'foo<font style="background-color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] forecolor',
+ 'command': 'forecolor',
+ 'tests': [
+ { 'id': 'FC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-forecolor-0',
+ 'desc': 'Change the text color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font color="blue">[bar]</font>baz' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] hilitecolor',
+ 'command': 'hilitecolor',
+ 'tests': [
+ { 'id': 'HC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-hilitecolor-0',
+ 'desc': 'Change the hilite color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz',
+ 'foo<font style="background-color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontname',
+ 'command': 'fontname',
+ 'tests': [
+ { 'id': 'FN:a_TEXT-1_SI',
+ 'rte1-id': 'a-fontname-0',
+ 'desc': 'Change the font name',
+ 'value': 'arial',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font face="arial">[bar]</font>baz' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontsize',
+ 'command': 'fontsize',
+ 'tests': [
+ { 'id': 'FS:2_TEXT-1_SI',
+ 'rte1-id': 'a-fontsize-0',
+ 'desc': 'Change the font size to "2"',
+ 'value': '2',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font size="2">[bar]</font>baz' },
+
+ { 'id': 'FS:18px_TEXT-1_SI',
+ 'desc': 'Change the font size to "18px"',
+ 'value': '18px',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font size="18px">[bar]</font>baz' },
+
+ { 'id': 'FS:large_TEXT-1_SI',
+ 'desc': 'Change the font size to "large"',
+ 'value': 'large',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<font size="large">[bar]</font>baz' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] increasefontsize',
+ 'command': 'increasefontsize',
+ 'tests': [
+ { 'id': 'INCFS:2_TEXT-1_SI',
+ 'desc': 'Decrease the font size (to small)',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<font size="4">[bar]</font>baz',
+ 'foo<font size="+1">[bar]</font>baz',
+ 'foo<big>[bar]</big>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] decreasefontsize',
+ 'command': 'decreasefontsize',
+ 'tests': [
+ { 'id': 'DECFS:2_TEXT-1_SI',
+ 'rte1-id': 'a-decreasefontsize-0',
+ 'desc': 'Decrease the font size (to small)',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<font size="2">[bar]</font>baz',
+ 'foo<font size="-1">[bar]</font>baz',
+ 'foo<small>[bar]</small>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] indent (note: accept the de-facto standard indent of 40px)',
+ 'command': 'indent',
+ 'tests': [
+ { 'id': 'IND_TEXT-1_SI',
+ 'rte1-id': 'a-indent-0',
+ 'desc': 'Indent the text (accept the de-facto standard of 40px indent)',
+ 'pad': 'foo[bar]baz',
+ 'checkAttrs': False,
+ 'expected': [ '<blockquote>foo[bar]baz</blockquote>',
+ '<div style="margin-left: 40px">foo[bar]baz</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="margin-left: 40px">foo[bar]baz</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] outdent (-> unapply tests)',
+ 'command': 'outdent',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifycenter',
+ 'command': 'justifycenter',
+ 'tests': [
+ { 'id': 'JC_TEXT-1_SC',
+ 'rte1-id': 'a-justifycenter-0',
+ 'desc': 'justify the text centrally',
+ 'pad': 'foo^bar',
+ 'expected': [ '<center>foo^bar</center>',
+ '<p align="center">foo^bar</p>',
+ '<p align="middle">foo^bar</p>',
+ '<div align="center">foo^bar</div>',
+ '<div align="middle">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': [ '<div align="center" contenteditable="true">foo^bar</div>',
+ '<div align="middle" contenteditable="true">foo^bar</div>' ] } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyfull',
+ 'command': 'justifyfull',
+ 'tests': [
+ { 'id': 'JF_TEXT-1_SC',
+ 'rte1-id': 'a-justifyfull-0',
+ 'desc': 'justify the text fully',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p align="justify">foo^bar</p>',
+ '<div align="justify">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div align="justify" contenteditable="true">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyleft',
+ 'command': 'justifyleft',
+ 'tests': [
+ { 'id': 'JL_TEXT-1_SC',
+ 'rte1-id': 'a-justifyleft-0',
+ 'desc': 'justify the text left',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p align="left">foo^bar</p>',
+ '<div align="left">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div align="left" contenteditable="true">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyright',
+ 'command': 'justifyright',
+ 'tests': [
+ { 'id': 'JR_TEXT-1_SC',
+ 'rte1-id': 'a-justifyright-0',
+ 'desc': 'justify the text right',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p align="right">foo^bar</p>',
+ '<div align="right">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div align="right" contenteditable="true">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] heading',
+ 'command': 'heading',
+ 'tests': [
+ { 'id': 'H:H1_TEXT-1_SC',
+ 'desc': 'create a heading from the paragraph that contains the selection',
+ 'value': 'h1',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<h1>foo[bar]baz</h1>' }
+ ]
+ },
+
+
+ { 'desc': '[Other] createbookmark',
+ 'command': 'createbookmark',
+ 'tests': [
+ { 'id': 'CB:name_TEXT-1_SI',
+ 'rte1-id': 'a-createbookmark-0',
+ 'desc': 'create a bookmark (named link) around selection',
+ 'value': 'created',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<a name="created">[bar]</a>baz' }
+ ]
+ }
+ ]
+}
+
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py
new file mode 100644
index 0000000000..94cdad83fb
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py
@@ -0,0 +1,244 @@
+
+APPLY_TESTS_CSS = {
+ 'id': 'AC',
+ 'caption': 'Apply Formatting Tests, using styleWithCSS',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': True,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] bold',
+ 'command': 'bold',
+ 'tests': [
+ { 'id': 'B_TEXT-1_SI',
+ 'rte1-id': 'a-bold-1',
+ 'desc': 'Bold selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="font-weight: bold">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_TEXT-1_SI',
+ 'rte1-id': 'a-italic-1',
+ 'desc': 'Italicize selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="font-style: italic">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_TEXT-1_SI',
+ 'rte1-id': 'a-underline-1',
+ 'desc': 'Underline selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="text-decoration: underline">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] strikethrough',
+ 'command': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_TEXT-1_SI',
+ 'rte1-id': 'a-strikethrough-1',
+ 'desc': 'Strike-through selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="text-decoration: line-through">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] subscript',
+ 'command': 'subscript',
+ 'tests': [
+ { 'id': 'SUB_TEXT-1_SI',
+ 'rte1-id': 'a-subscript-1',
+ 'desc': 'Change selection to subscript',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="vertical-align: sub">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': '[HTML5] superscript',
+ 'command': 'superscript',
+ 'tests': [
+ { 'id': 'SUP_TEXT-1_SI',
+ 'rte1-id': 'a-superscript-1',
+ 'desc': 'Change selection to superscript',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span style="vertical-align: super">[bar]</span>baz' }
+ ]
+ },
+
+
+ { 'desc': '[MIDAS] backcolor',
+ 'command': 'backcolor',
+ 'tests': [
+ { 'id': 'BC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-backcolor-1',
+ 'desc': 'Change background color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz',
+ 'foo<font style="background-color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] forecolor',
+ 'command': 'forecolor',
+ 'tests': [
+ { 'id': 'FC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-forecolor-1',
+ 'desc': 'Change the text color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="color: blue">[bar]</span>baz',
+ 'foo<font style="color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] hilitecolor',
+ 'command': 'hilitecolor',
+ 'tests': [
+ { 'id': 'HC:blue_TEXT-1_SI',
+ 'rte1-id': 'a-hilitecolor-1',
+ 'desc': 'Change the hilite color',
+ 'value': 'blue',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz',
+ 'foo<font style="background-color: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontname',
+ 'command': 'fontname',
+ 'tests': [
+ { 'id': 'FN:a_TEXT-1_SI',
+ 'rte1-id': 'a-fontname-1',
+ 'desc': 'Change the font name',
+ 'value': 'arial',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="font-family: arial">[bar]</span>baz',
+ 'foo<font style="font-family: blue">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontsize',
+ 'command': 'fontsize',
+ 'tests': [
+ { 'id': 'FS:2_TEXT-1_SI',
+ 'rte1-id': 'a-fontsize-1',
+ 'desc': 'Change the font size to "2"',
+ 'value': '2',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="font-size: small">[bar]</span>baz',
+ 'foo<font style="font-size: small">[bar]</font>baz' ] },
+
+ { 'id': 'FS:18px_TEXT-1_SI',
+ 'desc': 'Change the font size to "18px"',
+ 'value': '18px',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="font-size: 18px">[bar]</span>baz',
+ 'foo<font style="font-size: 18px">[bar]</font>baz' ] },
+
+ { 'id': 'FS:large_TEXT-1_SI',
+ 'desc': 'Change the font size to "large"',
+ 'value': 'large',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<span style="font-size: large">[bar]</span>baz',
+ 'foo<font style="font-size: large">[bar]</font>baz' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] indent',
+ 'command': 'indent',
+ 'tests': [
+ { 'id': 'IND_TEXT-1_SI',
+ 'rte1-id': 'a-indent-1',
+ 'desc': 'Indent the text (assume "standard" 40px)',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ '<div style="margin-left: 40px">foo[bar]baz</div>',
+ '<div style="margin: 0 0 0 40px">foo[bar]baz</div>',
+ '<blockquote style="margin-left: 40px">foo[bar]baz</blockquote>',
+ '<blockquote style="margin: 0 0 0 40px">foo[bar]baz</blockquote>' ],
+ 'div': {
+ 'accOuter': [ '<div contenteditable="true" style="margin-left: 40px">foo[bar]baz</div>',
+ '<div contenteditable="true" style="margin: 0 0 0 40px">foo[bar]baz</div>' ] } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] outdent (-> unapply tests)',
+ 'command': 'outdent',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifycenter',
+ 'command': 'justifycenter',
+ 'tests': [
+ { 'id': 'JC_TEXT-1_SC',
+ 'rte1-id': 'a-justifycenter-1',
+ 'desc': 'justify the text centrally',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p style="text-align: center">foo^bar</p>',
+ '<div style="text-align: center">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="text-align: center">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyfull',
+ 'command': 'justifyfull',
+ 'tests': [
+ { 'id': 'JF_TEXT-1_SC',
+ 'rte1-id': 'a-justifyfull-1',
+ 'desc': 'justify the text fully',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p style="text-align: justify">foo^bar</p>',
+ '<div style="text-align: justify">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="text-align: justify">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyleft',
+ 'command': 'justifyleft',
+ 'tests': [
+ { 'id': 'JL_TEXT-1_SC',
+ 'rte1-id': 'a-justifyleft-1',
+ 'desc': 'justify the text left',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p style="text-align: left">foo^bar</p>',
+ '<div style="text-align: left">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="text-align: left">foo^bar</div>' } }
+ ]
+ },
+
+ { 'desc': '[MIDAS] justifyright',
+ 'command': 'justifyright',
+ 'tests': [
+ { 'id': 'JR_TEXT-1_SC',
+ 'rte1-id': 'a-justifyright-1',
+ 'desc': 'justify the text right',
+ 'pad': 'foo^bar',
+ 'expected': [ '<p style="text-align: right">foo^bar</p>',
+ '<div style="text-align: right">foo^bar</div>' ],
+ 'div': {
+ 'accOuter': '<div contenteditable="true" style="text-align: right">foo^bar</div>' } }
+ ]
+ }
+ ]
+}
+
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py
new file mode 100644
index 0000000000..6a76d3d5fd
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py
@@ -0,0 +1,273 @@
+
+CHANGE_TESTS = {
+ 'id': 'C',
+ 'caption': 'Change Existing Format to Different Format Tests',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_I-1_SL',
+ 'desc': 'Italicize partially italicized text',
+ 'pad': 'foo[bar<i>baz]</i>qoz',
+ 'expected': 'foo<i>[barbaz]</i>qoz' },
+
+ { 'id': 'I_B-I-1_SO',
+ 'desc': 'Italicize partially italicized text in bold context',
+ 'pad': '<b>foo[bar<i>baz</i>}</b>',
+ 'expected': '<b>foo<i>[barbaz]</i></b>' }
+ ]
+ },
+
+ { 'desc': '[HTML5] underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_U-1_SO',
+ 'desc': 'Underline partially underlined text',
+ 'pad': 'foo[bar<u>baz</u>qoz]quz',
+ 'expected': 'foo<u>[barbazqoz]</u>quz' },
+
+ { 'id': 'U_U-1_SL',
+ 'desc': 'Underline partially underlined text',
+ 'pad': 'foo[bar<u>baz]qoz</u>quz',
+ 'expected': 'foo<u>[barbaz]qoz</u>quz' },
+
+ { 'id': 'U_S-U-1_SO',
+ 'desc': 'Underline partially underlined text in striked context',
+ 'pad': '<s>foo[bar<u>baz</u>}</s>',
+ 'expected': '<s>foo<u>[barbaz]</u></s>' }
+ ]
+ },
+
+
+ { 'desc': '[MIDAS] backcolor',
+ 'command': 'backcolor',
+ 'tests': [
+ { 'id': 'BC:842_FONTs:bc:fca-1_SW',
+ 'rte1-id': 'c-backcolor-0',
+ 'desc': 'Change background color to new color',
+ 'value': '#884422',
+ 'pad': '<font style="background-color: #ffccaa">[foobarbaz]</font>',
+ 'expected': [ '<font style="background-color: #884422">[foobarbaz]</font>',
+ '<span style="background-color: #884422">[foobarbaz]</span>' ] },
+
+ { 'id': 'BC:00f_SPANs:bc:f00-1_SW',
+ 'rte1-id': 'c-backcolor-2',
+ 'desc': 'Change background color to new color',
+ 'value': '#0000ff',
+ 'pad': '<span style="background-color: #ff0000">[foobarbaz]</span>',
+ 'expected': [ '<font style="background-color: #0000ff">[foobarbaz]</font>',
+ '<span style="background-color: #0000ff">[foobarbaz]</span>' ] },
+
+ { 'id': 'BC:ace_FONT.ass.s:bc:rgb-1_SW',
+ 'rte1-id': 'c-backcolor-1',
+ 'desc': 'Change background color in styled span to new color',
+ 'value': '#aaccee',
+ 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">[foobarbaz]</span>',
+ 'expected': [ '<font style="background-color: #aaccee">[foobarbaz]</font>',
+ '<span style="background-color: #aaccee">[foobarbaz]</span>' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] forecolor',
+ 'command': 'forecolor',
+ 'tests': [
+ { 'id': 'FC:g_FONTc:b-1_SW',
+ 'rte1-id': 'c-forecolor-0',
+ 'desc': 'Change the text color (without CSS)',
+ 'value': 'green',
+ 'pad': '<font color="blue">[foobarbaz]</font>',
+ 'expected': '<font color="green">[foobarbaz]</font>' },
+
+ { 'id': 'FC:g_SPANs:c:g-1_SW',
+ 'rte1-id': 'c-forecolor-1',
+ 'desc': 'Change the text color from a styled span (without CSS)',
+ 'value': 'green',
+ 'pad': '<span style="color: blue">[foobarbaz]</span>',
+ 'expected': '<font color="green">[foobarbaz]</font>' },
+
+ { 'id': 'FC:g_FONTc:b.s:c:r-1_SW',
+ 'rte1-id': 'c-forecolor-2',
+ 'desc': 'Change the text color from conflicting color and style (without CSS)',
+ 'value': 'green',
+ 'pad': '<font color="blue" style="color: red">[foobarbaz]</font>',
+ 'expected': '<font color="green">[foobarbaz]</font>' },
+
+ { 'id': 'FC:g_FONTc:b.sz:6-1_SI',
+ 'desc': 'Change the font color in content with a different font size and font color',
+ 'value': 'green',
+ 'pad': '<font color="blue" size="6">foo[bar]baz</font>',
+ 'expected': [ '<font color="blue" size="6">foo<font color="green">[bar]</font>baz</font>',
+ '<font size="6"><font color="blue">foo<font color="green">[bar]</font><font color="blue">baz</font></font>' ] }
+ ]
+ },
+
+ { 'desc': '[MIDAS] hilitecolor',
+ 'command': 'hilitecolor',
+ 'tests': [
+ { 'id': 'HC:g_FONTs:c:b-1_SW',
+ 'rte1-id': 'c-hilitecolor-0',
+ 'desc': 'Change the hilite color (without CSS)',
+ 'value': 'green',
+ 'pad': '<font style="background-color: blue">[foobarbaz]</font>',
+ 'expected': [ '<font style="background-color: green">[foobarbaz]</font>',
+ '<span style="background-color: green">[foobarbaz]</span>' ] },
+
+ { 'id': 'HC:g_SPANs:c:g-1_SW',
+ 'rte1-id': 'c-hilitecolor-2',
+ 'desc': 'Change the hilite color from a styled span (without CSS)',
+ 'value': 'green',
+ 'pad': '<span style="background-color: blue">[foobarbaz]</span>',
+ 'expected': '<span style="background-color: green">[foobarbaz]</span>' },
+
+ { 'id': 'HC:g_SPAN.ass.s:c:rgb-1_SW',
+ 'rte1-id': 'c-hilitecolor-1',
+ 'desc': 'Change the hilite color from a styled span (without CSS)',
+ 'value': 'green',
+ 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">[foobarbaz]</span>',
+ 'expected': '<span style="background-color: green">[foobarbaz]</span>' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontname',
+ 'command': 'fontname',
+ 'tests': [
+ { 'id': 'FN:c_FONTf:a-1_SW',
+ 'rte1-id': 'c-fontname-0',
+ 'desc': 'Change existing font name to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<font face="arial">[foobarbaz]</font>',
+ 'expected': '<font face="courier">[foobarbaz]</font>' },
+
+ { 'id': 'FN:c_SPANs:ff:a-1_SW',
+ 'rte1-id': 'c-fontname-1',
+ 'desc': 'Change existing font name from style to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<span style="font-family: arial">[foobarbaz]</span>',
+ 'expected': '<font face="courier">[foobarbaz]</font>' },
+
+ { 'id': 'FN:c_FONTf:a.s:ff:v-1_SW',
+ 'rte1-id': 'c-fontname-2',
+ 'desc': 'Change existing font name with conflicting face and style to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<font face="arial" style="font-family: verdana">[foobarbaz]</font>',
+ 'expected': '<font face="courier">[foobarbaz]</font>' },
+
+ { 'id': 'FN:c_FONTf:a-1_SI',
+ 'desc': 'Change existing font name to new font name, text partially selected',
+ 'value': 'courier',
+ 'pad': '<font face="arial">foo[bar]baz</font>',
+ 'expected': '<font face="arial">foo</font><font face="courier">[bar]</font><font face="arial">baz</font>',
+ 'accept': '<font face="arial">foo<font face="courier">[bar]</font>baz</font>' },
+
+ { 'id': 'FN:c_FONTf:a-2_SL',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': 'foo[bar<font face="arial">baz]qoz</font>',
+ 'expected': 'foo<font face="courier">[barbaz]</font><font face="arial">qoz</font>' },
+
+ { 'id': 'FN:c_FONTf:v-FONTf:a-1_SW',
+ 'rte1-id': 'c-fontname-3',
+ 'desc': 'Change existing font name in nested <font> tags to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<font face="verdana"><font face="arial">[foobarbaz]</font></font>',
+ 'expected': '<font face="courier">[foobarbaz]</font>',
+ 'accept': '<font face="verdana"><font face="courier">[foobarbaz]</font></font>' },
+
+ { 'id': 'FN:c_SPANs:ff:v-FONTf:a-1_SW',
+ 'rte1-id': 'c-fontname-4',
+ 'desc': 'Change existing font name in nested mixed tags to new font name (without CSS)',
+ 'value': 'courier',
+ 'pad': '<span style="font-family: verdana"><font face="arial">[foobarbaz]</font></span>',
+ 'expected': '<font face="courier">[foobarbaz]</font>',
+ 'accept': '<span style="font-family: verdana"><font face="courier">[foobarbaz]</font></span>' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontsize',
+ 'command': 'fontsize',
+ 'tests': [
+ { 'id': 'FS:1_FONTsz:4-1_SW',
+ 'rte1-id': 'c-fontsize-0',
+ 'desc': 'Change existing font size to new size (without CSS)',
+ 'value': '1',
+ 'pad': '<font size="4">[foobarbaz]</font>',
+ 'expected': '<font size="1">[foobarbaz]</font>' },
+
+ { 'id': 'FS:1_SPAN.ass.s:fs:large-1_SW',
+ 'rte1-id': 'c-fontsize-1',
+ 'desc': 'Change existing font size from styled span to new size (without CSS)',
+ 'value': '1',
+ 'pad': '<span class="Apple-style-span" style="font-size: large">[foobarbaz]</span>',
+ 'expected': '<font size="1">[foobarbaz]</font>' },
+
+ { 'id': 'FS:5_FONTsz:1.s:fs:xs-1_SW',
+ 'rte1-id': 'c-fontsize-2',
+ 'desc': 'Change existing font size from tag with conflicting size and style to new size (without CSS)',
+ 'value': '5',
+ 'pad': '<font size="1" style="font-size:x-small">[foobarbaz]</font>',
+ 'expected': '<font size="5">[foobarbaz]</font>' },
+
+ { 'id': 'FS:2_FONTc:b.sz:6-1_SI',
+ 'desc': 'Change the font size in content with a different font size and font color',
+ 'value': '2',
+ 'pad': '<font color="blue" size="6">foo[bar]baz</font>',
+ 'expected': [ '<font color="blue" size="6">foo<font size="2">[bar]</font>baz</font>',
+ '<font color="blue"><font size="6">foo</font><font size="2">[bar]</font><font size="6">baz</font></font>' ] },
+
+ { 'id': 'FS:larger_FONTsz:4',
+ 'desc': 'Change selection to use next larger font',
+ 'value': 'larger',
+ 'pad': '<font size="4">foo[bar]baz</font>',
+ 'expected': '<font size="4">foo<font size="larger">[bar]</font>baz</font>',
+ 'accept': '<font size="4">foo</font><font size="5">[bar]</font><font size="4">baz</font>' },
+
+ { 'id': 'FS:smaller_FONTsz:4',
+ 'desc': 'Change selection to use next smaller font',
+ 'value': 'smaller',
+ 'pad': '<font size="4">foo[bar]baz</font>',
+ 'expected': '<font size="4">foo<font size="smaller">[bar]</font>baz</font>',
+ 'accept': '<font size="4">foo</font><font size="3">[bar]</font><font size="4">baz</font>' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] formatblock',
+ 'command': 'formatblock',
+ 'tests': [
+ { 'id': 'FB:h1_ADDRESS-1_SW',
+ 'desc': 'change block from <address> to <h1>',
+ 'value': 'h1',
+ 'pad': '<address>foo [bar] baz</address>',
+ 'expected': '<h1>foo [bar] baz</h1>' },
+
+ { 'id': 'FB:h1_ADDRESS-FONTsz:4-1_SO',
+ 'desc': 'change block from <address> with partially formatted content to <h1>',
+ 'value': 'h1',
+ 'pad': '<address>foo [<font size="4">bar</font>] baz</address>',
+ 'expected': '<h1>foo [bar] baz</h1>' },
+
+ { 'id': 'FB:h1_ADDRESS-FONTsz:4-1_SW',
+ 'desc': 'change block from <address> with partially formatted content to <h1>',
+ 'value': 'h1',
+ 'pad': '<address>foo <font size="4">[bar]</font> baz</address>',
+ 'expected': '<h1>foo [bar] baz</h1>' },
+
+ { 'id': 'FB:h1_ADDRESS-FONT.ass.sz:4-1_SW',
+ 'desc': 'change block from <address> with partially formatted content to <h1>',
+ 'value': 'h1',
+ 'pad': '<address>foo <font class="Apple-style-span" size="4">[bar]</font> baz</address>',
+ 'expected': '<h1>foo [bar] baz</h1>' }
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py
new file mode 100644
index 0000000000..4862b9b733
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py
@@ -0,0 +1,210 @@
+
+CHANGE_TESTS_CSS = {
+ 'id': 'CC',
+ 'caption': 'Change Existing Format to Different Format Tests, using styleWithCSS',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': True,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_I-1_SL',
+ 'desc': 'Italicize partially italicized text',
+ 'pad': 'foo[bar<i>baz]</i>qoz',
+ 'expected': 'foo<span style="font-style: italic">[barbaz]</span>qoz' },
+
+ { 'id': 'I_B-1_SL',
+ 'desc': 'Italicize partially bolded text',
+ 'pad': 'foo[bar<b>baz]</b>qoz',
+ 'expected': 'foo<span style="font-style: italic">[bar<b>baz]</b></span>qoz',
+ 'accept': 'foo<span style="font-style: italic">[bar<b>baz</b>}</span>qoz' },
+
+ { 'id': 'I_B-1_SW',
+ 'desc': 'Italicize bold text, ideally combining both',
+ 'pad': 'foobar<b>[baz]</b>qoz',
+ 'expected': 'foobar<span style="font-style: italic; font-weight: bold">[baz]</span>qoz',
+ 'accept': 'foobar<b><span style="font-style: italic">[baz]</span></b>qoz' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] backcolor',
+ 'command': 'backcolor',
+ 'tests': [
+ { 'id': 'BC:gray_SPANs:bc:b-1_SW',
+ 'desc': 'Change background color from blue to gray',
+ 'value': 'gray',
+ 'pad': '<span style="background-color: blue">[foobarbaz]</span>',
+ 'expected': '<span style="background-color: gray">[foobarbaz]</span>' },
+
+ { 'id': 'BC:gray_SPANs:bc:b-1_SO',
+ 'desc': 'Change background color from blue to gray',
+ 'value': 'gray',
+ 'pad': '{<span style="background-color: blue">foobarbaz</span>}',
+ 'expected': [ '{<span style="background-color: gray">foobarbaz</span>}',
+ '<span style="background-color: gray">[foobarbaz]</span>' ] },
+
+ { 'id': 'BC:gray_SPANs:bc:b-1_SI',
+ 'desc': 'Change background color from blue to gray',
+ 'value': 'gray',
+ 'pad': '<span style="background-color: blue">foo[bar]baz</span>',
+ 'expected': '<span style="background-color: blue">foo</span><span style="background-color: gray">[bar]</span><span style="background-color: blue">baz</span>',
+ 'accept': '<span style="background-color: blue">foo<span style="background-color: gray">[bar]</span>baz</span>' },
+
+ { 'id': 'BC:gray_P-SPANs:bc:b-1_SW',
+ 'desc': 'Change background color within a paragraph from blue to gray',
+ 'value': 'gray',
+ 'pad': '<p><span style="background-color: blue">[foobarbaz]</span></p>',
+ 'expected': [ '<p><span style="background-color: gray">[foobarbaz]</span></p>',
+ '<p style="background-color: gray">[foobarbaz]</p>' ] },
+
+ { 'id': 'BC:gray_P-SPANs:bc:b-2_SW',
+ 'desc': 'Change background color within a paragraph from blue to gray',
+ 'value': 'gray',
+ 'pad': '<p>foo<span style="background-color: blue">[bar]</span>baz</p>',
+ 'expected': '<p>foo<span style="background-color: gray">[bar]</span>baz</p>' },
+
+ { 'id': 'BC:gray_P-SPANs:bc:b-3_SO',
+ 'desc': 'Change background color within a paragraph from blue to gray (selection encloses more than previous span)',
+ 'value': 'gray',
+ 'pad': '<p>[foo<span style="background-color: blue">barbaz</span>qoz]quz</p>',
+ 'expected': '<p><span style="background-color: gray">[foobarbazqoz]</span>quz</p>' },
+
+ { 'id': 'BC:gray_P-SPANs:bc:b-3_SL',
+ 'desc': 'Change background color within a paragraph from blue to gray (previous span partially selected)',
+ 'value': 'gray',
+ 'pad': '<p>[foo<span style="background-color: blue">bar]baz</span>qozquz</p>',
+ 'expected': '<p><span style="background-color: gray">[foobar]</span><span style="background-color: blue">baz</span>qozquz</p>' },
+
+ { 'id': 'BC:gray_SPANs:bc:b-2_SL',
+ 'desc': 'Change background color from blue to gray on partially covered span, selection extends left',
+ 'value': 'gray',
+ 'pad': 'foo [bar <span style="background-color: blue">baz] qoz</span> quz sic',
+ 'expected': 'foo <span style="background-color: gray">[bar baz]</span><span style="background-color: blue"> qoz</span> quz sic' },
+
+ { 'id': 'BC:gray_SPANs:bc:b-2_SR',
+ 'desc': 'Change background color from blue to gray on partially covered span, selection extends right',
+ 'value': 'gray',
+ 'pad': 'foo bar <span style="background-color: blue">baz [qoz</span> quz] sic',
+ 'expected': 'foo bar <span style="background-color: blue">baz </span><span style="background-color: gray">[qoz quz]</span> sic' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontname',
+ 'command': 'fontname',
+ 'tests': [
+ { 'id': 'FN:c_SPANs:ff:a-1_SW',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': '<span style="font-family: arial">[foobarbaz]</span>',
+ 'expected': '<span style="font-family: courier">[foobarbaz]</span>' },
+
+ { 'id': 'FN:c_FONTf:a-1_SW',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': '<font face="arial">[foobarbaz]</font>',
+ 'expected': [ '<font style="font-family: courier">[foobarbaz]</font>',
+ '<span style="font-family: courier">[foobarbaz]</span>' ] },
+
+ { 'id': 'FN:c_FONTf:a-1_SI',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': '<font face="arial">foo[bar]baz</font>',
+ 'expected': '<font face="arial">foo</font><span style="font-family: courier">[bar]</span><font face="arial">baz</font>' },
+
+ { 'id': 'FN:a_FONTf:a-1_SI',
+ 'desc': 'Change existing font name to same font name, using CSS styling (should be noop)',
+ 'value': 'arial',
+ 'pad': '<font face="arial">foo[bar]baz</font>',
+ 'expected': '<font face="arial">foo[bar]baz</font>' },
+
+ { 'id': 'FN:a_FONTf:a-1_SW',
+ 'desc': 'Change existing font name to same font name, using CSS styling (should be noop or perhaps change tag)',
+ 'value': 'arial',
+ 'pad': '<font face="arial">[foobarbaz]</font>',
+ 'expected': [ '<font face="arial">[foobarbaz]</font>',
+ '<span style="font-family: arial">[foobarbaz]</span>' ] },
+
+ { 'id': 'FN:a_FONTf:a-1_SO',
+ 'desc': 'Change existing font name to same font name, using CSS styling (should be noop or perhaps change tag)',
+ 'value': 'arial',
+ 'pad': '{<font face="arial">foobarbaz</font>}',
+ 'expected': [ '{<font face="arial">foobarbaz</font>}',
+ '<font face="arial">[foobarbaz]</font>',
+ '{<span style="font-family: arial">foobarbaz</span>}',
+ '<span style="font-family: arial">[foobarbaz]</span>' ] },
+
+ { 'id': 'FN:a_SPANs:ff:a-1_SI',
+ 'desc': 'Change existing font name to same font name, using CSS styling (should be noop)',
+ 'value': 'arial',
+ 'pad': '<span style="font-family: arial">[foobarbaz]</span>',
+ 'expected': '<span style="font-family: arial">[foobarbaz]</span>' },
+
+ { 'id': 'FN:c_FONTf:a-2_SL',
+ 'desc': 'Change existing font name to new font name, using CSS styling',
+ 'value': 'courier',
+ 'pad': 'foo[bar<font face="arial">baz]qoz</font>',
+ 'expected': 'foo<span style="font-family: courier">[barbaz]</span><font face="arial">qoz</font>' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] fontsize',
+ 'command': 'fontsize',
+ 'tests': [
+ { 'id': 'FS:1_SPANs:fs:l-1_SW',
+ 'desc': 'Change existing font size to new size, using CSS styling',
+ 'value': '1',
+ 'pad': '<span style="font-size: large">[foobarbaz]</span>',
+ 'expected': '<span style="font-size: x-small">[foobarbaz]</span>' },
+
+ { 'id': 'FS:large_SPANs:fs:l-1_SW',
+ 'desc': 'Change existing font size to same size (should be noop)',
+ 'value': 'large',
+ 'pad': '<span style="font-size: large">[foobarbaz]</span>',
+ 'expected': '<span style="font-size: large">[foobarbaz]</span>' },
+
+ { 'id': 'FS:18px_SPANs:fs:l-1_SW',
+ 'desc': 'Change existing font size to equivalent px size (should be noop, or change unit)',
+ 'value': '18px',
+ 'pad': '<span style="font-size: large">[foobarbaz]</span>',
+ 'expected': [ '<span style="font-size: 18px">[foobarbaz]</span>',
+ '<span style="font-size: large">[foobarbaz]</span>' ] },
+
+ { 'id': 'FS:4_SPANs:fs:l-1_SW',
+ 'desc': 'Change existing font size to equivalent numeric size (should be noop)',
+ 'value': '4',
+ 'pad': '<span style="font-size: large">[foobarbaz]</span>',
+ 'expected': '<span style="font-size: large">[foobarbaz]</span>' },
+
+ { 'id': 'FS:4_SPANs:fs:18px-1_SW',
+ 'desc': 'Change existing font size to equivalent numeric size (should be noop)',
+ 'value': '4',
+ 'pad': '<span style="font-size: 18px">[foobarbaz]</span>',
+ 'expected': '<span style="font-size: 18px">[foobarbaz]</span>' },
+
+ { 'id': 'FS:larger_SPANs:fs:l-1_SI',
+ 'desc': 'Change selection to use next larger font',
+ 'value': 'larger',
+ 'pad': '<span style="font-size: large">foo[bar]baz</span>',
+ 'expected': [ '<span style="font-size: large">foo<span style="font-size: x-large">[bar]</span>baz</span>',
+ '<span style="font-size: large">foo</span><span style="font-size: x-large">[bar]</span><span style="font-size: large">baz</span>' ],
+ 'accept': '<span style="font-size: large">foo<font size="larger">[bar]</font>baz</span>' },
+
+ { 'id': 'FS:smaller_SPANs:fs:l-1_SI',
+ 'desc': 'Change selection to use next smaller font',
+ 'value': 'smaller',
+ 'pad': '<span style="font-size: large">foo[bar]baz</span>',
+ 'expected': [ '<span style="font-size: large">foo<span style="font-size: medium">[bar]</span>baz</span>',
+ '<span style="font-size: large">foo</span><span style="font-size: medium">[bar]</span><span style="font-size: large">baz</span>' ],
+ 'accept': '<span style="font-size: large">foo<font size="smaller">[bar]</font>baz</span>' }
+ ]
+ }
+ ]
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py
new file mode 100644
index 0000000000..6fb81f76cc
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py
@@ -0,0 +1,330 @@
+
+DELETE_TESTS = {
+ 'id': 'D',
+ 'caption': 'Delete Tests',
+ 'command': 'delete',
+ 'checkAttrs': True,
+ 'checkStyle': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'delete single characters',
+ 'tests': [
+ { 'id': 'CHAR-1_SC',
+ 'desc': 'Delete 1 character',
+ 'pad': 'foo^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-2_SC',
+ 'desc': 'Delete 1 pre-composed character o with diaeresis',
+ 'pad': 'fo&#xF6;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-3_SC',
+ 'desc': 'Delete 1 character with combining diaeresis above',
+ 'pad': 'foo&#x0308;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-4_SC',
+ 'desc': 'Delete 1 character with combining diaeresis below',
+ 'pad': 'foo&#x0324;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SC',
+ 'desc': 'Delete 1 character with combining diaeresis above and below',
+ 'pad': 'foo&#x0308;&#x0324;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SI-1',
+ 'desc': 'Delete 1 character with combining diaeresis above and below, selection on diaeresis above',
+ 'pad': 'foo[&#x0308;]&#x0324;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SI-2',
+ 'desc': 'Delete 1 character with combining diaeresis above and below, selection on diaeresis below',
+ 'pad': 'foo&#x0308;[&#x0324;]barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SR',
+ 'desc': 'Delete 1 character with combining diaeresis above and below, selection oblique on diaeresis and following text',
+ 'pad': 'foo&#x0308;[&#x0324;bar]baz',
+ 'expected': 'fo^baz' },
+
+ { 'id': 'CHAR-6_SC',
+ 'desc': 'Delete 1 character with enclosing square',
+ 'pad': 'foo&#x20DE;^barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-7_SC',
+ 'desc': 'Delete 1 character with combining long solidus overlay',
+ 'pad': 'foo&#x0338;^barbaz',
+ 'expected': 'fo^barbaz' }
+ ]
+ },
+
+ { 'desc': 'delete text selection',
+ 'tests': [
+ { 'id': 'TEXT-1_SI',
+ 'desc': 'Delete text selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SS',
+ 'desc': 'Delete at start of span',
+ 'pad': 'foo<b>^bar</b>baz',
+ 'expected': 'fo^<b>bar</b>baz' },
+
+ { 'id': 'B-1_SA',
+ 'desc': 'Delete from position after span',
+ 'pad': 'foo<b>bar</b>^baz',
+ 'expected': 'foo<b>ba^</b>baz' },
+
+ { 'id': 'B-1_SW',
+ 'desc': 'Delete selection that wraps the whole span content',
+ 'pad': 'foo<b>[bar]</b>baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SO',
+ 'desc': 'Delete selection that wraps the whole span',
+ 'pad': 'foo[<b>bar</b>]baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SL',
+ 'desc': 'Delete oblique selection that starts before span',
+ 'pad': 'foo[bar<b>baz]quoz</b>quuz',
+ 'expected': 'foo^<b>quoz</b>quuz' },
+
+ { 'id': 'B-1_SR',
+ 'desc': 'Delete oblique selection that ends after span',
+ 'pad': 'foo<b>bar[baz</b>quoz]quuz',
+ 'expected': 'foo<b>bar^</b>quuz' },
+
+ { 'id': 'B.I-1_SM',
+ 'desc': 'Delete oblique selection that starts and ends in different spans',
+ 'pad': 'foo<b>bar[baz</b><i>qoz]quuz</i>quuuz',
+ 'expected': 'foo<b>bar^</b><i>quuz</i>quuuz' },
+
+ { 'id': 'GEN-1_SS',
+ 'desc': 'Delete at start of span with generated content',
+ 'pad': 'foo<gen>^bar</gen>baz',
+ 'expected': 'fo^<gen>bar</gen>baz' },
+
+ { 'id': 'GEN-1_SA',
+ 'desc': 'Delete from position after span with generated content',
+ 'pad': 'foo<gen>bar</gen>^baz',
+ 'expected': 'foo<gen>ba^</gen>baz' }
+ ]
+ },
+
+ { 'desc': 'delete paragraphs',
+ 'tests': [
+ { 'id': 'P2-1_SS2',
+ 'desc': 'Delete from collapsed selection at start of paragraph - should merge with previous',
+ 'pad': '<p>foobar</p><p>^bazqoz</p>',
+ 'expected': '<p>foobar^bazqoz</p>' },
+
+ { 'id': 'P2-1_SI2',
+ 'desc': 'Delete non-collapsed selection at start of paragraph - should not merge with previous',
+ 'pad': '<p>foobar</p><p>[baz]qoz</p>',
+ 'expected': '<p>foobar</p><p>^qoz</p>' },
+
+ { 'id': 'P2-1_SM',
+ 'desc': 'Delete non-collapsed selection spanning 2 paragraphs - should merge them',
+ 'pad': '<p>foo[bar</p><p>baz]qoz</p>',
+ 'expected': '<p>foo^qoz</p>' }
+ ]
+ },
+
+ { 'desc': 'delete lists and list items',
+ 'tests': [
+ { 'id': 'OL-LI2-1_SO1',
+ 'desc': 'Delete fully wrapped list item',
+ 'pad': 'foo<ol>{<li>bar</li>}<li>baz</li></ol>qoz',
+ 'expected': ['foo<ol>|<li>baz</li></ol>qoz',
+ 'foo<ol><li>^baz</li></ol>qoz'] },
+
+ { 'id': 'OL-LI2-1_SM',
+ 'desc': 'Delete oblique range between list items within same list',
+ 'pad': 'foo<ol><li>ba[r</li><li>b]az</li></ol>qoz',
+ 'expected': 'foo<ol><li>ba^az</li></ol>qoz' },
+
+ { 'id': 'OL-LI-1_SW',
+ 'desc': 'Delete contents of last list item (list should remain)',
+ 'pad': 'foo<ol><li>[foo]</li></ol>qoz',
+ 'expected': ['foo<ol><li>|</li></ol>qoz',
+ 'foo<ol><li>^</li></ol>qoz'] },
+
+ { 'id': 'OL-LI-1_SO',
+ 'desc': 'Delete last list item of list (should remove entire list)',
+ 'pad': 'foo<ol>{<li>foo</li>}</ol>qoz',
+ 'expected': 'foo^qoz' }
+ ]
+ },
+
+ { 'desc': 'delete with strange selections',
+ 'tests': [
+ { 'id': 'HR.BR-1_SM',
+ 'desc': 'Delete selection that starts and ends within nodes that don\'t have children',
+ 'pad': 'foo<hr {>bar<br }>baz',
+ 'expected': 'foo<hr>|<br>baz' }
+ ]
+ },
+
+ { 'desc': 'delete after table',
+ 'tests': [
+ { 'id': 'TABLE-1_SA',
+ 'desc': 'Delete from position immediately after table (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>bar</td></tr></tbody></table>^baz',
+ 'expected': 'foo<table><tbody><tr><td>bar</td></tr></tbody></table>^baz' }
+ ]
+ },
+
+ { 'desc': 'delete within table cells',
+ 'tests': [
+ { 'id': 'TD-1_SS',
+ 'desc': 'Delete from start of first cell (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>^bar</td></tr></tbody></table>baz',
+ 'expected': 'foo<table><tbody><tr><td>^bar</td></tr></tbody></table>baz' },
+
+ { 'id': 'TD2-1_SS2',
+ 'desc': 'Delete from start of inner cell (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>bar</td><td>^baz</td></tr></tbody></table>quoz',
+ 'expected': 'foo<table><tbody><tr><td>bar</td><td>^baz</td></tr></tbody></table>quoz' },
+
+ { 'id': 'TD2-1_SM',
+ 'desc': 'Delete with selection spanning 2 cells',
+ 'pad': 'foo<table><tbody><tr><td>ba[r</td><td>b]az</td></tr></tbody></table>quoz',
+ 'expected': 'foo<table><tbody><tr><td>ba^</td><td>az</td></tr></tbody></table>quoz' }
+ ]
+ },
+
+ { 'desc': 'delete table rows',
+ 'tests': [
+ { 'id': 'TR3-1_SO1',
+ 'desc': 'Delete first table row',
+ 'pad': '<table><tbody>{<tr><td>A</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^B</td></tr><tr><td>C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3-1_SO2',
+ 'desc': 'Delete middle table row',
+ 'pad': '<table><tbody><tr><td>A</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td></tr>|<tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>A</td></tr><tr><td>^C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3-1_SO3',
+ 'desc': 'Delete last table row',
+ 'pad': '<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td></tr><tr><td>B^</td></tr></tbody></table>'] },
+
+ { 'id': 'TR2rs:2-1_SO1',
+ 'desc': 'Delete first table row where a cell has rowspan 2',
+ 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=2>R</td></tr>}<tr><td>B</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>B</td><td>R</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^B</td><td>R</td></tr></tbody></table>'] },
+
+ { 'id': 'TR2rs:2-1_SO2',
+ 'desc': 'Delete second table row where a cell has rowspan 2',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=2>R</td></tr>{<tr><td>B</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td><td>R</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td><td>R^</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO1',
+ 'desc': 'Delete first table row where a cell has rowspan 3',
+ 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=3>R</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO2',
+ 'desc': 'Delete middle table row where a cell has rowspan 3',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr>|<tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr><tr><td>^C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO3',
+ 'desc': 'Delete last table row where a cell has rowspan 3',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B^</td></tr></tbody></table>'] }
+ ]
+ },
+
+ { 'desc': 'delete with non-editable nested content',
+ 'tests': [
+ { 'id': 'DIV:ce:false-1_SO',
+ 'desc': 'Delete nested non-editable <div>',
+ 'pad': 'foo[bar<div contenteditable="false">NESTED</div>baz]qoz',
+ 'expected': 'foo^qoz' },
+
+ { 'id': 'DIV:ce:false-1_SB',
+ 'desc': 'Delete from immediately after a nested non-editable <div>',
+ 'pad': 'foobar<div contenteditable="false">NESTED</div>^bazqoz',
+ 'expected': 'foobar^bazqoz' },
+
+ { 'id': 'DIV:ce:false-1_SL',
+ 'desc': 'Delete nested non-editable <div> with oblique selection',
+ 'pad': 'foo[bar<div contenteditable="false">NES]TED</div>bazqoz',
+ 'expected': [ 'foo^<div contenteditable="false">NESTED</div>bazqoz',
+ 'foo<div contenteditable="false">[NES]TED</div>bazqoz' ] },
+
+ { 'id': 'DIV:ce:false-1_SR',
+ 'desc': 'Delete nested non-editable <div> with oblique selection',
+ 'pad': 'foobar<div contenteditable="false">NES[TED</div>baz]qoz',
+ 'expected': [ 'foobar<div contenteditable="false">NESTED</div>^qoz',
+ 'foobar<div contenteditable="false">NES[TED]</div>qoz' ] },
+
+ { 'id': 'DIV:ce:false-1_SI',
+ 'desc': 'Delete inside nested non-editable <div> (should be no-op)',
+ 'pad': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz',
+ 'expected': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz' }
+ ]
+ },
+
+ { 'desc': 'Delete with display:inline-block',
+ 'checkStyle': True,
+ 'tests': [
+ { 'id': 'SPAN:d:ib-1_SC',
+ 'desc': 'Delete inside an inline-block <span>',
+ 'pad': 'foo<span style="display: inline-block">bar^baz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">ba^baz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-1_SA',
+ 'desc': 'Delete from immediately after an inline-block <span>',
+ 'pad': 'foo<span style="display: inline-block">barbaz</span>^qoz',
+ 'expected': 'foo<span style="display: inline-block">barba^</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-2_SL',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo[DEL<span style="display: inline-block">ETE]bar</span>baz',
+ 'expected': 'foo^<span style="display: inline-block">bar</span>baz' },
+
+ { 'id': 'SPAN:d:ib-3_SR',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">bar[DEL</span>ETE]baz',
+ 'expected': 'foo<span style="display: inline-block">bar^</span>baz' },
+
+ { 'id': 'SPAN:d:ib-4i_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">bar[DELETE]baz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">bar^baz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-4l_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">[DELETE]barbaz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">^barbaz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-4r_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">barbaz[DELETE]</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">barbaz^</span>qoz' }
+ ]
+ }
+ ]
+}
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py
new file mode 100644
index 0000000000..813b22914a
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py
@@ -0,0 +1,315 @@
+
+FORWARDDELETE_TESTS = {
+ 'id': 'FD',
+ 'caption': 'Forward-Delete Tests',
+ 'command': 'forwardDelete',
+ 'checkAttrs': True,
+ 'checkStyle': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'forward-delete single characters',
+ 'tests': [
+ { 'id': 'CHAR-1_SC',
+ 'desc': 'Delete 1 character',
+ 'pad': 'foo^barbaz',
+ 'expected': 'foo^arbaz' },
+
+ { 'id': 'CHAR-2_SC',
+ 'desc': 'Delete 1 pre-composed character o with diaeresis',
+ 'pad': 'fo^&#xF6;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-3_SC',
+ 'desc': 'Delete 1 character with combining diaeresis above',
+ 'pad': 'fo^o&#x0308;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-4_SC',
+ 'desc': 'Delete 1 character with combining diaeresis below',
+ 'pad': 'fo^o&#x0324;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-5_SC',
+ 'desc': 'Delete 1 character with combining diaeresis above and below',
+ 'pad': 'fo^o&#x0308;&#x0324;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-6_SC',
+ 'desc': 'Delete 1 character with enclosing square',
+ 'pad': 'fo^o&#x20DE;barbaz',
+ 'expected': 'fo^barbaz' },
+
+ { 'id': 'CHAR-7_SC',
+ 'desc': 'Delete 1 character with combining long solidus overlay',
+ 'pad': 'fo^o&#x0338;barbaz',
+ 'expected': 'fo^barbaz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete text selections',
+ 'tests': [
+ { 'id': 'TEXT-1_SI',
+ 'desc': 'Delete text selection',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SE',
+ 'desc': 'Forward-delete at end of span',
+ 'pad': 'foo<b>bar^</b>baz',
+ 'expected': 'foo<b>bar^</b>az' },
+
+ { 'id': 'B-1_SB',
+ 'desc': 'Forward-delete from position before span',
+ 'pad': 'foo^<b>bar</b>baz',
+ 'expected': 'foo^<b>ar</b>baz' },
+
+ { 'id': 'B-1_SW',
+ 'desc': 'Delete selection that wraps the whole span content',
+ 'pad': 'foo<b>[bar]</b>baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SO',
+ 'desc': 'Delete selection that wraps the whole span',
+ 'pad': 'foo[<b>bar</b>]baz',
+ 'expected': 'foo^baz' },
+
+ { 'id': 'B-1_SL',
+ 'desc': 'Delete oblique selection that starts before span',
+ 'pad': 'foo[bar<b>baz]quoz</b>quuz',
+ 'expected': 'foo^<b>quoz</b>quuz' },
+
+ { 'id': 'B-1_SR',
+ 'desc': 'Delete oblique selection that ends after span',
+ 'pad': 'foo<b>bar[baz</b>quoz]quuz',
+ 'expected': 'foo<b>bar^</b>quuz' },
+
+ { 'id': 'B.I-1_SM',
+ 'desc': 'Delete oblique selection that starts and ends in different spans',
+ 'pad': 'foo<b>bar[baz</b><i>qoz]quuz</i>quuuz',
+ 'expected': 'foo<b>bar^</b><i>quuz</i>quuuz' },
+
+ { 'id': 'GEN-1_SE',
+ 'desc': 'Delete at end of span with generated content',
+ 'pad': 'foo<gen>bar^</gen>baz',
+ 'expected': 'foo<gen>bar^</gen>az' },
+
+ { 'id': 'GEN-1_SB',
+ 'desc': 'Delete from position before span with generated content',
+ 'pad': 'foo^<gen>bar</gen>baz',
+ 'expected': 'foo^<gen>ar</gen>baz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete paragraphs',
+ 'tests': [
+ { 'id': 'P2-1_SE1',
+ 'desc': 'Delete from collapsed selection at end of paragraph - should merge with next',
+ 'pad': '<p>foobar^</p><p>bazqoz</p>',
+ 'expected': '<p>foobar^bazqoz</p>' },
+
+ { 'id': 'P2-1_SI1',
+ 'desc': 'Delete non-collapsed selection at end of paragraph - should not merge with next',
+ 'pad': '<p>foo[bar]</p><p>bazqoz</p>',
+ 'expected': '<p>foo^</p><p>bazqoz</p>' },
+
+ { 'id': 'P2-1_SM',
+ 'desc': 'Delete non-collapsed selection spanning 2 paragraphs - should merge them',
+ 'pad': '<p>foo[bar</p><p>baz]qoz</p>',
+ 'expected': '<p>foo^qoz</p>' }
+ ]
+ },
+
+ { 'desc': 'forward-delete lists and list items',
+ 'tests': [
+ { 'id': 'OL-LI2-1_SO1',
+ 'desc': 'Delete fully wrapped list item',
+ 'pad': 'foo<ol>{<li>bar</li>}<li>baz</li></ol>qoz',
+ 'expected': ['foo<ol>|<li>baz</li></ol>qoz',
+ 'foo<ol><li>^baz</li></ol>qoz'] },
+
+ { 'id': 'OL-LI2-1_SM',
+ 'desc': 'Delete oblique range between list items within same list',
+ 'pad': 'foo<ol><li>ba[r</li><li>b]az</li></ol>qoz',
+ 'expected': 'foo<ol><li>ba^az</li></ol>qoz' },
+
+ { 'id': 'OL-LI-1_SW',
+ 'desc': 'Delete contents of last list item (list should remain)',
+ 'pad': 'foo<ol><li>[foo]</li></ol>qoz',
+ 'expected': ['foo<ol><li>|</li></ol>qoz',
+ 'foo<ol><li>^</li></ol>qoz'] },
+
+ { 'id': 'OL-LI-1_SO',
+ 'desc': 'Delete last list item of list (should remove entire list)',
+ 'pad': 'foo<ol>{<li>foo</li>}</ol>qoz',
+ 'expected': 'foo^qoz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete with strange selections',
+ 'tests': [
+ { 'id': 'HR.BR-1_SM',
+ 'desc': 'Delete selection that starts and ends within nodes that don\'t have children',
+ 'pad': 'foo<hr {>bar<br }>baz',
+ 'expected': 'foo<hr>|<br>baz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete from immediately before a table',
+ 'tests': [
+ { 'id': 'TABLE-1_SB',
+ 'desc': 'Delete from position immediately before table (should have no effect)',
+ 'pad': 'foo^<table><tbody><tr><td>bar</td></tr></tbody></table>baz',
+ 'expected': 'foo^<table><tbody><tr><td>bar</td></tr></tbody></table>baz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete within table cells',
+ 'tests': [
+ { 'id': 'TD-1_SE',
+ 'desc': 'Delete from end of last cell (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>bar^</td></tr></tbody></table>baz',
+ 'expected': 'foo<table><tbody><tr><td>bar^</td></tr></tbody></table>baz' },
+
+ { 'id': 'TD2-1_SE1',
+ 'desc': 'Delete from end of inner cell (should have no effect)',
+ 'pad': 'foo<table><tbody><tr><td>bar^</td><td>baz</td></tr></tbody></table>quoz',
+ 'expected': 'foo<table><tbody><tr><td>bar^</td><td>baz</td></tr></tbody></table>quoz' },
+
+ { 'id': 'TD2-1_SM',
+ 'desc': 'Delete with selection spanning 2 cells',
+ 'pad': 'foo<table><tbody><tr><td>ba[r</td><td>b]az</td></tr></tbody></table>quoz',
+ 'expected': 'foo<table><tbody><tr><td>ba^</td><td>az</td></tr></tbody></table>quoz' }
+ ]
+ },
+
+ { 'desc': 'forward-delete table rows',
+ 'tests': [
+ { 'id': 'TR3-1_SO1',
+ 'desc': 'Delete first table row',
+ 'pad': '<table><tbody>{<tr><td>A</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^B</td></tr><tr><td>C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3-1_SO2',
+ 'desc': 'Delete middle table row',
+ 'pad': '<table><tbody><tr><td>A</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td></tr>|<tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>A</td></tr><tr><td>^C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3-1_SO3',
+ 'desc': 'Delete last table row',
+ 'pad': '<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td></tr><tr><td>B^</td></tr></tbody></table>'] },
+
+ { 'id': 'TR2rs:2-1_SO1',
+ 'desc': 'Delete first table row where a cell has rowspan 2',
+ 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=2>R</td></tr>}<tr><td>B</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>B</td><td>R</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^B</td><td>R</td></tr></tbody></table>'] },
+
+ { 'id': 'TR2rs:2-1_SO2',
+ 'desc': 'Delete second table row where a cell has rowspan 2',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=2>R</td></tr>{<tr><td>B</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td><td>R</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td><td>R^</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO1',
+ 'desc': 'Delete first table row where a cell has rowspan 3',
+ 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=3>R</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody>|<tr><td>A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>^A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO2',
+ 'desc': 'Delete middle table row where a cell has rowspan 3',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>',
+ 'expected': ['<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr>|<tr><td>C</td></tr></tbody></table>',
+ '<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr><tr><td>^C</td></tr></tbody></table>'] },
+
+ { 'id': 'TR3rs:3-1_SO3',
+ 'desc': 'Delete last table row where a cell has rowspan 3',
+ 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>',
+ 'expected': ['<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B</td></tr>|</tbody></table>',
+ '<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B^</td></tr></tbody></table>'] }
+ ]
+ },
+
+ { 'desc': 'delete with non-editable nested content',
+ 'tests': [
+ { 'id': 'DIV:ce:false-1_SO',
+ 'desc': 'Delete nested non-editable <div>',
+ 'pad': 'foo[bar<div contenteditable="false">NESTED</div>baz]qoz',
+ 'expected': 'foo^qoz' },
+
+ { 'id': 'DIV:ce:false-1_SB',
+ 'desc': 'Delete from immediately before a nested non-editable <div>',
+ 'pad': 'foobar^<div contenteditable="false">NESTED</div>bazqoz',
+ 'expected': 'foobar^bazqoz' },
+
+ { 'id': 'DIV:ce:false-1_SL',
+ 'desc': 'Delete nested non-editable <div> with oblique selection',
+ 'pad': 'foo[bar<div contenteditable="false">NES]TED</div>bazqoz',
+ 'expected': [ 'foo^<div contenteditable="false">NESTED</div>bazqoz',
+ 'foo<div contenteditable="false">[NES]TED</div>bazqoz' ] },
+
+ { 'id': 'DIV:ce:false-1_SR',
+ 'desc': 'Delete nested non-editable <div> with oblique selection',
+ 'pad': 'foobar<div contenteditable="false">NES[TED</div>baz]qoz',
+ 'expected': [ 'foobar<div contenteditable="false">NESTED</div>^qoz',
+ 'foobar<div contenteditable="false">NES[TED]</div>qoz' ] },
+
+ { 'id': 'DIV:ce:false-1_SI',
+ 'desc': 'Delete inside nested non-editable <div> (should be no-op)',
+ 'pad': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz',
+ 'expected': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz' }
+ ]
+ },
+
+ { 'desc': 'Delete with display:inline-block',
+ 'checkStyle': True,
+ 'tests': [
+ { 'id': 'SPAN:d:ib-1_SC',
+ 'desc': 'Delete inside an inline-block <span>',
+ 'pad': 'foo<span style="display: inline-block">bar^baz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">bar^az</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-1_SA',
+ 'desc': 'Delete from immediately before an inline-block <span>',
+ 'pad': 'foo^<span style="display: inline-block">barbaz</span>qoz',
+ 'expected': 'foo^<span style="display: inline-block">arbaz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-2_SL',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo[DEL<span style="display: inline-block">ETE]bar</span>baz',
+ 'expected': 'foo^<span style="display: inline-block">bar</span>baz' },
+
+ { 'id': 'SPAN:d:ib-3_SR',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">bar[DEL</span>ETE]baz',
+ 'expected': 'foo<span style="display: inline-block">bar^</span>baz' },
+
+ { 'id': 'SPAN:d:ib-4i_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">bar[DELETE]baz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">bar^baz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-4l_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">[DELETE]barbaz</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">^barbaz</span>qoz' },
+
+ { 'id': 'SPAN:d:ib-4r_SI',
+ 'desc': 'Delete with nested inline-block <span>, oblique selection',
+ 'pad': 'foo<span style="display: inline-block">barbaz[DELETE]</span>qoz',
+ 'expected': 'foo<span style="display: inline-block">barbaz^</span>qoz' }
+ ]
+ }
+ ]
+}
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py
new file mode 100644
index 0000000000..a2e79c27c8
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py
@@ -0,0 +1,285 @@
+
+INSERT_TESTS = {
+ 'id': 'I',
+ 'caption': 'Insert Tests',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'insert <hr>',
+ 'command': 'inserthorizontalrule',
+ 'tests': [
+ { 'id': 'IHR_TEXT-1_SC',
+ 'rte1-id': 'a-inserthorizontalrule-0',
+ 'desc': 'Insert <hr> into text',
+ 'pad': 'foo^bar',
+ 'expected': 'foo<hr>^bar',
+ 'accept': 'foo<hr>|bar' },
+
+ { 'id': 'IHR_TEXT-1_SI',
+ 'desc': 'Insert <hr>, replacing selected text',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<hr>^baz',
+ 'accept': 'foo<hr>|baz' },
+
+ { 'id': 'IHR_DIV-B-1_SX',
+ 'desc': 'Insert <hr> between elements',
+ 'pad': '<div><b>foo</b>|<b>bar</b></div>',
+ 'expected': '<div><b>foo</b><hr>|<b>bar</b></div>' },
+
+ { 'id': 'IHR_DIV-B-2_SO',
+ 'desc': 'Insert <hr>, replacing a fully wrapped element',
+ 'pad': '<div><b>foo</b>{<b>bar</b>}<b>baz</b></div>',
+ 'expected': '<div><b>foo</b><hr>|<b>baz</b></div>' },
+
+ { 'id': 'IHR_B-1_SC',
+ 'desc': 'Insert <hr> into a span, splitting it',
+ 'pad': '<b>foo^bar</b>',
+ 'expected': '<b>foo</b><hr><b>^bar</b>' },
+
+ { 'id': 'IHR_B-1_SS',
+ 'desc': 'Insert <hr> into a span at the start (should not create an empty span)',
+ 'pad': '<b>^foobar</b>',
+ 'expected': '<hr><b>^foobar</b>' },
+
+ { 'id': 'IHR_B-1_SE',
+ 'desc': 'Insert <hr> into a span at the end',
+ 'pad': '<b>foobar^</b>',
+ 'expected': [ '<b>foobar</b><hr>|',
+ '<b>foobar</b><hr><b>^</b>' ] },
+
+ { 'id': 'IHR_B-2_SL',
+ 'desc': 'Insert <hr> with oblique selection starting outside of span',
+ 'pad': 'foo[bar<b>baz]qoz</b>',
+ 'expected': 'foo<hr>|<b>qoz</b>' },
+
+ { 'id': 'IHR_B-2_SLR',
+ 'desc': 'Insert <hr> with oblique reversed selection starting outside of span',
+ 'pad': 'foo]bar<b>baz[qoz</b>',
+ 'expected': [ 'foo<hr>|<b>qoz</b>',
+ 'foo<hr><b>^qoz</b>' ] },
+
+ { 'id': 'IHR_B-3_SR',
+ 'desc': 'Insert <hr> with oblique selection ending outside of span',
+ 'pad': '<b>foo[bar</b>baz]quoz',
+ 'expected': [ '<b>foo</b><hr>|quoz',
+ '<b>foo</b><hr><b>^</b>quoz' ] },
+
+ { 'id': 'IHR_B-3_SRR',
+ 'desc': 'Insert <hr> with oblique reversed selection starting outside of span',
+ 'pad': '<b>foo]bar</b>baz[quoz',
+ 'expected': '<b>foo</b><hr>|quoz' },
+
+ { 'id': 'IHR_B-I-1_SM',
+ 'desc': 'Insert <hr> with oblique selection between different spans',
+ 'pad': '<b>foo[bar</b><i>baz]quoz</i>',
+ 'expected': [ '<b>foo</b><hr>|<i>quoz</i>',
+ '<b>foo</b><hr><b>^</b><i>quoz</i>' ] },
+
+ { 'id': 'IHR_B-I-1_SMR',
+ 'desc': 'Insert <hr> with reversed oblique selection between different spans',
+ 'pad': '<b>foo]bar</b><i>baz[quoz</i>',
+ 'expected': '<b>foo</b><hr><i>^quoz</i>' },
+
+ { 'id': 'IHR_P-1_SC',
+ 'desc': 'Insert <hr> into a paragraph, splitting it',
+ 'pad': '<p>foo^bar</p>',
+ 'expected': [ '<p>foo</p><hr>|<p>bar</p>',
+ '<p>foo</p><hr><p>^bar</p>' ] },
+
+ { 'id': 'IHR_P-1_SS',
+ 'desc': 'Insert <hr> into a paragraph at the start (should not create an empty span)',
+ 'pad': '<p>^foobar</p>',
+ 'expected': [ '<hr>|<p>foobar</p>',
+ '<hr><p>^foobar</p>' ] },
+
+ { 'id': 'IHR_P-1_SE',
+ 'desc': 'Insert <hr> into a paragraph at the end (should not create an empty span)',
+ 'pad': '<p>foobar^</p>',
+ 'expected': '<p>foobar</p><hr>|' }
+ ]
+ },
+
+ { 'desc': 'insert <p>',
+ 'command': 'insertparagraph',
+ 'tests': [
+ { 'id': 'IP_P-1_SC',
+ 'desc': 'Split paragraph',
+ 'pad': '<p>foo^bar</p>',
+ 'expected': '<p>foo</p><p>^bar</p>' },
+
+ { 'id': 'IP_UL-LI-1_SC',
+ 'desc': 'Split list item',
+ 'pad': '<ul><li>foo^bar</li></ul>',
+ 'expected': '<ul><li>foo</li><li>^bar</li></ul>' }
+ ]
+ },
+
+ { 'desc': 'insert text',
+ 'command': 'inserttext',
+ 'tests': [
+ { 'id': 'ITEXT:text_TEXT-1_SC',
+ 'desc': 'Insert text',
+ 'value': 'text',
+ 'pad': 'foo^bar',
+ 'expected': 'footext^bar' },
+
+ { 'id': 'ITEXT:text_TEXT-1_SI',
+ 'desc': 'Insert text, replacing selected text',
+ 'value': 'text',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'footext^baz' }
+ ]
+ },
+
+ { 'desc': 'insert <br>',
+ 'command': 'insertlinebreak',
+ 'tests': [
+ { 'id': 'IBR_TEXT-1_SC',
+ 'desc': 'Insert <br> into text',
+ 'pad': 'foo^bar',
+ 'expected': [ 'foo<br>|bar',
+ 'foo<br>^bar' ] },
+
+ { 'id': 'IBR_TEXT-1_SI',
+ 'desc': 'Insert <br>, replacing selected text',
+ 'pad': 'foo[bar]baz',
+ 'expected': [ 'foo<br>|baz',
+ 'foo<br>^baz' ] },
+
+ { 'id': 'IBR_LI-1_SC',
+ 'desc': 'Insert <br> within list item',
+ 'pad': '<ul><li>foo^bar</li></ul>',
+ 'expected': '<ul><li>foo<br>^bar</li></ul>' }
+ ]
+ },
+
+ { 'desc': 'insert <img>',
+ 'command': 'insertimage',
+ 'tests': [
+ { 'id': 'IIMG:url_TEXT-1_SC',
+ 'rte1-id': 'a-insertimage-0',
+ 'desc': 'Insert image with URL "bar.png"',
+ 'value': 'bar.png',
+ 'checkAttrs': True,
+ 'pad': 'foo^bar',
+ 'expected': [ 'foo<img src="bar.png">|bar',
+ 'foo<img src="bar.png">^bar' ] },
+
+ { 'id': 'IIMG:url_IMG-1_SO',
+ 'desc': 'Change existing image to new URL, selection on <img>',
+ 'value': 'quz.png',
+ 'checkAttrs': True,
+ 'pad': '<span>foo{<img src="bar.png">}bar</span>',
+ 'expected': [ '<span>foo<img src="quz.png"/>|bar</span>',
+ '<span>foo<img src="quz.png"/>^bar</span>' ] },
+
+ { 'id': 'IIMG:url_SPAN-IMG-1_SO',
+ 'desc': 'Change existing image to new URL, selection in text surrounding <img>',
+ 'value': 'quz.png',
+ 'checkAttrs': True,
+ 'pad': 'foo[<img src="bar.png">]bar',
+ 'expected': [ 'foo<img src="quz.png"/>|bar',
+ 'foo<img src="quz.png"/>^bar' ] },
+
+ { 'id': 'IIMG:._SPAN-IMG-1_SO',
+ 'desc': 'Remove existing image or URL, selection on <img>',
+ 'value': '',
+ 'checkAttrs': True,
+ 'pad': '<span>foo{<img src="bar.png">}bar</span>',
+ 'expected': [ '<span>foo^bar</span>',
+ '<span>foo<img>|bar</span>',
+ '<span>foo<img>^bar</span>',
+ '<span>foo<img src="">|bar</span>',
+ '<span>foo<img src="">^bar</span>' ] },
+
+ { 'id': 'IIMG:._IMG-1_SO',
+ 'desc': 'Remove existing image or URL, selection in text surrounding <img>',
+ 'value': '',
+ 'checkAttrs': True,
+ 'pad': 'foo[<img src="bar.png">]bar',
+ 'expected': [ 'foo^bar',
+ 'foo<img>|bar',
+ 'foo<img>^bar',
+ 'foo<img src="">|bar',
+ 'foo<img src="">^bar' ] }
+ ]
+ },
+
+ { 'desc': 'insert <ol>',
+ 'command': 'insertorderedlist',
+ 'tests': [
+ { 'id': 'IOL_TEXT-1_SC',
+ 'rte1-id': 'a-insertorderedlist-0',
+ 'desc': 'Insert ordered list on collapsed selection',
+ 'pad': 'foo^bar',
+ 'expected': '<ol><li>foo^bar</li></ol>' },
+
+ { 'id': 'IOL_TEXT-1_SI',
+ 'desc': 'Insert ordered list on selected text',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<ol><li>foo[bar]baz</li></ol>' }
+ ]
+ },
+
+ { 'desc': 'insert <ul>',
+ 'command': 'insertunorderedlist',
+ 'tests': [
+ { 'id': 'IUL_TEXT-1_SC',
+ 'desc': 'Insert unordered list on collapsed selection',
+ 'pad': 'foo^bar',
+ 'expected': '<ul><li>foo^bar</li></ul>' },
+
+ { 'id': 'IUL_TEXT-1_SI',
+ 'rte1-id': 'a-insertunorderedlist-0',
+ 'desc': 'Insert unordered list on selected text',
+ 'pad': 'foo[bar]baz',
+ 'expected': '<ul><li>foo[bar]baz</li></ul>' }
+ ]
+ },
+
+ { 'desc': 'insert arbitrary HTML',
+ 'command': 'inserthtml',
+ 'tests': [
+ { 'id': 'IHTML:BR_TEXT-1_SC',
+ 'rte1-id': 'a-inserthtml-0',
+ 'desc': 'InsertHTML: <br>',
+ 'value': '<br>',
+ 'pad': 'foo^barbaz',
+ 'expected': 'foo<br>^barbaz' },
+
+ { 'id': 'IHTML:text_TEXT-1_SI',
+ 'desc': 'InsertHTML: "NEW"',
+ 'value': 'NEW',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'fooNEW^baz' },
+
+ { 'id': 'IHTML:S_TEXT-1_SI',
+ 'desc': 'InsertHTML: "<span>NEW<span>"',
+ 'value': '<span>NEW</span>',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<span>NEW</span>^baz' },
+
+ { 'id': 'IHTML:H1.H2_TEXT-1_SI',
+ 'desc': 'InsertHTML: "<h1>NEW</h1><h2>HTML</h2>"',
+ 'value': '<h1>NEW</h1><h2>HTML</h2>',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<h1>NEW</h1><h2>HTML</h2>^baz' },
+
+ { 'id': 'IHTML:P-B_TEXT-1_SI',
+ 'desc': 'InsertHTML: "<p>NEW<b>HTML</b>!</p>"',
+ 'value': '<p>NEW<b>HTML</b>!</p>',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'foo<p>NEW<b>HTML</b>!</p>^baz' }
+ ]
+ }
+ ]
+}
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py
new file mode 100644
index 0000000000..eb721923b6
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py
@@ -0,0 +1,215 @@
+
+QUERYENABLED_TESTS = {
+ 'id': 'QE',
+ 'caption': 'queryCommandEnabled Tests',
+ 'pad': 'foo[bar]baz',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+ 'expected': True,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'HTML5 commands',
+ 'tests': [
+ { 'id': 'SELECTALL_TEXT-1',
+ 'desc': 'check whether the "selectall" command is enabled',
+ 'qcenabled': 'selectall' },
+
+ { 'id': 'UNSELECT_TEXT-1',
+ 'desc': 'check whether the "unselect" command is enabled',
+ 'qcenabled': 'unselect' },
+
+ { 'id': 'UNDO_TEXT-1',
+ 'desc': 'check whether the "undo" command is enabled',
+ 'qcenabled': 'undo' },
+
+ { 'id': 'REDO_TEXT-1',
+ 'desc': 'check whether the "redo" command is enabled',
+ 'qcenabled': 'redo' },
+
+ { 'id': 'BOLD_TEXT-1',
+ 'desc': 'check whether the "bold" command is enabled',
+ 'qcenabled': 'bold' },
+
+ { 'id': 'ITALIC_TEXT-1',
+ 'desc': 'check whether the "italic" command is enabled',
+ 'qcenabled': 'italic' },
+
+ { 'id': 'UNDERLINE_TEXT-1',
+ 'desc': 'check whether the "underline" command is enabled',
+ 'qcenabled': 'underline' },
+
+ { 'id': 'STRIKETHROUGH_TEXT-1',
+ 'desc': 'check whether the "strikethrough" command is enabled',
+ 'qcenabled': 'strikethrough' },
+
+ { 'id': 'SUBSCRIPT_TEXT-1',
+ 'desc': 'check whether the "subscript" command is enabled',
+ 'qcenabled': 'subscript' },
+
+ { 'id': 'SUPERSCRIPT_TEXT-1',
+ 'desc': 'check whether the "superscript" command is enabled',
+ 'qcenabled': 'superscript' },
+
+ { 'id': 'FORMATBLOCK_TEXT-1',
+ 'desc': 'check whether the "formatblock" command is enabled',
+ 'qcenabled': 'formatblock' },
+
+ { 'id': 'CREATELINK_TEXT-1',
+ 'desc': 'check whether the "createlink" command is enabled',
+ 'qcenabled': 'createlink' },
+
+ { 'id': 'UNLINK_TEXT-1',
+ 'desc': 'check whether the "unlink" command is enabled',
+ 'qcenabled': 'unlink' },
+
+ { 'id': 'INSERTHTML_TEXT-1',
+ 'desc': 'check whether the "inserthtml" command is enabled',
+ 'qcenabled': 'inserthtml' },
+
+ { 'id': 'INSERTHORIZONTALRULE_TEXT-1',
+ 'desc': 'check whether the "inserthorizontalrule" command is enabled',
+ 'qcenabled': 'inserthorizontalrule' },
+
+ { 'id': 'INSERTIMAGE_TEXT-1',
+ 'desc': 'check whether the "insertimage" command is enabled',
+ 'qcenabled': 'insertimage' },
+
+ { 'id': 'INSERTLINEBREAK_TEXT-1',
+ 'desc': 'check whether the "insertlinebreak" command is enabled',
+ 'qcenabled': 'insertlinebreak' },
+
+ { 'id': 'INSERTPARAGRAPH_TEXT-1',
+ 'desc': 'check whether the "insertparagraph" command is enabled',
+ 'qcenabled': 'insertparagraph' },
+
+ { 'id': 'INSERTORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertorderedlist" command is enabled',
+ 'qcenabled': 'insertorderedlist' },
+
+ { 'id': 'INSERTUNORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertunorderedlist" command is enabled',
+ 'qcenabled': 'insertunorderedlist' },
+
+ { 'id': 'INSERTTEXT_TEXT-1',
+ 'desc': 'check whether the "inserttext" command is enabled',
+ 'qcenabled': 'inserttext' },
+
+ { 'id': 'DELETE_TEXT-1',
+ 'desc': 'check whether the "delete" command is enabled',
+ 'qcenabled': 'delete' },
+
+ { 'id': 'FORWARDDELETE_TEXT-1',
+ 'desc': 'check whether the "forwarddelete" command is enabled',
+ 'qcenabled': 'forwarddelete' }
+ ]
+ },
+
+ { 'desc': 'MIDAS commands',
+ 'tests': [
+ { 'id': 'STYLEWITHCSS_TEXT-1',
+ 'desc': 'check whether the "styleWithCSS" command is enabled',
+ 'qcenabled': 'styleWithCSS' },
+
+ { 'id': 'CONTENTREADONLY_TEXT-1',
+ 'desc': 'check whether the "contentreadonly" command is enabled',
+ 'qcenabled': 'contentreadonly' },
+
+ { 'id': 'BACKCOLOR_TEXT-1',
+ 'desc': 'check whether the "backcolor" command is enabled',
+ 'qcenabled': 'backcolor' },
+
+ { 'id': 'FORECOLOR_TEXT-1',
+ 'desc': 'check whether the "forecolor" command is enabled',
+ 'qcenabled': 'forecolor' },
+
+ { 'id': 'HILITECOLOR_TEXT-1',
+ 'desc': 'check whether the "hilitecolor" command is enabled',
+ 'qcenabled': 'hilitecolor' },
+
+ { 'id': 'FONTNAME_TEXT-1',
+ 'desc': 'check whether the "fontname" command is enabled',
+ 'qcenabled': 'fontname' },
+
+ { 'id': 'FONTSIZE_TEXT-1',
+ 'desc': 'check whether the "fontsize" command is enabled',
+ 'qcenabled': 'fontsize' },
+
+ { 'id': 'INCREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "increasefontsize" command is enabled',
+ 'qcenabled': 'increasefontsize' },
+
+ { 'id': 'DECREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "decreasefontsize" command is enabled',
+ 'qcenabled': 'decreasefontsize' },
+
+ { 'id': 'HEADING_TEXT-1',
+ 'desc': 'check whether the "heading" command is enabled',
+ 'qcenabled': 'heading' },
+
+ { 'id': 'INDENT_TEXT-1',
+ 'desc': 'check whether the "indent" command is enabled',
+ 'qcenabled': 'indent' },
+
+ { 'id': 'OUTDENT_TEXT-1',
+ 'desc': 'check whether the "outdent" command is enabled',
+ 'qcenabled': 'outdent' },
+
+ { 'id': 'CREATEBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "createbookmark" command is enabled',
+ 'qcenabled': 'createbookmark' },
+
+ { 'id': 'UNBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "unbookmark" command is enabled',
+ 'qcenabled': 'unbookmark' },
+
+ { 'id': 'JUSTIFYCENTER_TEXT-1',
+ 'desc': 'check whether the "justifycenter" command is enabled',
+ 'qcenabled': 'justifycenter' },
+
+ { 'id': 'JUSTIFYFULL_TEXT-1',
+ 'desc': 'check whether the "justifyfull" command is enabled',
+ 'qcenabled': 'justifyfull' },
+
+ { 'id': 'JUSTIFYLEFT_TEXT-1',
+ 'desc': 'check whether the "justifyleft" command is enabled',
+ 'qcenabled': 'justifyleft' },
+
+ { 'id': 'JUSTIFYRIGHT_TEXT-1',
+ 'desc': 'check whether the "justifyright" command is enabled',
+ 'qcenabled': 'justifyright' },
+
+ { 'id': 'REMOVEFORMAT_TEXT-1',
+ 'desc': 'check whether the "removeformat" command is enabled',
+ 'qcenabled': 'removeformat' },
+
+ { 'id': 'COPY_TEXT-1',
+ 'desc': 'check whether the "copy" command is enabled',
+ 'qcenabled': 'copy' },
+
+ { 'id': 'CUT_TEXT-1',
+ 'desc': 'check whether the "cut" command is enabled',
+ 'qcenabled': 'cut' },
+
+ { 'id': 'PASTE_TEXT-1',
+ 'desc': 'check whether the "paste" command is enabled',
+ 'qcenabled': 'paste' }
+ ]
+ },
+
+ { 'desc': 'Other tests',
+ 'tests': [
+ { 'id': 'garbage-1_TEXT-1',
+ 'desc': 'check correct return value with garbage input',
+ 'qcenabled': '#!#@7',
+ 'expected': False }
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py
new file mode 100644
index 0000000000..d1ad8debdb
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py
@@ -0,0 +1,214 @@
+
+QUERYINDETERM_TESTS = {
+ 'id': 'QI',
+ 'caption': 'queryCommandIndeterm Tests',
+ 'pad': 'foo[bar]baz',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+ 'expected': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'HTML5 commands',
+ 'tests': [
+ { 'id': 'SELECTALL_TEXT-1',
+ 'desc': 'check whether the "selectall" command is indeterminate',
+ 'qcindeterm': 'selectall' },
+
+ { 'id': 'UNSELECT_TEXT-1',
+ 'desc': 'check whether the "unselect" command is indeterminate',
+ 'qcindeterm': 'unselect' },
+
+ { 'id': 'UNDO_TEXT-1',
+ 'desc': 'check whether the "undo" command is indeterminate',
+ 'qcindeterm': 'undo' },
+
+ { 'id': 'REDO_TEXT-1',
+ 'desc': 'check whether the "redo" command is indeterminate',
+ 'qcindeterm': 'redo' },
+
+ { 'id': 'BOLD_TEXT-1',
+ 'desc': 'check whether the "bold" command is indeterminate',
+ 'qcindeterm': 'bold' },
+
+ { 'id': 'ITALIC_TEXT-1',
+ 'desc': 'check whether the "italic" command is indeterminate',
+ 'qcindeterm': 'italic' },
+
+ { 'id': 'UNDERLINE_TEXT-1',
+ 'desc': 'check whether the "underline" command is indeterminate',
+ 'qcindeterm': 'underline' },
+
+ { 'id': 'STRIKETHROUGH_TEXT-1',
+ 'desc': 'check whether the "strikethrough" command is indeterminate',
+ 'qcindeterm': 'strikethrough' },
+
+ { 'id': 'SUBSCRIPT_TEXT-1',
+ 'desc': 'check whether the "subscript" command is indeterminate',
+ 'qcindeterm': 'subscript' },
+
+ { 'id': 'SUPERSCRIPT_TEXT-1',
+ 'desc': 'check whether the "superscript" command is indeterminate',
+ 'qcindeterm': 'superscript' },
+
+ { 'id': 'FORMATBLOCK_TEXT-1',
+ 'desc': 'check whether the "formatblock" command is indeterminate',
+ 'qcindeterm': 'formatblock' },
+
+ { 'id': 'CREATELINK_TEXT-1',
+ 'desc': 'check whether the "createlink" command is indeterminate',
+ 'qcindeterm': 'createlink' },
+
+ { 'id': 'UNLINK_TEXT-1',
+ 'desc': 'check whether the "unlink" command is indeterminate',
+ 'qcindeterm': 'unlink' },
+
+ { 'id': 'INSERTHTML_TEXT-1',
+ 'desc': 'check whether the "inserthtml" command is indeterminate',
+ 'qcindeterm': 'inserthtml' },
+
+ { 'id': 'INSERTHORIZONTALRULE_TEXT-1',
+ 'desc': 'check whether the "inserthorizontalrule" command is indeterminate',
+ 'qcindeterm': 'inserthorizontalrule' },
+
+ { 'id': 'INSERTIMAGE_TEXT-1',
+ 'desc': 'check whether the "insertimage" command is indeterminate',
+ 'qcindeterm': 'insertimage' },
+
+ { 'id': 'INSERTLINEBREAK_TEXT-1',
+ 'desc': 'check whether the "insertlinebreak" command is indeterminate',
+ 'qcindeterm': 'insertlinebreak' },
+
+ { 'id': 'INSERTPARAGRAPH_TEXT-1',
+ 'desc': 'check whether the "insertparagraph" command is indeterminate',
+ 'qcindeterm': 'insertparagraph' },
+
+ { 'id': 'INSERTORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertorderedlist" command is indeterminate',
+ 'qcindeterm': 'insertorderedlist' },
+
+ { 'id': 'INSERTUNORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertunorderedlist" command is indeterminate',
+ 'qcindeterm': 'insertunorderedlist' },
+
+ { 'id': 'INSERTTEXT_TEXT-1',
+ 'desc': 'check whether the "inserttext" command is indeterminate',
+ 'qcindeterm': 'inserttext' },
+
+ { 'id': 'DELETE_TEXT-1',
+ 'desc': 'check whether the "delete" command is indeterminate',
+ 'qcindeterm': 'delete' },
+
+ { 'id': 'FORWARDDELETE_TEXT-1',
+ 'desc': 'check whether the "forwarddelete" command is indeterminate',
+ 'qcindeterm': 'forwarddelete' }
+ ]
+ },
+
+ { 'desc': 'MIDAS commands',
+ 'tests': [
+ { 'id': 'STYLEWITHCSS_TEXT-1',
+ 'desc': 'check whether the "styleWithCSS" command is indeterminate',
+ 'qcindeterm': 'styleWithCSS' },
+
+ { 'id': 'CONTENTREADONLY_TEXT-1',
+ 'desc': 'check whether the "contentreadonly" command is indeterminate',
+ 'qcindeterm': 'contentreadonly' },
+
+ { 'id': 'BACKCOLOR_TEXT-1',
+ 'desc': 'check whether the "backcolor" command is indeterminate',
+ 'qcindeterm': 'backcolor' },
+
+ { 'id': 'FORECOLOR_TEXT-1',
+ 'desc': 'check whether the "forecolor" command is indeterminate',
+ 'qcindeterm': 'forecolor' },
+
+ { 'id': 'HILITECOLOR_TEXT-1',
+ 'desc': 'check whether the "hilitecolor" command is indeterminate',
+ 'qcindeterm': 'hilitecolor' },
+
+ { 'id': 'FONTNAME_TEXT-1',
+ 'desc': 'check whether the "fontname" command is indeterminate',
+ 'qcindeterm': 'fontname' },
+
+ { 'id': 'FONTSIZE_TEXT-1',
+ 'desc': 'check whether the "fontsize" command is indeterminate',
+ 'qcindeterm': 'fontsize' },
+
+ { 'id': 'INCREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "increasefontsize" command is indeterminate',
+ 'qcindeterm': 'increasefontsize' },
+
+ { 'id': 'DECREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "decreasefontsize" command is indeterminate',
+ 'qcindeterm': 'decreasefontsize' },
+
+ { 'id': 'HEADING_TEXT-1',
+ 'desc': 'check whether the "heading" command is indeterminate',
+ 'qcindeterm': 'heading' },
+
+ { 'id': 'INDENT_TEXT-1',
+ 'desc': 'check whether the "indent" command is indeterminate',
+ 'qcindeterm': 'indent' },
+
+ { 'id': 'OUTDENT_TEXT-1',
+ 'desc': 'check whether the "outdent" command is indeterminate',
+ 'qcindeterm': 'outdent' },
+
+ { 'id': 'CREATEBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "createbookmark" command is indeterminate',
+ 'qcindeterm': 'createbookmark' },
+
+ { 'id': 'UNBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "unbookmark" command is indeterminate',
+ 'qcindeterm': 'unbookmark' },
+
+ { 'id': 'JUSTIFYCENTER_TEXT-1',
+ 'desc': 'check whether the "justifycenter" command is indeterminate',
+ 'qcindeterm': 'justifycenter' },
+
+ { 'id': 'JUSTIFYFULL_TEXT-1',
+ 'desc': 'check whether the "justifyfull" command is indeterminate',
+ 'qcindeterm': 'justifyfull' },
+
+ { 'id': 'JUSTIFYLEFT_TEXT-1',
+ 'desc': 'check whether the "justifyleft" command is indeterminate',
+ 'qcindeterm': 'justifyleft' },
+
+ { 'id': 'JUSTIFYRIGHT_TEXT-1',
+ 'desc': 'check whether the "justifyright" command is indeterminate',
+ 'qcindeterm': 'justifyright' },
+
+ { 'id': 'REMOVEFORMAT_TEXT-1',
+ 'desc': 'check whether the "removeformat" command is indeterminate',
+ 'qcindeterm': 'removeformat' },
+
+ { 'id': 'COPY_TEXT-1',
+ 'desc': 'check whether the "copy" command is indeterminate',
+ 'qcindeterm': 'copy' },
+
+ { 'id': 'CUT_TEXT-1',
+ 'desc': 'check whether the "cut" command is indeterminate',
+ 'qcindeterm': 'cut' },
+
+ { 'id': 'PASTE_TEXT-1',
+ 'desc': 'check whether the "paste" command is indeterminate',
+ 'qcindeterm': 'paste' }
+ ]
+ },
+
+ { 'desc': 'Other tests',
+ 'tests': [
+ { 'id': 'garbage-1_TEXT-1',
+ 'desc': 'check correct return value with garbage input',
+ 'qcindeterm': '#!#@7' }
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py
new file mode 100644
index 0000000000..297559d625
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py
@@ -0,0 +1,575 @@
+
+QUERYSTATE_TESTS = {
+ 'id': 'QS',
+ 'caption': 'queryCommandState Tests',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'qcstate': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'query bold state',
+ 'qcstate': 'bold',
+ 'tests': [
+ { 'id': 'B_TEXT_SI',
+ 'rte1-id': 'q-bold-0',
+ 'desc': 'query the "bold" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'B_B-1_SI',
+ 'rte1-id': 'q-bold-1',
+ 'desc': 'query the "bold" state',
+ 'pad': '<b>foo[bar]baz</b>',
+ 'expected': True },
+
+ { 'id': 'B_STRONG-1_SI',
+ 'rte1-id': 'q-bold-2',
+ 'desc': 'query the "bold" state',
+ 'pad': '<strong>foo[bar]baz</strong>',
+ 'expected': True },
+
+ { 'id': 'B_SPANs:fw:b-1_SI',
+ 'rte1-id': 'q-bold-3',
+ 'desc': 'query the "bold" state',
+ 'pad': '<span style="font-weight: bold">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'B_SPANs:fw:n-1_SI',
+ 'desc': 'query the "bold" state',
+ 'pad': '<span style="font-weight: normal">foo[bar]baz</span>',
+ 'expected': False },
+
+ { 'id': 'B_Bs:fw:n-1_SI',
+ 'rte1-id': 'q-bold-4',
+ 'desc': 'query the "bold" state',
+ 'pad': '<span style="font-weight: normal">foo[bar]baz</span>',
+ 'expected': False },
+
+ { 'id': 'B_B-SPANs:fw:n-1_SI',
+ 'rte1-id': 'q-bold-5',
+ 'desc': 'query the "bold" state',
+ 'pad': '<b><span style="font-weight: normal">foo[bar]baz</span></b>',
+ 'expected': False },
+
+ { 'id': 'B_SPAN.b-1-SI',
+ 'desc': 'query the "bold" state',
+ 'pad': '<span class="b">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'B_MYB-1-SI',
+ 'desc': 'query the "bold" state',
+ 'pad': '<myb>foo[bar]baz</myb>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-1_SC',
+ 'desc': 'query the "bold" state, bold tag not immediate parent',
+ 'pad': '<b>foo<i>ba^r</i>baz</b>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-1_SL',
+ 'desc': 'query the "bold" state, selection partially in child element',
+ 'pad': '<b>fo[o<i>b]ar</i>baz</b>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-1_SR',
+ 'desc': 'query the "bold" state, selection partially in child element',
+ 'pad': '<b>foo<i>ba[r</i>b]az</b>',
+ 'expected': True },
+
+ { 'id': 'B_STRONG-I-1_SC',
+ 'desc': 'query the "bold" state, bold tag not immediate parent',
+ 'pad': '<strong>foo<i>ba^r</i>baz</strong>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-U-1_SC',
+ 'desc': 'query the "bold" state, bold tag not immediate parent',
+ 'pad': '<b>foo<i>bar<u>b^az</u></i></strong>',
+ 'expected': True },
+
+ { 'id': 'B_B-I-U-1_SM',
+ 'desc': 'query the "bold" state, bold tag not immediate parent',
+ 'pad': '<b>foo<i>ba[r<u>b]az</u></i></strong>',
+ 'expected': True },
+
+ { 'id': 'B_TEXT-B-1_SO-1',
+ 'desc': 'query the "bold" state, selection wrapping the bold tag',
+ 'pad': 'foo[<b>bar</b>]baz',
+ 'expected': True },
+
+ { 'id': 'B_TEXT-B-1_SO-2',
+ 'desc': 'query the "bold" state, selection wrapping the bold tag',
+ 'pad': 'foo{<b>bar</b>}baz',
+ 'expected': True },
+
+ { 'id': 'B_TEXT-B-1_SL',
+ 'desc': 'query the "bold" state, selection containing non-bold text',
+ 'pad': 'fo[o<b>ba]r</b>baz',
+ 'expected': False },
+
+ { 'id': 'B_TEXT-B-1_SR',
+ 'desc': 'query the "bold" state, selection containing non-bold text',
+ 'pad': 'foo<b>b[ar</b>b]az',
+ 'expected': False },
+
+ { 'id': 'B_TEXT-B-1_SO-3',
+ 'desc': 'query the "bold" state, selection containing non-bold text',
+ 'pad': 'fo[o<b>bar</b>b]az',
+ 'expected': False },
+
+ { 'id': 'B_B.TEXT.B-1_SM',
+ 'desc': 'query the "bold" state, selection including non-bold text',
+ 'pad': '<b>fo[o</b>bar<b>b]az</b>',
+ 'expected': False },
+
+ { 'id': 'B_B.B.B-1_SM',
+ 'desc': 'query the "bold" state, selection mixed, but all bold',
+ 'pad': '<b>fo[o</b><b>bar</b><b>b]az</b>',
+ 'expected': True },
+
+ { 'id': 'B_B.STRONG.B-1_SM',
+ 'desc': 'query the "bold" state, selection mixed, but all bold',
+ 'pad': '<b>fo[o</b><strong>bar</strong><b>b]az</b>',
+ 'expected': True },
+
+ { 'id': 'B_SPAN.b.MYB.SPANs:fw:b-1_SM',
+ 'desc': 'query the "bold" state, selection mixed, but all bold',
+ 'pad': '<span class="b">fo[o</span><myb>bar</myb><span style="font-weight: bold">b]az</span>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query italic state',
+ 'qcstate': 'italic',
+ 'tests': [
+ { 'id': 'I_TEXT_SI',
+ 'rte1-id': 'q-italic-0',
+ 'desc': 'query the "italic" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'I_I-1_SI',
+ 'rte1-id': 'q-italic-1',
+ 'desc': 'query the "italic" state',
+ 'pad': '<i>foo[bar]baz</i>',
+ 'expected': True },
+
+ { 'id': 'I_EM-1_SI',
+ 'rte1-id': 'q-italic-2',
+ 'desc': 'query the "italic" state',
+ 'pad': '<em>foo[bar]baz</em>',
+ 'expected': True },
+
+ { 'id': 'I_SPANs:fs:i-1_SI',
+ 'rte1-id': 'q-italic-3',
+ 'desc': 'query the "italic" state',
+ 'pad': '<span style="font-style: italic">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'I_SPANs:fs:n-1_SI',
+ 'desc': 'query the "italic" state',
+ 'pad': '<span style="font-style: normal">foo[bar]baz</span>',
+ 'expected': False },
+
+ { 'id': 'I_I-SPANs:fs:n-1_SI',
+ 'rte1-id': 'q-italic-4',
+ 'desc': 'query the "italic" state',
+ 'pad': '<i><span style="font-style: normal">foo[bar]baz</span></i>',
+ 'expected': False },
+
+ { 'id': 'I_SPAN.i-1-SI',
+ 'desc': 'query the "italic" state',
+ 'pad': '<span class="i">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'I_MYI-1-SI',
+ 'desc': 'query the "italic" state',
+ 'pad': '<myi>foo[bar]baz</myi>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query underline state',
+ 'qcstate': 'underline',
+ 'tests': [
+ { 'id': 'U_TEXT_SI',
+ 'rte1-id': 'q-underline-0',
+ 'desc': 'query the "underline" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'U_U-1_SI',
+ 'rte1-id': 'q-underline-1',
+ 'desc': 'query the "underline" state',
+ 'pad': '<u>foo[bar]baz</u>',
+ 'expected': True },
+
+ { 'id': 'U_Us:td:n-1_SI',
+ 'rte1-id': 'q-underline-4',
+ 'desc': 'query the "underline" state',
+ 'pad': '<u style="text-decoration: none">foo[bar]baz</u>',
+ 'expected': False },
+
+ { 'id': 'U_Ah:url-1_SI',
+ 'rte1-id': 'q-underline-2',
+ 'desc': 'query the "underline" state',
+ 'pad': '<a href="http://www.goo.gl">foo[bar]baz</a>',
+ 'expected': True },
+
+ { 'id': 'U_Ah:url.s:td:n-1_SI',
+ 'rte1-id': 'q-underline-5',
+ 'desc': 'query the "underline" state',
+ 'pad': '<a href="http://www.goo.gl" style="text-decoration: none">foo[bar]baz</a>',
+ 'expected': False },
+
+ { 'id': 'U_SPANs:td:u-1_SI',
+ 'rte1-id': 'q-underline-3',
+ 'desc': 'query the "underline" state',
+ 'pad': '<span style="text-decoration: underline">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'U_SPAN.u-1-SI',
+ 'desc': 'query the "underline" state',
+ 'pad': '<span class="u">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'U_MYU-1-SI',
+ 'desc': 'query the "underline" state',
+ 'pad': '<myu>foo[bar]baz</myu>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query strike-through state',
+ 'qcstate': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_TEXT_SI',
+ 'rte1-id': 'q-strikethrough-0',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'S_S-1_SI',
+ 'rte1-id': 'q-strikethrough-3',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<s>foo[bar]baz</s>',
+ 'expected': True },
+
+ { 'id': 'S_STRIKE-1_SI',
+ 'rte1-id': 'q-strikethrough-1',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<strike>foo[bar]baz</strike>',
+ 'expected': True },
+
+ { 'id': 'S_STRIKEs:td:n-1_SI',
+ 'rte1-id': 'q-strikethrough-2',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<strike style="text-decoration: none">foo[bar]baz</strike>',
+ 'expected': False },
+
+ { 'id': 'S_DEL-1_SI',
+ 'rte1-id': 'q-strikethrough-4',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<del>foo[bar]baz</del>',
+ 'expected': True },
+
+ { 'id': 'S_SPANs:td:lt-1_SI',
+ 'rte1-id': 'q-strikethrough-5',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<span style="text-decoration: line-through">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'S_SPAN.s-1-SI',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<span class="s">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'S_MYS-1-SI',
+ 'desc': 'query the "strikethrough" state',
+ 'pad': '<mys>foo[bar]baz</mys>',
+ 'expected': True },
+
+ { 'id': 'S_S.STRIKE.DEL-1_SM',
+ 'desc': 'query the "strikethrough" state, selection mixed, but all struck',
+ 'pad': '<s>fo[o</s><strike>bar</strike><del>b]az</del>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query subscript state',
+ 'qcstate': 'subscript',
+ 'tests': [
+ { 'id': 'SUB_TEXT_SI',
+ 'rte1-id': 'q-subscript-0',
+ 'desc': 'query the "subscript" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'SUB_SUB-1_SI',
+ 'rte1-id': 'q-subscript-1',
+ 'desc': 'query the "subscript" state',
+ 'pad': '<sub>foo[bar]baz</sub>',
+ 'expected': True },
+
+ { 'id': 'SUB_SPAN.sub-1-SI',
+ 'desc': 'query the "subscript" state',
+ 'pad': '<span class="sub">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'SUB_MYSUB-1-SI',
+ 'desc': 'query the "subscript" state',
+ 'pad': '<mysub>foo[bar]baz</mysub>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query superscript state',
+ 'qcstate': 'superscript',
+ 'tests': [
+ { 'id': 'SUP_TEXT_SI',
+ 'rte1-id': 'q-superscript-0',
+ 'desc': 'query the "superscript" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'SUP_SUP-1_SI',
+ 'rte1-id': 'q-superscript-1',
+ 'desc': 'query the "superscript" state',
+ 'pad': '<sup>foo[bar]baz</sup>',
+ 'expected': True },
+
+ { 'id': 'IOL_TEXT_SI',
+ 'desc': 'query the "insertorderedlist" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'SUP_SPAN.sup-1-SI',
+ 'desc': 'query the "superscript" state',
+ 'pad': '<span class="sup">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'SUP_MYSUP-1-SI',
+ 'desc': 'query the "superscript" state',
+ 'pad': '<mysup>foo[bar]baz</mysup>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the selection is in an ordered list',
+ 'qcstate': 'insertorderedlist',
+ 'tests': [
+ { 'id': 'IOL_TEXT-1_SI',
+ 'rte1-id': 'q-insertorderedlist-0',
+ 'desc': 'query the "insertorderedlist" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'IOL_OL-LI-1_SI',
+ 'rte1-id': 'q-insertorderedlist-1',
+ 'desc': 'query the "insertorderedlist" state',
+ 'pad': '<ol><li>foo[bar]baz</li></ol>',
+ 'expected': True },
+
+ { 'id': 'IOL_UL_LI-1_SI',
+ 'rte1-id': 'q-insertorderedlist-2',
+ 'desc': 'query the "insertorderedlist" state',
+ 'pad': '<ul><li>foo[bar]baz</li></ul>',
+ 'expected': False }
+ ]
+ },
+
+ { 'desc': 'query whether the selection is in an unordered list',
+ 'qcstate': 'insertunorderedlist',
+ 'tests': [
+ { 'id': 'IUL_TEXT_SI',
+ 'rte1-id': 'q-insertunorderedlist-0',
+ 'desc': 'query the "insertunorderedlist" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'IUL_OL-LI-1_SI',
+ 'rte1-id': 'q-insertunorderedlist-1',
+ 'desc': 'query the "insertunorderedlist" state',
+ 'pad': '<ol><li>foo[bar]baz</li></ol>',
+ 'expected': False },
+
+ { 'id': 'IUL_UL-LI-1_SI',
+ 'rte1-id': 'q-insertunorderedlist-2',
+ 'desc': 'query the "insertunorderedlist" state',
+ 'pad': '<ul><li>foo[bar]baz</li></ul>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the paragraph is centered',
+ 'qcstate': 'justifycenter',
+ 'tests': [
+ { 'id': 'JC_TEXT_SI',
+ 'rte1-id': 'q-justifycenter-0',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'JC_DIVa:c-1_SI',
+ 'rte1-id': 'q-justifycenter-1',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<div align="center">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JC_Pa:c-1_SI',
+ 'rte1-id': 'q-justifycenter-2',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<p align="center">foo[bar]baz</p>',
+ 'expected': True },
+
+ { 'id': 'JC_SPANs:ta:c-1_SI',
+ 'rte1-id': 'q-justifycenter-3',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<div style="text-align: center">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JC_SPAN.jc-1-SI',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<span class="jc">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JC_MYJC-1-SI',
+ 'desc': 'query the "justifycenter" state',
+ 'pad': '<myjc>foo[bar]baz</myjc>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the paragraph is justified',
+ 'qcstate': 'justifyfull',
+ 'tests': [
+ { 'id': 'JF_TEXT_SI',
+ 'rte1-id': 'q-justifyfull-0',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'JF_DIVa:j-1_SI',
+ 'rte1-id': 'q-justifyfull-1',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<div align="justify">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JF_Pa:j-1_SI',
+ 'rte1-id': 'q-justifyfull-2',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<p align="justify">foo[bar]baz</p>',
+ 'expected': True },
+
+ { 'id': 'JF_SPANs:ta:j-1_SI',
+ 'rte1-id': 'q-justifyfull-3',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<span style="text-align: justify">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JF_SPAN.jf-1-SI',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<span class="jf">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JF_MYJF-1-SI',
+ 'desc': 'query the "justifyfull" state',
+ 'pad': '<myjf>foo[bar]baz</myjf>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the paragraph is aligned left',
+ 'qcstate': 'justifyleft',
+ 'tests': [
+ { 'id': 'JL_TEXT_SI',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'JL_DIVa:l-1_SI',
+ 'rte1-id': 'q-justifyleft-0',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<div align="left">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JL_Pa:l-1_SI',
+ 'rte1-id': 'q-justifyleft-1',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<p align="left">foo[bar]baz</p>',
+ 'expected': True },
+
+ { 'id': 'JL_SPANs:ta:l-1_SI',
+ 'rte1-id': 'q-justifyleft-2',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<span style="text-align: left">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JL_SPAN.jl-1-SI',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<span class="jl">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JL_MYJL-1-SI',
+ 'desc': 'query the "justifyleft" state',
+ 'pad': '<myjl>foo[bar]baz</myjl>',
+ 'expected': True }
+ ]
+ },
+
+ { 'desc': 'query whether the paragraph is aligned right',
+ 'qcstate': 'justifyright',
+ 'tests': [
+ { 'id': 'JR_TEXT_SI',
+ 'rte1-id': 'q-justifyright-0',
+ 'desc': 'query the "justifyright" state',
+ 'pad': 'foo[bar]baz',
+ 'expected': False },
+
+ { 'id': 'JR_DIVa:r-1_SI',
+ 'rte1-id': 'q-justifyright-1',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<div align="right">foo[bar]baz</div>',
+ 'expected': True },
+
+ { 'id': 'JR_Pa:r-1_SI',
+ 'rte1-id': 'q-justifyright-2',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<p align="right">foo[bar]baz</p>',
+ 'expected': True },
+
+ { 'id': 'JR_SPANs:ta:r-1_SI',
+ 'rte1-id': 'q-justifyright-3',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<span style="text-align: right">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JR_SPAN.jr-1-SI',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<span class="jr">foo[bar]baz</span>',
+ 'expected': True },
+
+ { 'id': 'JR_MYJR-1-SI',
+ 'desc': 'query the "justifyright" state',
+ 'pad': '<myjr>foo[bar]baz</myjr>',
+ 'expected': True }
+ ]
+ }
+ ]
+}
+
+QUERYSTATE_TESTS_CSS = {
+ 'id': 'QSC',
+ 'caption': 'queryCommandState Tests, using styleWithCSS',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': True,
+
+ 'Proposed': QUERYSTATE_TESTS['Proposed']
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py
new file mode 100644
index 0000000000..af23a428ce
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py
@@ -0,0 +1,226 @@
+
+QUERYSUPPORTED_TESTS = {
+ 'id': 'Q',
+ 'caption': 'queryCommandSupported Tests',
+ 'pad': 'foo[bar]baz',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+ 'expected': True,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'HTML5 commands',
+ 'tests': [
+ { 'id': 'SELECTALL_TEXT-1',
+ 'desc': 'check whether the "selectall" command is supported',
+ 'qcsupported': 'selectall' },
+
+ { 'id': 'UNSELECT_TEXT-1',
+ 'desc': 'check whether the "unselect" command is supported',
+ 'qcsupported': 'unselect' },
+
+ { 'id': 'UNDO_TEXT-1',
+ 'desc': 'check whether the "undo" command is supported',
+ 'qcsupported': 'undo' },
+
+ { 'id': 'REDO_TEXT-1',
+ 'desc': 'check whether the "redo" command is supported',
+ 'qcsupported': 'redo' },
+
+ { 'id': 'BOLD_TEXT-1',
+ 'desc': 'check whether the "bold" command is supported',
+ 'qcsupported': 'bold' },
+
+ { 'id': 'BOLD_B',
+ 'desc': 'check whether the "bold" command is supported',
+ 'qcsupported': 'bold',
+ 'pad': '<b>foo[bar]baz</b>' },
+
+ { 'id': 'ITALIC_TEXT-1',
+ 'desc': 'check whether the "italic" command is supported',
+ 'qcsupported': 'italic' },
+
+ { 'id': 'ITALIC_I',
+ 'desc': 'check whether the "italic" command is supported',
+ 'qcsupported': 'italic',
+ 'pad': '<i>foo[bar]baz</i>' },
+
+ { 'id': 'UNDERLINE_TEXT-1',
+ 'desc': 'check whether the "underline" command is supported',
+ 'qcsupported': 'underline' },
+
+ { 'id': 'STRIKETHROUGH_TEXT-1',
+ 'desc': 'check whether the "strikethrough" command is supported',
+ 'qcsupported': 'strikethrough' },
+
+ { 'id': 'SUBSCRIPT_TEXT-1',
+ 'desc': 'check whether the "subscript" command is supported',
+ 'qcsupported': 'subscript' },
+
+ { 'id': 'SUPERSCRIPT_TEXT-1',
+ 'desc': 'check whether the "superscript" command is supported',
+ 'qcsupported': 'superscript' },
+
+ { 'id': 'FORMATBLOCK_TEXT-1',
+ 'desc': 'check whether the "formatblock" command is supported',
+ 'qcsupported': 'formatblock' },
+
+ { 'id': 'CREATELINK_TEXT-1',
+ 'desc': 'check whether the "createlink" command is supported',
+ 'qcsupported': 'createlink' },
+
+ { 'id': 'UNLINK_TEXT-1',
+ 'desc': 'check whether the "unlink" command is supported',
+ 'qcsupported': 'unlink' },
+
+ { 'id': 'INSERTHTML_TEXT-1',
+ 'desc': 'check whether the "inserthtml" command is supported',
+ 'qcsupported': 'inserthtml' },
+
+ { 'id': 'INSERTHORIZONTALRULE_TEXT-1',
+ 'desc': 'check whether the "inserthorizontalrule" command is supported',
+ 'qcsupported': 'inserthorizontalrule' },
+
+ { 'id': 'INSERTIMAGE_TEXT-1',
+ 'desc': 'check whether the "insertimage" command is supported',
+ 'qcsupported': 'insertimage' },
+
+ { 'id': 'INSERTLINEBREAK_TEXT-1',
+ 'desc': 'check whether the "insertlinebreak" command is supported',
+ 'qcsupported': 'insertlinebreak' },
+
+ { 'id': 'INSERTPARAGRAPH_TEXT-1',
+ 'desc': 'check whether the "insertparagraph" command is supported',
+ 'qcsupported': 'insertparagraph' },
+
+ { 'id': 'INSERTORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertorderedlist" command is supported',
+ 'qcsupported': 'insertorderedlist' },
+
+ { 'id': 'INSERTUNORDEREDLIST_TEXT-1',
+ 'desc': 'check whether the "insertunorderedlist" command is supported',
+ 'qcsupported': 'insertunorderedlist' },
+
+ { 'id': 'INSERTTEXT_TEXT-1',
+ 'desc': 'check whether the "inserttext" command is supported',
+ 'qcsupported': 'inserttext' },
+
+ { 'id': 'DELETE_TEXT-1',
+ 'desc': 'check whether the "delete" command is supported',
+ 'qcsupported': 'delete' },
+
+ { 'id': 'FORWARDDELETE_TEXT-1',
+ 'desc': 'check whether the "forwarddelete" command is supported',
+ 'qcsupported': 'forwarddelete' }
+ ]
+ },
+
+ { 'desc': 'MIDAS commands',
+ 'tests': [
+ { 'id': 'STYLEWITHCSS_TEXT-1',
+ 'desc': 'check whether the "styleWithCSS" command is supported',
+ 'qcsupported': 'styleWithCSS' },
+
+ { 'id': 'CONTENTREADONLY_TEXT-1',
+ 'desc': 'check whether the "contentreadonly" command is supported',
+ 'qcsupported': 'contentreadonly' },
+
+ { 'id': 'BACKCOLOR_TEXT-1',
+ 'desc': 'check whether the "backcolor" command is supported',
+ 'qcsupported': 'backcolor' },
+
+ { 'id': 'FORECOLOR_TEXT-1',
+ 'desc': 'check whether the "forecolor" command is supported',
+ 'qcsupported': 'forecolor' },
+
+ { 'id': 'HILITECOLOR_TEXT-1',
+ 'desc': 'check whether the "hilitecolor" command is supported',
+ 'qcsupported': 'hilitecolor' },
+
+ { 'id': 'FONTNAME_TEXT-1',
+ 'desc': 'check whether the "fontname" command is supported',
+ 'qcsupported': 'fontname' },
+
+ { 'id': 'FONTSIZE_TEXT-1',
+ 'desc': 'check whether the "fontsize" command is supported',
+ 'qcsupported': 'fontsize' },
+
+ { 'id': 'INCREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "increasefontsize" command is supported',
+ 'qcsupported': 'increasefontsize' },
+
+ { 'id': 'DECREASEFONTSIZE_TEXT-1',
+ 'desc': 'check whether the "decreasefontsize" command is supported',
+ 'qcsupported': 'decreasefontsize' },
+
+ { 'id': 'HEADING_TEXT-1',
+ 'desc': 'check whether the "heading" command is supported',
+ 'qcsupported': 'heading' },
+
+ { 'id': 'INDENT_TEXT-1',
+ 'desc': 'check whether the "indent" command is supported',
+ 'qcsupported': 'indent' },
+
+ { 'id': 'OUTDENT_TEXT-1',
+ 'desc': 'check whether the "outdent" command is supported',
+ 'qcsupported': 'outdent' },
+
+ { 'id': 'CREATEBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "createbookmark" command is supported',
+ 'qcsupported': 'createbookmark' },
+
+ { 'id': 'UNBOOKMARK_TEXT-1',
+ 'desc': 'check whether the "unbookmark" command is supported',
+ 'qcsupported': 'unbookmark' },
+
+ { 'id': 'JUSTIFYCENTER_TEXT-1',
+ 'desc': 'check whether the "justifycenter" command is supported',
+ 'qcsupported': 'justifycenter' },
+
+ { 'id': 'JUSTIFYFULL_TEXT-1',
+ 'desc': 'check whether the "justifyfull" command is supported',
+ 'qcsupported': 'justifyfull' },
+
+ { 'id': 'JUSTIFYLEFT_TEXT-1',
+ 'desc': 'check whether the "justifyleft" command is supported',
+ 'qcsupported': 'justifyleft' },
+
+ { 'id': 'JUSTIFYRIGHT_TEXT-1',
+ 'desc': 'check whether the "justifyright" command is supported',
+ 'qcsupported': 'justifyright' },
+
+ { 'id': 'REMOVEFORMAT_TEXT-1',
+ 'desc': 'check whether the "removeformat" command is supported',
+ 'qcsupported': 'removeformat' },
+
+ { 'id': 'COPY_TEXT-1',
+ 'desc': 'check whether the "copy" command is supported',
+ 'qcsupported': 'copy' },
+
+ { 'id': 'CUT_TEXT-1',
+ 'desc': 'check whether the "cut" command is supported',
+ 'qcsupported': 'cut' },
+
+ { 'id': 'PASTE_TEXT-1',
+ 'desc': 'check whether the "paste" command is supported',
+ 'qcsupported': 'paste' }
+ ]
+ },
+
+ { 'desc': 'Other tests',
+ 'tests': [
+ { 'id': 'garbage-1_TEXT-1',
+ 'desc': 'check correct return value with garbage input',
+ 'qcsupported': '#!#@7',
+ 'expected': False }
+ ]
+ }
+ ]
+}
+
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py
new file mode 100644
index 0000000000..793b7cb6cf
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py
@@ -0,0 +1,429 @@
+
+QUERYVALUE_TESTS = {
+ 'id': 'QV',
+ 'caption': 'queryCommandValue Tests',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': '[HTML5] query bold value',
+ 'qcvalue': 'bold',
+ 'tests': [
+ { 'id': 'B_TEXT_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'false' },
+
+ { 'id': 'B_B-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<b>foo[bar]baz</b>',
+ 'expected': 'true' },
+
+ { 'id': 'B_STRONG-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<strong>foo[bar]baz</strong>',
+ 'expected': 'true' },
+
+ { 'id': 'B_SPANs:fw:b-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span style="font-weight: bold">foo[bar]baz</span>',
+ 'expected': 'true' },
+
+ { 'id': 'B_SPANs:fw:n-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span style="font-weight: normal">foo[bar]baz</span>',
+ 'expected': 'false' },
+
+ { 'id': 'B_Bs:fw:n-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<b><span style="font-weight: normal">foo[bar]baz</span></b>',
+ 'expected': 'false' },
+
+ { 'id': 'B_SPAN.b-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span class="b">foo[bar]baz</span>',
+ 'expected': 'true' },
+
+ { 'id': 'B_MYB-1-SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<myb>foo[bar]baz</myb>',
+ 'expected': 'true' }
+ ]
+ },
+
+ { 'desc': '[HTML5] query italic value',
+ 'qcvalue': 'italic',
+ 'tests': [
+ { 'id': 'I_TEXT_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': 'foo[bar]baz',
+ 'expected': 'false' },
+
+ { 'id': 'I_I-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<i>foo[bar]baz</i>',
+ 'expected': 'true' },
+
+ { 'id': 'I_EM-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<em>foo[bar]baz</em>',
+ 'expected': 'true' },
+
+ { 'id': 'I_SPANs:fs:i-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span style="font-style: italic">foo[bar]baz</span>',
+ 'expected': 'true' },
+
+ { 'id': 'I_SPANs:fs:n-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<span style="font-style: normal">foo[bar]baz</span>',
+ 'expected': 'false' },
+
+ { 'id': 'I_I-SPANs:fs:n-1_SI',
+ 'desc': 'query the "bold" value',
+ 'pad': '<i><span style="font-style: normal">foo[bar]baz</span></i>',
+ 'expected': 'false' },
+
+ { 'id': 'I_SPAN.i-1_SI',
+ 'desc': 'query the "italic" value',
+ 'pad': '<span class="i">foo[bar]baz</span>',
+ 'expected': 'true' },
+
+ { 'id': 'I_MYI-1-SI',
+ 'desc': 'query the "italic" value',
+ 'pad': '<myi>foo[bar]baz</myi>',
+ 'expected': 'true' }
+ ]
+ },
+
+ { 'desc': '[HTML5] query block formatting value',
+ 'qcvalue': 'formatblock',
+ 'tests': [
+ { 'id': 'FB_TEXT-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': 'foobar^baz',
+ 'expected': '',
+ 'accept': 'normal' },
+
+ { 'id': 'FB_H1-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<h1>foobar^baz</h1>',
+ 'expected': 'h1' },
+
+ { 'id': 'FB_PRE-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<pre>foobar^baz</pre>',
+ 'expected': 'pre' },
+
+ { 'id': 'FB_BQ-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<blockquote>foobar^baz</blockquote>',
+ 'expected': 'blockquote' },
+
+ { 'id': 'FB_ADDRESS-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<address>foobar^baz</address>',
+ 'expected': 'address' },
+
+ { 'id': 'FB_H1-H2-1_SC',
+ 'desc': 'query the "formatBlock" value',
+ 'pad': '<h1>foo<h2>ba^r</h2>baz</h1>',
+ 'expected': 'h2' },
+
+ { 'id': 'FB_H1-H2-1_SL',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': '<h1>fo[o<h2>ba]r</h2>baz</h1>',
+ 'expected': 'h1' },
+
+ { 'id': 'FB_H1-H2-1_SR',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': '<h1>foo<h2>b[ar</h2>ba]z</h1>',
+ 'expected': 'h1' },
+
+ { 'id': 'FB_TEXT-ADDRESS-1_SL',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': 'fo[o<ADDRESS>ba]r</ADDRESS>baz',
+ 'expected': '',
+ 'accept': 'normal' },
+
+ { 'id': 'FB_TEXT-ADDRESS-1_SR',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': 'foo<ADDRESS>b[ar</ADDRESS>ba]z',
+ 'expected': '',
+ 'accept': 'normal' },
+
+ { 'id': 'FB_H1-H2.TEXT.H2-1_SM',
+ 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)',
+ 'pad': '<h1><h2>fo[o</h2>bar<h2>b]az</h2></h1>',
+ 'expected': 'h1' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query heading type',
+ 'qcvalue': 'heading',
+ 'tests': [
+ { 'id': 'H_H1-1_SC',
+ 'desc': 'query the "heading" type',
+ 'pad': '<h1>foobar^baz</h1>',
+ 'expected': 'h1',
+ 'accept': '<h1>' },
+
+ { 'id': 'H_H3-1_SC',
+ 'desc': 'query the "heading" type',
+ 'pad': '<h3>foobar^baz</h3>',
+ 'expected': 'h3',
+ 'accept': '<h3>' },
+
+ { 'id': 'H_H1-H2-H3-H4-1_SC',
+ 'desc': 'query the "heading" type within nested heading tags',
+ 'pad': '<h1><h2><h3><h4>foobar^baz</h4></h3></h2></h1>',
+ 'expected': 'h4',
+ 'accept': '<h4>' },
+
+ { 'id': 'H_P-1_SC',
+ 'desc': 'query the "heading" type outside of a heading',
+ 'pad': '<p>foobar^baz</p>',
+ 'expected': '' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query font name',
+ 'qcvalue': 'fontname',
+ 'tests': [
+ { 'id': 'FN_FONTf:a-1_SI',
+ 'rte1-id': 'q-fontname-0',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<font face="arial">foo[bar]baz</font>',
+ 'expected': 'arial' },
+
+ { 'id': 'FN_SPANs:ff:a-1_SI',
+ 'rte1-id': 'q-fontname-1',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<span style="font-family: arial">foo[bar]baz</span>',
+ 'expected': 'arial' },
+
+ { 'id': 'FN_FONTf:a.s:ff:c-1_SI',
+ 'rte1-id': 'q-fontname-2',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<font face="arial" style="font-family: courier">foo[bar]baz</font>',
+ 'expected': 'courier' },
+
+ { 'id': 'FN_FONTf:a-FONTf:c-1_SI',
+ 'rte1-id': 'q-fontname-3',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<font face="arial"><font face="courier">foo[bar]baz</font></font>',
+ 'expected': 'courier' },
+
+ { 'id': 'FN_SPANs:ff:c-FONTf:a-1_SI',
+ 'rte1-id': 'q-fontname-4',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<span style="font-family: courier"><font face="arial">foo[bar]baz</font></span>',
+ 'expected': 'arial' },
+
+ { 'id': 'FN_SPAN.fs18px-1_SI',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<span class="courier">foo[bar]baz</span>',
+ 'expected': 'courier' },
+
+ { 'id': 'FN_MYCOURIER-1-SI',
+ 'desc': 'query the "fontname" value',
+ 'pad': '<mycourier>foo[bar]baz</mycourier>',
+ 'expected': 'courier' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query font size',
+ 'qcvalue': 'fontsize',
+ 'tests': [
+ { 'id': 'FS_FONTsz:4-1_SI',
+ 'rte1-id': 'q-fontsize-0',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<font size=4>foo[bar]baz</font>',
+ 'expected': '18px' },
+
+ { 'id': 'FS_FONTs:fs:l-1_SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<font style="font-size: large">foo[bar]baz</font>',
+ 'expected': '18px' },
+
+ { 'id': 'FS_FONT.ass.s:fs:l-1_SI',
+ 'rte1-id': 'q-fontsize-1',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<font class="Apple-style-span" style="font-size: large">foo[bar]baz</font>',
+ 'expected': '18px' },
+
+ { 'id': 'FS_FONTsz:1.s:fs:xl-1_SI',
+ 'rte1-id': 'q-fontsize-2',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<font size=1 style="font-size: x-large">foo[bar]baz</font>',
+ 'expected': '24px' },
+
+ { 'id': 'FS_SPAN.large-1_SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<span class="large">foo[bar]baz</span>',
+ 'expected': 'large' },
+
+ { 'id': 'FS_SPAN.fs18px-1_SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<span class="fs18px">foo[bar]baz</span>',
+ 'expected': '18px' },
+
+ { 'id': 'FA_MYLARGE-1-SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<mylarge>foo[bar]baz</mylarge>',
+ 'expected': 'large' },
+
+ { 'id': 'FA_MYFS18PX-1-SI',
+ 'desc': 'query the "fontsize" value',
+ 'pad': '<myfs18px>foo[bar]baz</myfs18px>',
+ 'expected': '18px' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query background color',
+ 'qcvalue': 'backcolor',
+ 'tests': [
+ { 'id': 'BC_FONTs:bc:fca-1_SI',
+ 'rte1-id': 'q-backcolor-0',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<font style="background-color: #ffccaa">foo[bar]baz</font>',
+ 'expected': '#ffccaa' },
+
+ { 'id': 'BC_SPANs:bc:abc-1_SI',
+ 'rte1-id': 'q-backcolor-2',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<span style="background-color: #aabbcc">foo[bar]baz</span>',
+ 'expected': '#aabbcc' },
+
+ { 'id': 'BC_FONTs:bc:084-SPAN-1_SI',
+ 'desc': 'query the "backcolor" value, where the color was set on an ancestor',
+ 'pad': '<font style="background-color: #008844"><span>foo[bar]baz</span></font>',
+ 'expected': '#008844' },
+
+ { 'id': 'BC_SPANs:bc:cde-SPAN-1_SI',
+ 'desc': 'query the "backcolor" value, where the color was set on an ancestor',
+ 'pad': '<span style="background-color: #ccddee"><span>foo[bar]baz</span></span>',
+ 'expected': '#ccddee' },
+
+ { 'id': 'BC_SPAN.ass.s:bc:rgb-1_SI',
+ 'rte1-id': 'q-backcolor-1',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">foo[bar]baz</span>',
+ 'expected': '#ff0000' },
+
+ { 'id': 'BC_SPAN.bcred-1_SI',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<span class="bcred">foo[bar]baz</span>',
+ 'expected': 'red' },
+
+ { 'id': 'BC_MYBCRED-1-SI',
+ 'desc': 'query the "backcolor" value',
+ 'pad': '<mybcred>foo[bar]baz</mybcred>',
+ 'expected': 'red' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query text color',
+ 'qcvalue': 'forecolor',
+ 'tests': [
+ { 'id': 'FC_FONTc:f00-1_SI',
+ 'rte1-id': 'q-forecolor-0',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<font color="#ff0000">foo[bar]baz</font>',
+ 'expected': '#ff0000' },
+
+ { 'id': 'FC_SPANs:c:0f0-1_SI',
+ 'rte1-id': 'q-forecolor-1',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<span style="color: #00ff00">foo[bar]baz</span>',
+ 'expected': '#00ff00' },
+
+ { 'id': 'FC_FONTc:333.s:c:999-1_SI',
+ 'rte1-id': 'q-forecolor-2',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<font color="#333333" style="color: #999999">foo[bar]baz</font>',
+ 'expected': '#999999' },
+
+ { 'id': 'FC_FONTc:641-SPAN-1_SI',
+ 'desc': 'query the "forecolor" value, where the color was set on an ancestor',
+ 'pad': '<font color="#664411"><span>foo[bar]baz</span></font>',
+ 'expected': '#664411' },
+
+ { 'id': 'FC_SPANs:c:d95-SPAN-1_SI',
+ 'desc': 'query the "forecolor" value, where the color was set on an ancestor',
+ 'pad': '<span style="color: #dd9955"><span>foo[bar]baz</span></span>',
+ 'expected': '#dd9955' },
+
+ { 'id': 'FC_SPAN.red-1_SI',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<span class="red">foo[bar]baz</span>',
+ 'expected': 'red' },
+
+ { 'id': 'FC_MYRED-1-SI',
+ 'desc': 'query the "forecolor" value',
+ 'pad': '<myred>foo[bar]baz</myred>',
+ 'expected': 'red' }
+ ]
+ },
+
+ { 'desc': '[MIDAS] query hilight color (same as background color)',
+ 'qcvalue': 'hilitecolor',
+ 'tests': [
+ { 'id': 'HC_FONTs:bc:fc0-1_SI',
+ 'rte1-id': 'q-hilitecolor-0',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<font style="background-color: #ffcc00">foo[bar]baz</font>',
+ 'expected': '#ffcc00' },
+
+ { 'id': 'HC_SPANs:bc:a0c-1_SI',
+ 'rte1-id': 'q-hilitecolor-2',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<span style="background-color: #aa00cc">foo[bar]baz</span>',
+ 'expected': '#aa00cc' },
+
+ { 'id': 'HC_SPAN.ass.s:bc:rgb-1_SI',
+ 'rte1-id': 'q-hilitecolor-1',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">foo[bar]baz</span>',
+ 'expected': '#ff0000' },
+
+ { 'id': 'HC_FONTs:bc:83e-SPAN-1_SI',
+ 'desc': 'query the "hilitecolor" value, where the color was set on an ancestor',
+ 'pad': '<font style="background-color: #8833ee"><span>foo[bar]baz</span></font>',
+ 'expected': '#8833ee' },
+
+ { 'id': 'HC_SPANs:bc:b12-SPAN-1_SI',
+ 'desc': 'query the "hilitecolor" value, where the color was set on an ancestor',
+ 'pad': '<span style="background-color: #bb1122"><span>foo[bar]baz</span></span>',
+ 'expected': '#bb1122' },
+
+ { 'id': 'HC_SPAN.bcred-1_SI',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<span class="bcred">foo[bar]baz</span>',
+ 'expected': 'red' },
+
+ { 'id': 'HC_MYBCRED-1-SI',
+ 'desc': 'query the "hilitecolor" value',
+ 'pad': '<mybcred>foo[bar]baz</mybcred>',
+ 'expected': 'red' }
+ ]
+ }
+ ]
+}
+
+QUERYVALUE_TESTS_CSS = {
+ 'id': 'QVC',
+ 'caption': 'queryCommandValue Tests, using styleWithCSS',
+ 'checkAttrs': False,
+ 'checkStyle': False,
+ 'styleWithCSS': True,
+
+ 'Proposed': QUERYVALUE_TESTS['Proposed']
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py
new file mode 100644
index 0000000000..6fa7e69a66
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py
@@ -0,0 +1,801 @@
+
+SELECTION_TESTS = {
+ 'id': 'S',
+ 'caption': 'Selection Tests',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': False,
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'selectall',
+ 'command': 'selectall',
+ 'tests': [
+ { 'id': 'SELALL_TEXT-1_SI',
+ 'desc': 'select all, text only',
+ 'pad': 'foo [bar] baz',
+ 'expected': [ '[foo bar baz]',
+ '{foo bar baz}' ] },
+
+ { 'id': 'SELALL_I-1_SI',
+ 'desc': 'select all, with outer tags',
+ 'pad': '<i>foo [bar] baz</i>',
+ 'expected': '{<i>foo bar baz</i>}' }
+ ]
+ },
+
+ { 'desc': 'unselect',
+ 'command': 'unselect',
+ 'tests': [
+ { 'id': 'UNSEL_TEXT-1_SI',
+ 'desc': 'unselect',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo bar baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify (generic)',
+ 'tests': [
+ { 'id': 'SM:m.f.c_TEXT-1_SC-1',
+ 'desc': 'move caret 1 character forward',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo ba^r baz' },
+
+ { 'id': 'SM:m.b.c_TEXT-1_SC-1',
+ 'desc': 'move caret 1 character backward',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:m.f.c_TEXT-1_SI-1',
+ 'desc': 'move caret forward (sollapse selection)',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo bar^ baz' },
+
+ { 'id': 'SM:m.b.c_TEXT-1_SI-1',
+ 'desc': 'move caret backward (collapse selection)',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:m.f.w_TEXT-1_SC-1',
+ 'desc': 'move caret 1 word forward',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo bar^ baz' },
+
+ { 'id': 'SM:m.f.w_TEXT-1_SC-2',
+ 'desc': 'move caret 1 word forward',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': 'foo^ bar baz',
+ 'expected': 'foo bar^ baz' },
+
+ { 'id': 'SM:m.f.w_TEXT-1_SI-1',
+ 'desc': 'move caret 1 word forward from non-collapsed selection',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo bar baz^' },
+
+ { 'id': 'SM:m.b.w_TEXT-1_SC-1',
+ 'desc': 'move caret 1 word backward',
+ 'function': 'sel.modify("move", "backward", "word");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:m.b.w_TEXT-1_SC-3',
+ 'desc': 'move caret 1 word backward',
+ 'function': 'sel.modify("move", "backward", "word");',
+ 'pad': 'foo bar ^baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:m.b.w_TEXT-1_SI-1',
+ 'desc': 'move caret 1 word backward from non-collapsed selection',
+ 'function': 'sel.modify("move", "backward", "word");',
+ 'pad': 'foo [bar] baz',
+ 'expected': '^foo bar baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: move forward over combining diacritics, etc.',
+ 'tests': [
+ { 'id': 'SM:m.f.c_CHAR-2_SC-1',
+ 'desc': 'move 1 character forward over combined o with diaeresis',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^&#xF6;barbaz',
+ 'expected': 'fo&#xF6;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-3_SC-1',
+ 'desc': 'move 1 character forward over character with combining diaeresis above',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x0308;barbaz',
+ 'expected': 'foo&#x0308;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-4_SC-1',
+ 'desc': 'move 1 character forward over character with combining diaeresis below',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x0324;barbaz',
+ 'expected': 'foo&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SC-1',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x0308;&#x0324;barbaz',
+ 'expected': 'foo&#x0308;&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SI-1',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection on diaeresis above',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo[&#x0308;]&#x0324;barbaz',
+ 'expected': 'foo&#x0308;&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SI-2',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection on diaeresis below',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo&#x0308;[&#x0324;]barbaz',
+ 'expected': 'foo&#x0308;&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SL',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection oblique on diaeresis and preceding text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo[o&#x0308;]&#x0324;barbaz',
+ 'expected': 'foo&#x0308;&#x0324;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-5_SR',
+ 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection oblique on diaeresis and following text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo&#x0308;[&#x0324;bar]baz',
+ 'expected': 'foo&#x0308;&#x0324;bar^baz' },
+
+ { 'id': 'SM:m.f.c_CHAR-6_SC-1',
+ 'desc': 'move 1 character forward over character with enclosing square',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x20DE;barbaz',
+ 'expected': 'foo&#x20DE;^barbaz' },
+
+ { 'id': 'SM:m.f.c_CHAR-7_SC-1',
+ 'desc': 'move 1 character forward over character with combining long solidus overlay',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'fo^o&#x0338;barbaz',
+ 'expected': 'foo&#x0338;^barbaz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: move backward over combining diacritics, etc.',
+ 'tests': [
+ { 'id': 'SM:m.b.c_CHAR-2_SC-1',
+ 'desc': 'move 1 character backward over combined o with diaeresis',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'fo&#xF6;^barbaz',
+ 'expected': 'fo^&#xF6;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-3_SC-1',
+ 'desc': 'move 1 character backward over character with combining diaeresis above',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0308;^barbaz',
+ 'expected': 'fo^o&#x0308;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-4_SC-1',
+ 'desc': 'move 1 character backward over character with combining diaeresis below',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0324;^barbaz',
+ 'expected': 'fo^o&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SC-1',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0308;&#x0324;^barbaz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SI-1',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection on diaeresis above',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo[&#x0308;]&#x0324;barbaz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SI-2',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection on diaeresis below',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0308;[&#x0324;]barbaz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SL',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection oblique on diaeresis and preceding text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'fo[o&#x0308;]&#x0324;barbaz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-5_SR',
+ 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection oblique on diaeresis and following text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0308;[&#x0324;bar]baz',
+ 'expected': 'fo^o&#x0308;&#x0324;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-6_SC-1',
+ 'desc': 'move 1 character backward over character with enclosing square',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x20DE;^barbaz',
+ 'expected': 'fo^o&#x20DE;barbaz' },
+
+ { 'id': 'SM:m.b.c_CHAR-7_SC-1',
+ 'desc': 'move 1 character backward over character with combining long solidus overlay',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo&#x0338;^barbaz',
+ 'expected': 'fo^o&#x0338;barbaz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: move forward/backward/left/right in RTL text',
+ 'tests': [
+ { 'id': 'SM:m.f.c_Pdir:rtl-1_SC-1',
+ 'desc': 'move caret forward 1 character in right-to-left text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': '<p dir="rtl">foo b^ar baz</p>',
+ 'expected': '<p dir="rtl">foo ba^r baz</p>' },
+
+ { 'id': 'SM:m.b.c_Pdir:rtl-1_SC-1',
+ 'desc': 'move caret backward 1 character in right-to-left text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': '<p dir="rtl">foo ba^r baz</p>',
+ 'expected': '<p dir="rtl">foo b^ar baz</p>' },
+
+ { 'id': 'SM:m.r.c_Pdir:rtl-1_SC-1',
+ 'desc': 'move caret 1 character to the right in LTR text within RTL context',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': '<p dir="rtl">foo b^ar baz</p>',
+ 'expected': '<p dir="rtl">foo ba^r baz</p>' },
+
+ { 'id': 'SM:m.l.c_Pdir:rtl-1_SC-1',
+ 'desc': 'move caret 1 character to the left in LTR text within RTL context',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': '<p dir="rtl">foo ba^r baz</p>',
+ 'expected': '<p dir="rtl">foo b^ar baz</p>' },
+
+
+ { 'id': 'SM:m.f.c_TEXT:ar-1_SC-1',
+ 'desc': 'move caret forward 1 character in Arabic text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': '&#1605;&#x0631;&#1581;^&#1576;&#x0627; &#x0627;&#1604;&#x0639;&#x0627;&#1604;&#1605;',
+ 'expected': '&#1605;&#x0631;&#1581;&#1576;^&#x0627; &#x0627;&#1604;&#x0639;&#x0627;&#1604;&#1605;' },
+
+ { 'id': 'SM:m.b.c_TEXT:ar-1_SC-1',
+ 'desc': 'move caret backward 1 character in Arabic text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': '&#1605;&#x0631;&#1581;^&#1576;&#x0627; &#x0627;&#1604;&#x0639;&#x0627;&#1604;&#1605;',
+ 'expected': '&#1605;&#x0631;^&#1581;&#1576;&#x0627; &#x0627;&#1604;&#x0639;&#x0627;&#1604;&#1605;' },
+
+ { 'id': 'SM:m.f.c_TEXT:he-1_SC-1',
+ 'desc': 'move caret forward 1 character in Hebrew text',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': '&#x05E9;&#x05DC;^&#x05D5;&#x05DD; &#x05E2;&#x05D5;&#x05DC;&#x05DD;',
+ 'expected': '&#x05E9;&#x05DC;&#x05D5;^&#x05DD; &#x05E2;&#x05D5;&#x05DC;&#x05DD;' },
+
+ { 'id': 'SM:m.b.c_TEXT:he-1_SC-1',
+ 'desc': 'move caret backward 1 character in Hebrew text',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': '&#x05E9;&#x05DC;^&#x05D5;&#x05DD; &#x05E2;&#x05D5;&#x05DC;&#x05DD;',
+ 'expected': '&#x05E9;^&#x05DC;&#x05D5;&#x05DD; &#x05E2;&#x05D5;&#x05DC;&#x05DD;' },
+
+
+ { 'id': 'SM:m.f.c_BDOdir:rtl-1_SC-1',
+ 'desc': 'move caret forward 1 character inside <bdo>',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'foo <bdo dir="rtl">b^ar</bdo> baz',
+ 'expected': 'foo <bdo dir="rtl">ba^r</bdo> baz' },
+
+ { 'id': 'SM:m.b.c_BDOdir:rtl-1_SC-1',
+ 'desc': 'move caret backward 1 character inside <bdo>',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'foo <bdo dir="rtl">ba^r</bdo> baz',
+ 'expected': 'foo <bdo dir="rtl">b^ar</bdo> baz' },
+
+ { 'id': 'SM:m.r.c_BDOdir:rtl-1_SC-1',
+ 'desc': 'move caret 1 character to the right inside <bdo>',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'foo <bdo dir="rtl">ba^r</bdo> baz',
+ 'expected': 'foo <bdo dir="rtl">b^ar</bdo> baz' },
+
+ { 'id': 'SM:m.l.c_BDOdir:rtl-1_SC-1',
+ 'desc': 'move caret 1 character to the left inside <bdo>',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'foo <bdo dir="rtl">b^ar</bdo> baz',
+ 'expected': 'foo <bdo dir="rtl">ba^r</bdo> baz' },
+
+
+ { 'id': 'SM:m.f.c_TEXTrle-1_SC-rtl-1',
+ 'desc': 'move caret forward in RTL text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;^&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrle-1_SC-rtl-1',
+ 'desc': 'move caret backward in RTL text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;^&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrle-1_SC-rtl-1',
+ 'desc': 'move caret 1 character to the right in RTL text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;^&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrle-1_SC-rtl-1',
+ 'desc': 'move caret 1 character to the left in RTL text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;^&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.f.c_TEXTrle-1_SC-ltr-1',
+ 'desc': 'move caret forward in LTR text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrle-1_SC-ltr-1',
+ 'desc': 'move caret backward in LTR text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrle-1_SC-ltr-1',
+ 'desc': 'move caret 1 character to the right in LTR text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrle-1_SC-ltr-1',
+ 'desc': 'move caret 1 character to the left in LTR text within RLE-PDF marks',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "(RLE)&#x202B;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLE)&#x202B;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+
+ { 'id': 'SM:m.f.c_TEXTrlo-1_SC-rtl-1',
+ 'desc': 'move caret forward in RTL text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;^&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrlo-1_SC-rtl-1',
+ 'desc': 'move caret backward in RTL text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;^&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrlo-1_SC-rtl-1',
+ 'desc': 'move caret 1 character to the right in RTL text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;^&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrlo-1_SC-rtl-1',
+ 'desc': 'move caret 1 character to the left in RTL text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;^&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;car &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;^&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.f.c_TEXTrlo-1_SC-ltr-1',
+ 'desc': 'move caret forward in Latin text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrlo-1_SC-ltr-1',
+ 'desc': 'move caret backward in Latin text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrlo-1_SC-ltr-1',
+ 'desc': 'move caret 1 character to the right in Latin text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrlo-1_SC-ltr-1',
+ 'desc': 'move caret 1 character to the left in Latin text within RLO-PDF marks',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "(RLO)&#x202E;c^ar &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".',
+ 'expected': 'I said, "(RLO)&#x202E;ca^r &#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;&#x202C;(PDF)".' },
+
+
+ { 'id': 'SM:m.f.c_TEXTrlm-1_SC-1',
+ 'desc': 'move caret forward in RTL text within neutral characters followed by RLM',
+ 'function': 'sel.modify("move", "forward", "character");',
+ 'pad': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!^?!&#x200F;(RLM)".',
+ 'expected': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!?^!&#x200F;(RLM)".' },
+
+ { 'id': 'SM:m.b.c_TEXTrlm-1_SC-1',
+ 'desc': 'move caret backward in RTL text within neutral characters followed by RLM',
+ 'function': 'sel.modify("move", "backward", "character");',
+ 'pad': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!?^!&#x200F;(RLM)".',
+ 'expected': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!^?!&#x200F;(RLM)".' },
+
+ { 'id': 'SM:m.r.c_TEXTrlm-1_SC-1',
+ 'desc': 'move caret 1 character to the right in RTL text within neutral characters followed by RLM',
+ 'function': 'sel.modify("move", "right", "character");',
+ 'pad': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!?^!&#x200F;(RLM)".',
+ 'expected': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!^?!&#x200F;(RLM)".' },
+
+ { 'id': 'SM:m.l.c_TEXTrlm-1_SC-1',
+ 'desc': 'move caret 1 character to the left in RTL text within neutral characters followed by RLM',
+ 'function': 'sel.modify("move", "left", "character");',
+ 'pad': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!^?!&#x200F;(RLM)".',
+ 'expected': 'I said, "&#x064A;&#x0639;&#x0646;&#x064A; &#x0633;&#x064A;&#x0627;&#x0631;&#x0629;!?^!&#x200F;(RLM)".' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: move forward/backward over words in Japanese text',
+ 'tests': [
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-1',
+ 'desc': 'move caret forward 1 word in Japanese text (adjective)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '^&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': '&#x9762;&#x767D;&#x3044;^&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;' },
+
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-2',
+ 'desc': 'move caret forward 1 word in Japanese text (in the middle of a word)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '&#x9762;^&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': '&#x9762;&#x767D;&#x3044;^&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;' },
+
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-3',
+ 'desc': 'move caret forward 1 word in Japanese text (noun)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '&#x9762;&#x767D;&#x3044;^&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': [ '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;^&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;^&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;' ] },
+
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-4',
+ 'desc': 'move caret forward 1 word in Japanese text (Katakana)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;^&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;^&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;' },
+
+ { 'id': 'SM:m.f.w_TEXT-jp_SC-5',
+ 'desc': 'move caret forward 1 word in Japanese text (verb)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;^&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;&#x3002;',
+ 'expected': '&#x9762;&#x767D;&#x3044;&#x4F8B;&#x6587;&#x3092;&#x30C6;&#x30B9;&#x30C8;&#x3057;&#x307E;&#x3057;&#x3087;&#x3046;^&#x3002;' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: move forward/backward over words in Thai text',
+ 'tests': [
+ { 'id': 'SM:m.f.w_TEXT-th_SC-1',
+ 'desc': 'move caret forward 1 word in Thai text',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '^&#x0E17;&#x0E14;&#x0E2A;&#x0E2D;&#x0E1A;&#x0E01;&#x0E32;&#x0E23;&#x0E17;&#x0E33;&#x0E07;&#x0E32;&#x0E19;',
+ 'expected': '&#x0E17;&#x0E14;&#x0E2A;&#x0E2D;&#x0E1A;^&#x0E01;&#x0E32;&#x0E23;&#x0E17;&#x0E33;&#x0E07;&#x0E32;&#x0E19;' },
+
+ { 'id': 'SM:m.f.w_TEXT-th_SC-2',
+ 'desc': 'move caret forward 1 word in Thai text (mid-word)',
+ 'function': 'sel.modify("move", "forward", "word");',
+ 'pad': '&#x0E17;&#x0E14;&#x0E2A;&#x0E2D;&#x0E1A;&#x0E01;^&#x0E32;&#x0E23;&#x0E17;&#x0E33;&#x0E07;&#x0E32;&#x0E19;',
+ 'expected': '&#x0E17;&#x0E14;&#x0E2A;&#x0E2D;&#x0E1A;&#x0E01;&#x0E32;&#x0E23;^&#x0E17;&#x0E33;&#x0E07;&#x0E32;&#x0E19;' },
+
+ { 'id': 'SM:m.b.w_TEXT-th_SC-1',
+ 'desc': 'move caret backward 1 word in Thai text',
+ 'function': 'sel.modify("move", "backward", "word");',
+ 'pad': '&#x0E17;&#x0E14;&#x0E2A;&#x0E2D;&#x0E1A;&#x0E01;&#x0E32;&#x0E23;^&#x0E17;&#x0E33;&#x0E07;&#x0E32;&#x0E19;',
+ 'expected': '&#x0E17;&#x0E14;&#x0E2A;&#x0E2D;&#x0E1A;^&#x0E01;&#x0E32;&#x0E23;&#x0E17;&#x0E33;&#x0E07;&#x0E32;&#x0E19;' },
+
+ { 'id': 'SM:m.b.w_TEXT-th_SC-2',
+ 'desc': 'move caret backward 1 word in Thai text (mid-word)',
+ 'function': 'sel.modify("move", "backward", "word");',
+ 'pad': '&#x0E17;&#x0E14;&#x0E2A;&#x0E2D;&#x0E1A;&#x0E01;&#x0E32;&#x0E23;&#x0E17;&#x0E33;&#x0E07;&#x0E32;^&#x0E19;',
+ 'expected': [ '&#x0E17;&#x0E14;&#x0E2A;&#x0E2D;&#x0E1A;&#x0E01;&#x0E32;&#x0E23;^&#x0E17;&#x0E33;&#x0E07;&#x0E32;&#x0E19;',
+ '&#x0E17;&#x0E14;&#x0E2A;&#x0E2D;&#x0E1A;&#x0E01;&#x0E32;&#x0E23;&#x0E17;&#x0E33;^&#x0E07;&#x0E32;&#x0E19;' ] },
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection forward',
+ 'tests': [
+ { 'id': 'SM:e.f.c_TEXT-1_SC-1',
+ 'desc': 'extend selection 1 character forward',
+ 'function': 'sel.modify("extend", "forward", "character");',
+ 'pad': 'foo ^bar baz',
+ 'expected': 'foo [b]ar baz' },
+
+ { 'id': 'SM:e.f.c_TEXT-1_SI-1',
+ 'desc': 'extend selection 1 character forward',
+ 'function': 'sel.modify("extend", "forward", "character");',
+ 'pad': 'foo [b]ar baz',
+ 'expected': 'foo [ba]r baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SC-1',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': 'foo ^bar baz',
+ 'expected': 'foo [bar] baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SI-1',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': 'foo [b]ar baz',
+ 'expected': 'foo [bar] baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SI-2',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo [bar baz]' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection backward, shrinking it',
+ 'tests': [
+ { 'id': 'SM:e.b.c_TEXT-1_SI-2',
+ 'desc': 'extend selection 1 character backward',
+ 'function': 'sel.modify("extend", "backward", "character");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo [ba]r baz' },
+
+ { 'id': 'SM:e.b.c_TEXT-1_SI-1',
+ 'desc': 'extend selection 1 character backward',
+ 'function': 'sel.modify("extend", "backward", "character");',
+ 'pad': 'foo [b]ar baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SI-3',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo [bar baz]',
+ 'expected': 'foo [bar] baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SI-2',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo [bar] baz',
+ 'expected': 'foo ^bar baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SI-4',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo b[ar baz]',
+ 'expected': 'foo b[ar] baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SI-5',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo b[ar] baz',
+ 'expected': 'foo b^ar baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection backward, creating or extending a reverse selections',
+ 'tests': [
+ { 'id': 'SM:e.b.c_TEXT-1_SC-1',
+ 'desc': 'extend selection 1 character backward',
+ 'function': 'sel.modify("extend", "backward", "character");',
+ 'pad': 'foo b^ar baz',
+ 'expected': 'foo ]b[ar baz' },
+
+ { 'id': 'SM:e.b.c_TEXT-1_SIR-1',
+ 'desc': 'extend selection 1 character backward',
+ 'function': 'sel.modify("extend", "backward", "character");',
+ 'pad': 'foo b]a[r baz',
+ 'expected': 'foo ]ba[r baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SIR-1',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo b]a[r baz',
+ 'expected': 'foo ]ba[r baz' },
+
+ { 'id': 'SM:e.b.w_TEXT-1_SIR-2',
+ 'desc': 'extend selection 1 word backward',
+ 'function': 'sel.modify("extend", "backward", "word");',
+ 'pad': 'foo ]ba[r baz',
+ 'expected': ']foo ba[r baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection forward, shrinking a reverse selections',
+ 'tests': [
+ { 'id': 'SM:e.f.c_TEXT-1_SIR-1',
+ 'desc': 'extend selection 1 character forward',
+ 'function': 'sel.modify("extend", "forward", "character");',
+ 'pad': 'foo b]a[r baz',
+ 'expected': 'foo ba^r baz' },
+
+ { 'id': 'SM:e.f.c_TEXT-1_SIR-2',
+ 'desc': 'extend selection 1 character forward',
+ 'function': 'sel.modify("extend", "forward", "character");',
+ 'pad': 'foo ]ba[r baz',
+ 'expected': 'foo b]a[r baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SIR-1',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': 'foo ]ba[r baz',
+ 'expected': 'foo ba^r baz' },
+
+ { 'id': 'SM:e.f.w_TEXT-1_SIR-3',
+ 'desc': 'extend selection 1 word forward',
+ 'function': 'sel.modify("extend", "forward", "word");',
+ 'pad': ']foo ba[r baz',
+ 'expected': 'foo ]ba[r baz' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection forward to line boundary',
+ 'tests': [
+ { 'id': 'SM:e.f.lb_BR.BR-1_SC-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': 'fo^o<br>bar<br>baz',
+ 'expected': 'fo[o]<br>bar<br>baz' },
+
+ { 'id': 'SM:e.f.lb_BR.BR-1_SI-1',
+ 'desc': 'extend selection forward to next line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': 'fo[o]<br>bar<br>baz',
+ 'expected': 'fo[o<br>bar]<br>baz' },
+
+ { 'id': 'SM:e.f.lb_BR.BR-1_SM-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': 'fo[o<br>b]ar<br>baz',
+ 'expected': 'fo[o<br>bar]<br>baz' },
+
+ { 'id': 'SM:e.f.lb_P.P.P-1_SC-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': '<p>fo^o</p><p>bar</p><p>baz</p>',
+ 'expected': '<p>fo[o]</p><p>bar</p><p>baz</p>' },
+
+ { 'id': 'SM:e.f.lb_P.P.P-1_SI-1',
+ 'desc': 'extend selection forward to next line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': '<p>fo[o]</p><p>bar</p><p>baz</p>',
+ 'expected': '<p>fo[o</p><p>bar]</p><p>baz</p>' },
+
+ { 'id': 'SM:e.f.lb_P.P.P-1_SM-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': '<p>fo[o</p><p>b]ar</p><p>baz</p>',
+ 'expected': '<p>fo[o</p><p>bar]</p><p>baz</p>' },
+
+ { 'id': 'SM:e.f.lb_P.P.P-1_SMR-1',
+ 'desc': 'extend selection forward to line boundary',
+ 'function': 'sel.modify("extend", "forward", "lineboundary");',
+ 'pad': '<p>foo</p><p>b]a[r</p><p>baz</p>',
+ 'expected': '<p>foo</p><p>ba[r]</p><p>baz</p>' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection backward to line boundary',
+ 'tests': [
+ { 'id': 'SM:e.b.lb_BR.BR-1_SC-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': 'foo<br>bar<br>b^az',
+ 'expected': 'foo<br>bar<br>]b[az' },
+
+ { 'id': 'SM:e.b.lb_BR.BR-1_SIR-2',
+ 'desc': 'extend selection backward to previous line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': 'foo<br>bar<br>]b[az',
+ 'expected': 'foo<br>]bar<br>b[az' },
+
+ { 'id': 'SM:e.b.lb_BR.BR-1_SMR-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': 'foo<br>ba]r<br>b[az',
+ 'expected': 'foo<br>]bar<br>b[az' },
+
+ { 'id': 'SM:e.b.lb_P.P.P-1_SC-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': '<p>foo</p><p>bar</p><p>b^az</p>',
+ 'expected': '<p>foo</p><p>bar</p><p>]b[az</p>' },
+
+ { 'id': 'SM:e.b.lb_P.P.P-1_SIR-2',
+ 'desc': 'extend selection backward to previous line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': '<p>foo</p><p>bar</p><p>]b[az</p>',
+ 'expected': '<p>foo</p><p>]bar</p><p>b[az</p>' },
+
+ { 'id': 'SM:e.b.lb_P.P.P-1_SMR-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': '<p>foo</p><p>ba]r</p><p>b[az</p>',
+ 'expected': '<p>foo</p><p>]bar</p><p>b[az</p>' },
+
+ { 'id': 'SM:e.b.lb_P.P.P-1_SM-2',
+ 'desc': 'extend selection backward to line boundary',
+ 'function': 'sel.modify("extend", "backward", "lineboundary");',
+ 'pad': '<p>foo</p><p>b[a]r</p><p>baz</p>',
+ 'expected': '<p>foo</p><p>]b[ar</p><p>baz</p>' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection forward to next line (NOTE: use identical text in every line!)',
+ 'tests': [
+ { 'id': 'SM:e.f.l_BR.BR-2_SC-1',
+ 'desc': 'extend selection forward to next line',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': 'fo^o<br>foo<br>foo',
+ 'expected': 'fo[o<br>fo]o<br>foo' },
+
+ { 'id': 'SM:e.f.l_BR.BR-2_SI-1',
+ 'desc': 'extend selection forward to next line',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': 'fo[o]<br>foo<br>foo',
+ 'expected': 'fo[o<br>foo]<br>foo' },
+
+ { 'id': 'SM:e.f.l_BR.BR-2_SM-1',
+ 'desc': 'extend selection forward to next line',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': 'fo[o<br>f]oo<br>foo',
+ 'expected': 'fo[o<br>foo<br>f]oo' },
+
+ { 'id': 'SM:e.f.l_P.P-1_SC-1',
+ 'desc': 'extend selection forward to next line over paragraph boundaries',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': '<p>foo^bar</p><p>foobar</p>',
+ 'expected': '<p>foo[bar</p><p>foo]bar</p>' },
+
+ { 'id': 'SM:e.f.l_P.P-1_SMR-1',
+ 'desc': 'extend selection forward to next line over paragraph boundaries',
+ 'function': 'sel.modify("extend", "forward", "line");',
+ 'pad': '<p>fo]obar</p><p>foob[ar</p>',
+ 'expected': '<p>foobar</p><p>fo]ob[ar</p>' }
+ ]
+ },
+
+ { 'desc': 'sel.modify: extend selection backward to previous line (NOTE: use identical text in every line!)',
+ 'tests': [
+ { 'id': 'SM:e.b.l_BR.BR-2_SC-2',
+ 'desc': 'extend selection backward to previous line',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': 'foo<br>foo<br>f^oo',
+ 'expected': 'foo<br>f]oo<br>f[oo' },
+
+ { 'id': 'SM:e.b.l_BR.BR-2_SIR-2',
+ 'desc': 'extend selection backward to previous line',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': 'foo<br>foo<br>]f[oo',
+ 'expected': 'foo<br>]foo<br>f[oo' },
+
+ { 'id': 'SM:e.b.l_BR.BR-2_SMR-2',
+ 'desc': 'extend selection backward to previous line',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': 'foo<br>fo]o<br>f[oo',
+ 'expected': 'fo]o<br>foo<br>f[oo' },
+
+ { 'id': 'SM:e.b.l_P.P-1_SC-2',
+ 'desc': 'extend selection backward to next line over paragraph boundaries',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': '<p>foobar</p><p>foo^bar</p>',
+ 'expected': '<p>foo]bar</p><p>foo[bar</p>' },
+
+ { 'id': 'SM:e.b.l_P.P-1_SM-1',
+ 'desc': 'extend selection backward to next line over paragraph boundaries',
+ 'function': 'sel.modify("extend", "backward", "line");',
+ 'pad': '<p>fo[obar</p><p>foob]ar</p>',
+ 'expected': '<p>fo[ob]ar</p><p>foobar</p>' }
+ ]
+ },
+
+ { 'desc': 'sel.selectAllChildren(<element>)',
+ 'function': 'sel.selectAllChildren(doc.getElementById("div"));',
+ 'tests': [
+ { 'id': 'SAC:div_DIV-1_SC-1',
+ 'desc': 'selectAllChildren(<div>)',
+ 'pad': 'foo<div id="div">bar <span>ba^z</span></div>qoz',
+ 'expected': [ 'foo<div id="div">[bar <span>baz</span>}</div>qoz',
+ 'foo<div id="div">{bar <span>baz</span>}</div>qoz' ] },
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py
new file mode 100644
index 0000000000..adad65617e
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py
@@ -0,0 +1,462 @@
+
+UNAPPLY_TESTS = {
+ 'id': 'U',
+ 'caption': 'Unapply Existing Formatting Tests',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': False,
+ 'expected': 'foo[bar]baz',
+
+ 'RFC': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'remove link',
+ 'command': 'unlink',
+ 'tests': [
+ { 'id': 'UNLINK_A-1_SO',
+ 'desc': 'unlink wrapped <a> element',
+ 'pad': 'foo[<a>bar</a>]baz' },
+
+ { 'id': 'UNLINK_A-1_SW',
+ 'desc': 'unlink <a> element where the selection wraps the full content',
+ 'pad': 'foo<a>[bar]</a>baz' },
+
+ { 'id': 'UNLINK_An:a.h:id-1_SO',
+ 'desc': 'unlink wrapped <a> element that has a name and href attribute',
+ 'pad': 'foo[<a name="A" href="#UNLINK:An:a.h:id-1_SO">bar</a>]baz' },
+
+ { 'id': 'UNLINK_A-2_SO',
+ 'desc': 'unlink contained <a> element',
+ 'pad': 'foo[b<a>a</a>r]baz' },
+
+ { 'id': 'UNLINK_A2-1_SO',
+ 'desc': 'unlink 2 contained <a> elements',
+ 'pad': 'foo[<a>b</a>a<a>r</a>]baz' }
+ ]
+ }
+ ],
+
+ 'Proposed': [
+ { 'desc': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'remove bold',
+ 'command': 'bold',
+ 'tests': [
+ { 'id': 'B_B-1_SW',
+ 'rte1-id': 'u-bold-0',
+ 'desc': 'Selection within tags; remove <b> tags',
+ 'pad': 'foo<b>[bar]</b>baz' },
+
+ { 'id': 'B_B-1_SO',
+ 'desc': 'Selection outside of tags; remove <b> tags',
+ 'pad': 'foo[<b>bar</b>]baz' },
+
+ { 'id': 'B_B-1_SL',
+ 'desc': 'Selection oblique left; remove <b> tags',
+ 'pad': 'foo[<b>bar]</b>baz' },
+
+ { 'id': 'B_B-1_SR',
+ 'desc': 'Selection oblique right; remove <b> tags',
+ 'pad': 'foo<b>[bar</b>]baz' },
+
+ { 'id': 'B_STRONG-1_SW',
+ 'rte1-id': 'u-bold-1',
+ 'desc': 'Selection within tags; remove <strong> tags',
+ 'pad': 'foo<strong>[bar]</strong>baz' },
+
+ { 'id': 'B_STRONG-1_SO',
+ 'desc': 'Selection outside of tags; remove <strong> tags',
+ 'pad': 'foo[<strong>bar</strong>]baz' },
+
+ { 'id': 'B_STRONG-1_SL',
+ 'desc': 'Selection oblique left; remove <strong> tags',
+ 'pad': 'foo[<strong>bar]</strong>baz' },
+
+ { 'id': 'B_STRONG-1_SR',
+ 'desc': 'Selection oblique right; remove <strong> tags',
+ 'pad': 'foo<strong>[bar</strong>]baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SW',
+ 'rte1-id': 'u-bold-2',
+ 'desc': 'Selection within tags; remove "font-weight: bold"',
+ 'pad': 'foo<span style="font-weight: bold">[bar]</span>baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SO',
+ 'desc': 'Selection outside of tags; remove "font-weight: bold"',
+ 'pad': 'foo[<span style="font-weight: bold">bar</span>]baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SL',
+ 'desc': 'Selection oblique left; remove "font-weight: bold"',
+ 'pad': 'foo[<span style="font-weight: bold">bar]</span>baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SR',
+ 'desc': 'Selection oblique right; remove "font-weight: bold"',
+ 'pad': 'foo<span style="font-weight: bold">[bar</span>]baz' },
+
+ { 'id': 'B_B-P3-1_SO12',
+ 'desc': 'Unbolding multiple paragraphs in inside bolded content with content-model violation',
+ 'pad': '<b>{<p>foo</p><p>bar</p>}<p>baz</p></b>',
+ 'expected': [ '<p>[foo</p><p>bar]</p><p><b>baz</b></p>',
+ '<p>[foo</p><p>bar]</p><b><p>baz</p></b>' ] },
+
+ { 'id': 'B_B-P-I..P-1_SO-I',
+ 'desc': 'Unbolding italicized content inside bolded content with content-model violation',
+ 'pad': '<b><p>foo[<i>bar</i>]</p><p>baz</p></b>',
+ 'expected': [ '<p><b>foo</b><i>[bar]</i></p><p><b>baz</b></p>',
+ '<b><p>foo</p></b><p><i>[bar]</i></p><b><p>baz</p></b>' ] },
+
+ { 'id': 'B_B-2_SL',
+ 'desc': 'Remove partially covered bold, selection extends left',
+ 'pad': 'foo [bar <b>baz] qoz</b> quz sic',
+ 'expected': 'foo [bar baz]<b> qoz</b> quz sic' },
+
+ { 'id': 'B_B-2_SR',
+ 'desc': 'Remove partially covered bold, selection extends right',
+ 'pad': 'foo bar <b>baz [qoz</b> quz] sic',
+ 'expected': 'foo bar <b>baz </b>[qoz quz] sic' }
+ ]
+ },
+
+ { 'desc': 'remove italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_I-1_SW',
+ 'rte1-id': 'u-italic-0',
+ 'desc': 'Selection within tags; remove <i> tags',
+ 'pad': 'foo<i>[bar]</i>baz' },
+
+ { 'id': 'I_I-1_SO',
+ 'desc': 'Selection outside of tags; remove <i> tags',
+ 'pad': 'foo[<i>bar</i>]baz' },
+
+ { 'id': 'I_I-1_SL',
+ 'desc': 'Selection oblique left; remove <i> tags',
+ 'pad': 'foo[<i>bar]</i>baz' },
+
+ { 'id': 'I_I-1_SR',
+ 'desc': 'Selection oblique right; remove <i> tags',
+ 'pad': 'foo<i>[bar</i>]baz' },
+
+ { 'id': 'I_EM-1_SW',
+ 'rte1-id': 'u-italic-1',
+ 'desc': 'Selection within tags; remove <em> tags',
+ 'pad': 'foo<em>[bar]</em>baz' },
+
+ { 'id': 'I_EM-1_SO',
+ 'desc': 'Selection outside of tags; remove <em> tags',
+ 'pad': 'foo[<em>bar</em>]baz' },
+
+ { 'id': 'I_EM-1_SL',
+ 'desc': 'Selection oblique left; remove <em> tags',
+ 'pad': 'foo[<em>bar]</em>baz' },
+
+ { 'id': 'I_EM-1_SR',
+ 'desc': 'Selection oblique right; remove <em> tags',
+ 'pad': 'foo<em>[bar</em>]baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SW',
+ 'rte1-id': 'u-italic-2',
+ 'desc': 'Selection within tags; remove "font-style: italic"',
+ 'pad': 'foo<span style="font-style: italic">[bar]</span>baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SO',
+ 'desc': 'Selection outside of tags; Italicize "font-style: italic"',
+ 'pad': 'foo[<span style="font-style: italic">bar</span>]baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SL',
+ 'desc': 'Selection oblique left; Italicize "font-style: italic"',
+ 'pad': 'foo[<span style="font-style: italic">bar]</span>baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SR',
+ 'desc': 'Selection oblique right; Italicize "font-style: italic"',
+ 'pad': 'foo<span style="font-style: italic">[bar</span>]baz' },
+
+ { 'id': 'I_I-P3-1_SO2',
+ 'desc': 'Unitalicize content with content-model violation',
+ 'pad': '<i><p>foo</p>{<p>bar</p>}<p>baz</p></i>',
+ 'expected': [ '<p><i>foo</i></p><p>[bar]</p><p><i>baz</i></p>',
+ '<i><p>foo</p></i><p>[bar]</p><i><p>baz</p></i>' ] }
+ ]
+ },
+
+ { 'desc': 'remove underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_U-1_SW',
+ 'rte1-id': 'u-underline-0',
+ 'desc': 'Selection within tags; remove <u> tags',
+ 'pad': 'foo<u>[bar]</u>baz' },
+
+ { 'id': 'U_U-1_SO',
+ 'desc': 'Selection outside of tags; remove <u> tags',
+ 'pad': 'foo[<u>bar</u>]baz' },
+
+ { 'id': 'U_U-1_SL',
+ 'desc': 'Selection oblique left; remove <u> tags',
+ 'pad': 'foo[<u>bar]</u>baz' },
+
+ { 'id': 'U_U-1_SR',
+ 'desc': 'Selection oblique right; remove <u> tags',
+ 'pad': 'foo<u>[bar</u>]baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SW',
+ 'rte1-id': 'u-underline-1',
+ 'desc': 'Selection within tags; remove "text-decoration: underline"',
+ 'pad': 'foo<span style="text-decoration: underline">[bar]</span>baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SO',
+ 'desc': 'Selection outside of tags; remove "text-decoration: underline"',
+ 'pad': 'foo[<span style="text-decoration: underline">bar</span>]baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SL',
+ 'desc': 'Selection oblique left; remove "text-decoration: underline"',
+ 'pad': 'foo[<span style="text-decoration: underline">bar]</span>baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SR',
+ 'desc': 'Selection oblique right; remove "text-decoration: underline"',
+ 'pad': 'foo<span style="text-decoration: underline">[bar</span>]baz' },
+
+ { 'id': 'U_U-S-1_SO',
+ 'desc': 'Removing underline from underlined content with striked content',
+ 'pad': '<u>foo[bar<s>baz</s>quoz]</u>',
+ 'expected': '<u>foo</u>[bar<s>baz</s>quoz]' },
+
+ { 'id': 'U_U-S-2_SI',
+ 'desc': 'Removing underline from striked content inside underlined content',
+ 'pad': '<u><s>foo[bar]baz</s>quoz</u>',
+ 'expected': '<s><u>foo</u>[bar]<u>baz</u>quoz</s>' },
+
+ { 'id': 'U_U-P3-1_SO',
+ 'desc': 'Removing underline from underlined content with content-model violation',
+ 'pad': '<u><p>foo</p>{<p>bar</p>}<p>baz</p></u>',
+ 'expected': [ '<p><u>foo</u></p><p>[bar]</p><p><u>baz</u></p>',
+ '<u><p>foo</p></u><p>[bar]</p><u><p>baz</p></u>' ] }
+ ]
+ },
+
+ { 'desc': 'remove strike through',
+ 'command': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_S-1_SW',
+ 'rte1-id': 'u-strikethrough-1',
+ 'desc': 'Selection within tags; remove <s> tags',
+ 'pad': 'foo<s>[bar]</s>baz' },
+
+ { 'id': 'S_S-1_SO',
+ 'desc': 'Selection outside of tags; remove <s> tags',
+ 'pad': 'foo[<s>bar</s>]baz' },
+
+ { 'id': 'S_S-1_SL',
+ 'desc': 'Selection oblique left; remove <s> tags',
+ 'pad': 'foo[<s>bar]</s>baz' },
+
+ { 'id': 'S_S-1_SR',
+ 'desc': 'Selection oblique right; remove <s> tags',
+ 'pad': 'foo<s>[bar</s>]baz' },
+
+ { 'id': 'S_STRIKE-1_SW',
+ 'rte1-id': 'u-strikethrough-0',
+ 'desc': 'Selection within tags; remove <strike> tags',
+ 'pad': 'foo<strike>[bar]</strike>baz' },
+
+ { 'id': 'S_STRIKE-1_SO',
+ 'desc': 'Selection outside of tags; remove <strike> tags',
+ 'pad': 'foo[<strike>bar</strike>]baz' },
+
+ { 'id': 'S_STRIKE-1_SL',
+ 'desc': 'Selection oblique left; remove <strike> tags',
+ 'pad': 'foo[<strike>bar]</strike>baz' },
+
+ { 'id': 'S_STRIKE-2_SR',
+ 'desc': 'Selection oblique right; remove <strike> tags',
+ 'pad': 'foo<strike>[bar</strike>]baz' },
+
+ { 'id': 'S_DEL-1_SW',
+ 'rte1-id': 'u-strikethrough-2',
+ 'desc': 'Selection within tags; remove <del> tags',
+ 'pad': 'foo<del>[bar]</del>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SW',
+ 'rte1-id': 'u-strikethrough-3',
+ 'desc': 'Selection within tags; remove "text-decoration:line-through"',
+ 'pad': 'foo<span style="text-decoration:line-through">[bar]</span>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SO',
+ 'desc': 'Selection outside of tags; Italicize "text-decoration:line-through"',
+ 'pad': 'foo[<span style="text-decoration:line-through">bar</span>]baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SL',
+ 'desc': 'Selection oblique left; Italicize "text-decoration:line-through"',
+ 'pad': 'foo[<span style="text-decoration:line-through">bar]</span>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SR',
+ 'desc': 'Selection oblique right; Italicize "text-decoration:line-through"',
+ 'pad': 'foo<span style="text-decoration:line-through">[bar</span>]baz' },
+
+ { 'id': 'S_S-U-1_SI',
+ 'desc': 'Removing underline from underlined content inside striked content',
+ 'pad': '<s><u>foo[bar]baz</u>quoz</s>',
+ 'expected': '<s><u>foo</u></s><u>[bar]</u><s><u>baz</u>quoz</s>' },
+
+ { 'id': 'S_U-S-1_SI',
+ 'desc': 'Removing underline from striked content inside underlined content',
+ 'pad': '<u><s>foo[bar]baz</s>quoz</u>',
+ 'expected': '<u><s>foo</s>[bar]<s>baz</s>quoz</u>' }
+ ]
+ },
+
+ { 'desc': 'remove subscript',
+ 'command': 'subscript',
+ 'tests': [
+ { 'id': 'SUB_SUB-1_SW',
+ 'rte1-id': 'u-subscript-0',
+ 'desc': 'remove subscript',
+ 'pad': 'foo<sub>[bar]</sub>baz' },
+
+ { 'id': 'SUB_SPANs:va:sub-1_SW',
+ 'rte1-id': 'u-subscript-1',
+ 'desc': 'remove subscript',
+ 'pad': 'foo<span style="vertical-align: sub">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': 'remove superscript',
+ 'command': 'superscript',
+ 'tests': [
+ { 'id': 'SUP_SUP-1_SW',
+ 'rte1-id': 'u-superscript-0',
+ 'desc': 'remove superscript',
+ 'pad': 'foo<sup>[bar]</sup>baz' },
+
+ { 'id': 'SUP_SPANs:va:super-1_SW',
+ 'rte1-id': 'u-superscript-1',
+ 'desc': 'remove superscript',
+ 'pad': 'foo<span style="vertical-align: super">[bar]</span>baz' }
+ ]
+ },
+
+ { 'desc': 'remove links',
+ 'command': 'unlink',
+ 'tests': [
+ { 'id': 'UNLINK_Ahref:url-1_SW',
+ 'rte1-id': 'u-unlink-0',
+ 'desc': 'unlink an <a> element with href attribute where all children are selected',
+ 'pad': 'foo<a href="http://www.goo.gl">[bar]</a>baz' },
+
+ { 'id': 'UNLINK_A-1_SC',
+ 'desc': 'unlink an <a> element that contains the collapsed selection',
+ 'pad': 'foo<a>ba^r</a>baz',
+ 'expected': 'fooba^rbaz' },
+
+ { 'id': 'UNLINK_A-1_SI',
+ 'desc': 'unlink an <a> element that contains the whole selection',
+ 'pad': 'foo<a>b[a]r</a>baz',
+ 'expected': 'foob[a]rbaz' },
+
+ { 'id': 'UNLINK_A-2_SL',
+ 'desc': 'unlink a partially contained <a> element',
+ 'pad': 'foo[ba<a>r]ba</a>z' },
+
+ { 'id': 'UNLINK_A-3_SR',
+ 'desc': 'unlink a partially contained <a> element',
+ 'pad': 'fo<a>o[ba</a>r]baz' },
+
+ { 'id': 'UNLINK_As:d:b.fw:b-1_SW',
+ 'desc': 'unlink, preserving styles',
+ 'pad': 'foo<a href="#" style="display: block; font-weight: bold">[bar]</a>baz',
+ 'expected': 'foo<span style="display: block; font-weight: bold">[bar]</span>baz' },
+
+ { 'id': 'UNLINK_A-IMG-1_SO',
+ 'desc': 'unlink a linked image at the start of the content',
+ 'pad': '{<a href="#"><img src="pic.jpg" align="right" height="140" width="200"></a>abc]',
+ 'expected': '{<img src="pic.jpg" align="right" height="140" width="200">abc]' }
+ ]
+ },
+
+ { 'desc': 'outdent',
+ 'command': 'outdent',
+ 'tests': [
+ { 'id': 'OUTDENT_BQ-1_SW',
+ 'rte1-id': 'u-outdent-0',
+ 'desc': 'outdent (remove) a <blockquote>',
+ 'pad': 'foo<blockquote>[bar]</blockquote>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' },
+
+ { 'id': 'OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW',
+ 'rte1-id': 'u-outdent-1',
+ 'desc': 'outdent (remove) a styled <blockquote>',
+ 'pad': 'foo<blockquote class="webkit-indent-blockquote" style="margin: 0 0 0 40px; border: none; padding: 0px">[bar]</blockquote>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' },
+
+ { 'id': 'OUTDENT_OL-LI-1_SW',
+ 'rte1-id': 'u-outdent-3',
+ 'desc': 'outdent (remove) an ordered list',
+ 'pad': 'foo<ol><li>[bar]</li></ol>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' },
+
+ { 'id': 'OUTDENT_UL-LI-1_SW',
+ 'rte1-id': 'u-outdent-2',
+ 'desc': 'outdent (remove) an unordered list',
+ 'pad': 'foo<ul><li>[bar]</li></ul>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' },
+
+ { 'id': 'OUTDENT_DIV-1_SW',
+ 'rte1-id': 'u-outdent-4',
+ 'desc': 'outdent (remove) a styled <div> with margin',
+ 'pad': 'foo<div style="margin-left: 40px;">[bar]</div>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' }
+ ]
+ },
+
+ { 'desc': 'remove all formatting',
+ 'command': 'removeformat',
+ 'tests': [
+ { 'id': 'REMOVEFORMAT_B-1_SW',
+ 'rte1-id': 'u-removeformat-0',
+ 'desc': 'remove a <b> tag using "removeformat"',
+ 'pad': 'foo<b>[bar]</b>baz' },
+
+ { 'id': 'REMOVEFORMAT_Ahref:url-1_SW',
+ 'rte1-id': 'u-removeformat-0',
+ 'desc': 'remove a link using "removeformat"',
+ 'pad': 'foo<a href="http://www.goo.gl">[bar]</a>baz' },
+
+ { 'id': 'REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW',
+ 'rte1-id': 'u-removeformat-2',
+ 'desc': 'remove a table using "removeformat"',
+ 'pad': 'foo<table><tbody><tr><td>[bar]</td></tr></tbody></table>baz',
+ 'expected': [ 'foo<p>[bar]</p>baz',
+ 'foo<div>[bar]</div>baz' ],
+ 'accept': 'foo<br>[bar]<br>baz' }
+ ]
+ },
+
+ { 'desc': 'remove bookmark',
+ 'command': 'unbookmark',
+ 'tests': [
+ { 'id': 'UNBOOKMARK_An:name-1_SW',
+ 'rte1-id': 'u-unbookmark-0',
+ 'desc': 'unlink a bookmark (a named <a> element) where all children are selected',
+ 'pad': 'foo<a name="bookmark">[bar]</a>baz' }
+ ]
+ }
+ ]
+}
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py
new file mode 100644
index 0000000000..6f934a0f02
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py
@@ -0,0 +1,226 @@
+
+UNAPPLY_TESTS_CSS = {
+ 'id': 'UC',
+ 'caption': 'Unapply Existing Formatting Tests, using styleWithCSS',
+ 'checkAttrs': True,
+ 'checkStyle': True,
+ 'styleWithCSS': True,
+ 'expected': 'foo[bar]baz',
+
+ 'Proposed': [
+ { 'desc': '',
+ 'id': '',
+ 'command': '',
+ 'tests': [
+ ]
+ },
+
+ { 'desc': 'remove bold',
+ 'command': 'bold',
+ 'tests': [
+ { 'id': 'B_B-1_SW',
+ 'desc': 'Selection within tags; remove <b> tags',
+ 'pad': 'foo<b>[bar]</b>baz' },
+
+ { 'id': 'B_B-1_SO',
+ 'desc': 'Selection outside of tags; remove <b> tags',
+ 'pad': 'foo[<b>bar</b>]baz' },
+
+ { 'id': 'B_B-1_SL',
+ 'desc': 'Selection oblique left; remove <b> tags',
+ 'pad': 'foo[<b>bar]</b>baz' },
+
+ { 'id': 'B_B-1_SR',
+ 'desc': 'Selection oblique right; remove <b> tags',
+ 'pad': 'foo<b>[bar</b>]baz' },
+
+ { 'id': 'B_STRONG-1_SW',
+ 'desc': 'Selection within tags; remove <strong> tags',
+ 'pad': 'foo<strong>[bar]</strong>baz' },
+
+ { 'id': 'B_STRONG-1_SO',
+ 'desc': 'Selection outside of tags; remove <strong> tags',
+ 'pad': 'foo[<strong>bar</strong>]baz' },
+
+ { 'id': 'B_STRONG-1_SL',
+ 'desc': 'Selection oblique left; remove <strong> tags',
+ 'pad': 'foo[<strong>bar]</strong>baz' },
+
+ { 'id': 'B_STRONG-1_SR',
+ 'desc': 'Selection oblique right; remove <strong> tags',
+ 'pad': 'foo<strong>[bar</strong>]baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SW',
+ 'desc': 'Selection within tags; remove "font-weight: bold"',
+ 'pad': 'foo<span style="font-weight: bold">[bar]</span>baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SO',
+ 'desc': 'Selection outside of tags; remove "font-weight: bold"',
+ 'pad': 'foo[<span style="font-weight: bold">bar</span>]baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SL',
+ 'desc': 'Selection oblique left; remove "font-weight: bold"',
+ 'pad': 'foo[<span style="font-weight: bold">bar]</span>baz' },
+
+ { 'id': 'B_SPANs:fw:b-1_SR',
+ 'desc': 'Selection oblique right; remove "font-weight: bold"',
+ 'pad': 'foo<span style="font-weight: bold">[bar</span>]baz' }
+ ]
+ },
+
+ { 'desc': 'remove italic',
+ 'command': 'italic',
+ 'tests': [
+ { 'id': 'I_I-1_SW',
+ 'desc': 'Selection within tags; remove <i> tags',
+ 'pad': 'foo<i>[bar]</i>baz' },
+
+ { 'id': 'I_I-1_SO',
+ 'desc': 'Selection outside of tags; remove <i> tags',
+ 'pad': 'foo[<i>bar</i>]baz' },
+
+ { 'id': 'I_I-1_SL',
+ 'desc': 'Selection oblique left; remove <i> tags',
+ 'pad': 'foo[<i>bar]</i>baz' },
+
+ { 'id': 'I_I-1_SR',
+ 'desc': 'Selection oblique right; remove <i> tags',
+ 'pad': 'foo<i>[bar</i>]baz' },
+
+ { 'id': 'I_EM-1_SW',
+ 'desc': 'Selection within tags; remove <em> tags',
+ 'pad': 'foo<em>[bar]</em>baz' },
+
+ { 'id': 'I_EM-1_SO',
+ 'desc': 'Selection outside of tags; remove <em> tags',
+ 'pad': 'foo[<em>bar</em>]baz' },
+
+ { 'id': 'I_EM-1_SL',
+ 'desc': 'Selection oblique left; remove <em> tags',
+ 'pad': 'foo[<em>bar]</em>baz' },
+
+ { 'id': 'I_EM-1_SR',
+ 'desc': 'Selection oblique right; remove <em> tags',
+ 'pad': 'foo<em>[bar</em>]baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SW',
+ 'desc': 'Selection within tags; remove "font-style: italic"',
+ 'pad': 'foo<span style="font-style: italic">[bar]</span>baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SO',
+ 'desc': 'Selection outside of tags; Italicize "font-style: italic"',
+ 'pad': 'foo[<span style="font-style: italic">bar</span>]baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SL',
+ 'desc': 'Selection oblique left; Italicize "font-style: italic"',
+ 'pad': 'foo[<span style="font-style: italic">bar]</span>baz' },
+
+ { 'id': 'I_SPANs:fs:i-1_SR',
+ 'desc': 'Selection oblique right; Italicize "font-style: italic"',
+ 'pad': 'foo<span style="font-style: italic">[bar</span>]baz' }
+ ]
+ },
+
+ { 'desc': 'remove underline',
+ 'command': 'underline',
+ 'tests': [
+ { 'id': 'U_U-1_SW',
+ 'desc': 'Selection within tags; remove <u> tags',
+ 'pad': 'foo<u>[bar]</u>baz' },
+
+ { 'id': 'U_U-1_SO',
+ 'desc': 'Selection outside of tags; remove <u> tags',
+ 'pad': 'foo[<u>bar</u>]baz' },
+
+ { 'id': 'U_U-1_SL',
+ 'desc': 'Selection oblique left; remove <u> tags',
+ 'pad': 'foo[<u>bar]</u>baz' },
+
+ { 'id': 'U_U-1_SR',
+ 'desc': 'Selection oblique right; remove <u> tags',
+ 'pad': 'foo<u>[bar</u>]baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SW',
+ 'desc': 'Selection within tags; remove "text-decoration: underline"',
+ 'pad': 'foo<span style="text-decoration: underline">[bar]</span>baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SO',
+ 'desc': 'Selection outside of tags; remove "text-decoration: underline"',
+ 'pad': 'foo[<span style="text-decoration: underline">bar</span>]baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SL',
+ 'desc': 'Selection oblique left; remove "text-decoration: underline"',
+ 'pad': 'foo[<span style="text-decoration: underline">bar]</span>baz' },
+
+ { 'id': 'U_SPANs:td:u-1_SR',
+ 'desc': 'Selection oblique right; remove "text-decoration: underline"',
+ 'pad': 'foo<span style="text-decoration: underline">[bar</span>]baz' }
+ ]
+ },
+
+ { 'desc': 'remove strike-through',
+ 'command': 'strikethrough',
+ 'tests': [
+ { 'id': 'S_S-1_SW',
+ 'desc': 'Selection within tags; remove <s> tags',
+ 'pad': 'foo<s>[bar]</s>baz' },
+
+ { 'id': 'S_S-1_SO',
+ 'desc': 'Selection outside of tags; remove <s> tags',
+ 'pad': 'foo[<s>bar</s>]baz' },
+
+ { 'id': 'S_S-1_SL',
+ 'desc': 'Selection oblique left; remove <s> tags',
+ 'pad': 'foo[<s>bar]</s>baz' },
+
+ { 'id': 'S_S-1_SR',
+ 'desc': 'Selection oblique right; remove <s> tags',
+ 'pad': 'foo<s>[bar</s>]baz' },
+
+ { 'id': 'S_STRIKE-1_SW',
+ 'desc': 'Selection within tags; remove <strike> tags',
+ 'pad': 'foo<strike>[bar]</strike>baz' },
+
+ { 'id': 'S_STRIKE-1_SO',
+ 'desc': 'Selection outside of tags; remove <strike> tags',
+ 'pad': 'foo[<strike>bar</strike>]baz' },
+
+ { 'id': 'S_STRIKE-1_SL',
+ 'desc': 'Selection oblique left; remove <strike> tags',
+ 'pad': 'foo[<strike>bar]</strike>baz' },
+
+ { 'id': 'S_STRIKE-1_SR',
+ 'desc': 'Selection oblique right; remove <strike> tags',
+ 'pad': 'foo<strike>[bar</strike>]baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SW',
+ 'desc': 'Selection within tags; remove "text-decoration:line-through"',
+ 'pad': 'foo<span style="text-decoration:line-through">[bar]</span>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SO',
+ 'desc': 'Selection outside of tags; Italicize "text-decoration:line-through"',
+ 'pad': 'foo[<span style="text-decoration:line-through">bar</span>]baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SL',
+ 'desc': 'Selection oblique left; Italicize "text-decoration:line-through"',
+ 'pad': 'foo[<span style="text-decoration:line-through">bar]</span>baz' },
+
+ { 'id': 'S_SPANs:td:lt-1_SR',
+ 'desc': 'Selection oblique right; Italicize "text-decoration:line-through"',
+ 'pad': 'foo<span style="text-decoration:line-through">[bar</span>]baz' },
+
+ { 'id': 'S_SPANc:s-1_SW',
+ 'desc': 'Unapply "strike-through" on interited CSS style',
+ 'checkClass': True,
+ 'pad': 'foo<span class="s">[bar]</span>baz' },
+
+ { 'id': 'S_SPANc:s-2_SI',
+ 'desc': 'Unapply "strike-through" on interited CSS style',
+ 'pad': '<span class="s">foo[bar]baz</span>',
+ 'checkClass': True,
+ 'expected': '<span class="s">foo</span>[bar]<span class="s">baz</span>' }
+ ]
+ }
+ ]
+}
+
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html
new file mode 100644
index 0000000000..4e27b05540
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <title>Rich Text 2 Unit Test Example</title>
+
+ <!-- utility scripts -->
+ <script type="text/javascript" src="static/js/variables.js"></script>
+ <script type="text/javascript" src="static/js/canonicalize.js"></script>
+ <script type="text/javascript" src="static/js/compare.js"></script>
+ <script type="text/javascript" src="static/js/pad.js"></script>
+ <script type="text/javascript" src="static/js/range.js"></script>
+ <script type="text/javascript" src="static/js/units.js"></script>
+ <script type="text/javascript" src="static/js/run.js"></script>
+ <!-- you do not need static/js/output.js -->
+
+ <!--
+ Tests - note that those have the extensions .py,
+ but can be used as JS files directly.
+ -->
+ <script type="text/javascript" src="tests/selection.py"></script>
+ <script type="text/javascript" src="tests/apply.py"></script>
+ <script type="text/javascript" src="tests/applyCSS.py"></script>
+ <script type="text/javascript" src="tests/change.py"></script>
+ <script type="text/javascript" src="tests/changeCSS.py"></script>
+ <script type="text/javascript" src="tests/unapply.py"></script>
+ <script type="text/javascript" src="tests/unapplyCSS.py"></script>
+ <script type="text/javascript" src="tests/delete.py"></script>
+ <script type="text/javascript" src="tests/forwarddelete.py"></script>
+ <script type="text/javascript" src="tests/insert.py"></script>
+ <script type="text/javascript" src="tests/querySupported.py"></script>
+ <script type="text/javascript" src="tests/queryEnabled.py"></script>
+ <script type="text/javascript" src="tests/queryIndeterm.py"></script>
+ <script type="text/javascript" src="tests/queryState.py"></script>
+ <script type="text/javascript" src="tests/queryValue.py"></script>
+
+ <!-- Do something -->
+ <script type="text/javascript">
+ function runTest() {
+ initVariables();
+ initEditorDocs();
+
+ runTestSuite(UNAPPLY_TESTS);
+
+ // Below alert is just a simple demonstration on how to access the test results.
+ // Note that we only ran UNAPPLY tests above, so we have only results from that test set.
+ //
+ // The 'results' structure is as follows:
+ //
+ // results structure containing all results
+ // [<suite ID>] structure containing the results for the given suite *)
+ // .count number of tests in the given suite
+ // .valscore sum of all test value results (HTML or query value)
+ // .selscore sum of all selection results (HTML tests only)
+ // [<class ID>] structure containing the results for the given class **)
+ // .count number of tests in the given suite
+ // .valscore sum of all test value results (HTML or query value)
+ // .selscore sum of all selection results (HTML tests only)
+ // [<test ID>] structure containing the reults for a given test ***)
+ // .valscore value score (0 or 1), minimum over all containers
+ // .selscore selection score (0 or 1), minimum over all containers (HTML tests only)
+ // .valresult worst test value result (integer, see variables.js)
+ // .selresult worst selection result (integer, see variables.js)
+ // [<cont. ID>] structure containing the results of the test for a given container ****)
+ // .valscore value score (0 or 1)
+ // .selscore selection score (0 or 1)
+ // .valresult value result (integer, see variables.js)
+ // .selresult selection result (integer, see variables.js)
+ // .output output string (mainly for use by the online version)
+ // .innerHTML inner HTML of the testing container (<div> or <body>) after the test
+ // .outerHTML outer HTML of the testing container (<div> or <body>) after the test
+ // .bodyInnerHTML inner HTML of the <body> after the test
+ // .bodyOuterHTML outer HTML of the <body> after the test
+ //
+ // *) <suite ID>: a 1-3 character ID, e.g. UNAPPLY_TESTS.id, or 'U' (both referring the same suite)
+ // **) <class ID>: one of 'Proposed', 'RFC' or 'Finalized'
+ // ***) <test ID>: the ID of the test, without the leading 'RTE2-<suite ID>_' part
+ // ****) <container ID>: one of 'div' (test within a <div contenteditable="true">)
+ // 'dM' (test with designMode = 'on')
+ // 'body' (test within a <body contenteditable="true">)
+
+ alert("Result of 'Apply' tests:\nOut of " +
+ results[UNAPPLY_TESTS.id].count + " tests\n" +
+ results[UNAPPLY_TESTS.id].valscore + " had correct HTML, and\n" +
+ results[UNAPPLY_TESTS.id].selscore + " had a correct result selection\n(in all testing containers)." +
+ "\n\n" +
+ "Test RTE2-U_B_B-1_SW results with a contenteditable <body>:\n" +
+ results['U']['Proposed']['B_B-1_SW']['body'].valscore + " points for the value result, and\n" +
+ results['U']['Proposed']['B_B-1_SW']['body'].selscore + " points for the selection" +
+ ""
+ );
+ }
+ </script>
+</head>
+
+<body onload="runTest()">
+ <iframe name="iframe-dM" id="iframe-dM" src="static/editable-dM.html"></iframe>
+ <iframe name="iframe-body" id="iframe-body" src="static/editable-body.html"></iframe>
+ <iframe name="iframe-div" id="iframe-div" src="static/editable-div.html"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream b/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream
new file mode 100644
index 0000000000..baeb767454
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+set -x
+
+if test -d richtext2; then
+ rm -drf richtext2;
+fi
+
+svn checkout http://browserscope.googlecode.com/svn/trunk/categories/richtext2 richtext2 | tail -1 | sed 's/[^0-9]//g' > current_revision
+
+find richtext2 -type d -name .svn -exec rm -drf \{\} \; 2> /dev/null
+
+# Remove test_set.py and other similarly named files because they confuse our mochitest runner
+find richtext2 =type f -name test_\* -exec rm -rf \{\} \; 2> /dev/null
+
+hg add current_revision richtext2
+
+hg stat .
+
diff --git a/editor/libeditor/tests/browserscope/mochitest.toml b/editor/libeditor/tests/browserscope/mochitest.toml
new file mode 100644
index 0000000000..a2db4d4863
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/mochitest.toml
@@ -0,0 +1,60 @@
+[DEFAULT]
+support-files = [
+ "lib/richtext2/current_revision",
+ "lib/richtext2/richtext2/common.py",
+ "lib/richtext2/richtext2/unittestexample.html",
+ "lib/richtext2/richtext2/static/editable-dM.html",
+ "lib/richtext2/richtext2/static/editable.css",
+ "lib/richtext2/richtext2/static/editable-body.html",
+ "lib/richtext2/richtext2/static/editable-div.html",
+ "lib/richtext2/richtext2/static/js/variables.js",
+ "lib/richtext2/richtext2/static/js/range-bootstrap.js",
+ "lib/richtext2/richtext2/static/js/range.js",
+ "lib/richtext2/richtext2/static/js/output.js",
+ "lib/richtext2/richtext2/static/js/compare.js",
+ "lib/richtext2/richtext2/static/js/canonicalize.js",
+ "lib/richtext2/richtext2/static/js/pad.js",
+ "lib/richtext2/richtext2/static/js/run.js",
+ "lib/richtext2/richtext2/static/js/units.js",
+ "lib/richtext2/richtext2/static/common.css",
+ "lib/richtext2/richtext2/__init__.py",
+ "lib/richtext2/richtext2/handlers.py",
+ "lib/richtext2/richtext2/templates/output.html",
+ "lib/richtext2/richtext2/templates/richtext2.html",
+ "lib/richtext2/richtext2/tests/forwarddelete.py",
+ "lib/richtext2/richtext2/tests/selection.py",
+ "lib/richtext2/richtext2/tests/queryIndeterm.py",
+ "lib/richtext2/richtext2/tests/unapplyCSS.py",
+ "lib/richtext2/richtext2/tests/apply.py",
+ "lib/richtext2/richtext2/tests/unapply.py",
+ "lib/richtext2/richtext2/tests/change.py",
+ "lib/richtext2/richtext2/tests/queryState.py",
+ "lib/richtext2/richtext2/tests/queryValue.py",
+ "lib/richtext2/richtext2/tests/__init__.py",
+ "lib/richtext2/richtext2/tests/insert.py",
+ "lib/richtext2/richtext2/tests/queryEnabled.py",
+ "lib/richtext2/richtext2/tests/applyCSS.py",
+ "lib/richtext2/richtext2/tests/changeCSS.py",
+ "lib/richtext2/richtext2/tests/delete.py",
+ "lib/richtext2/richtext2/tests/querySupported.py",
+ "lib/richtext2/README",
+ "lib/richtext2/update_from_upstream",
+ "lib/richtext2/LICENSE",
+ "lib/richtext2/README.Mozilla",
+ "lib/richtext2/currentStatus.js",
+ "lib/richtext2/platformFailures.js",
+ "lib/richtext/current_revision",
+ "lib/richtext/README",
+ "lib/richtext/update_from_upstream",
+ "lib/richtext/LICENSE",
+ "lib/richtext/README.Mozilla",
+ "lib/richtext/richtext/editable.html",
+ "lib/richtext/richtext/richtext.html",
+ "lib/richtext/richtext/js/range.js",
+ "lib/richtext/currentStatus.js",
+]
+
+["test_richtext.html"]
+
+["test_richtext2.html"]
+skip-if = ["os == 'android'"] # Bug 1202045
diff --git a/editor/libeditor/tests/browserscope/test_richtext.html b/editor/libeditor/tests/browserscope/test_richtext.html
new file mode 100644
index 0000000000..c07f0a366a
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/test_richtext.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+BrowserScope richtext category tests
+-->
+<head>
+ <title>BrowserScope Richtext Tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="lib/richtext/currentStatus.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=550569">Mozilla Bug 550569</a>
+<p id="display"></p>
+<div id="content">
+ <iframe src="lib/richtext/richtext/richtext.html"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+// Running all of the tests can take a long time, try to account for it
+SimpleTest.requestLongerTimeout(5);
+
+function sendScore(results, continueParams) {
+ ok(results.length > 1, "At least one test should have been run");
+ for (var i = 1; i < results.length; ++i) {
+ var result = results[i];
+ let [type, command, param, success] = result.split(/[\-=]/);
+ var comp = is;
+ if (isKnownFailure(type, command, param)) {
+ comp = todo_is;
+ }
+ comp(success, "1", "Browserscope richtext category=" + type +
+ " test=" + command +
+ " param=" + param);
+ }
+}
+
+document.getElementsByTagName("iframe")[0].addEventListener("load", function() {
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/test_richtext2.html b/editor/libeditor/tests/browserscope/test_richtext2.html
new file mode 100644
index 0000000000..70aa74d802
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/test_richtext2.html
@@ -0,0 +1,238 @@
+<!DOCTYPE html>
+<html lang="en">
+<!--
+BrowserScope richtext2 category tests
+
+This test is originally based on the unit test example available as part of the
+RichText2 suite:
+http://code.google.com/p/browserscope/source/browse/trunk/categories/richtext2/unittestexample.html
+-->
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+
+ <title>BrowserScope Richtext2 Tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- utility scripts -->
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/variables.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/canonicalize.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/compare.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/pad.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/range.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/units.js"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/static/js/run.js"></script>
+ <!-- you do not need static/js/output.js -->
+
+ <!--
+ Tests - note that those have the extensions .py,
+ but can be used as JS files directly.
+ -->
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/selection.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/apply.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/applyCSS.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/change.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/changeCSS.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/unapply.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/unapplyCSS.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/delete.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/forwarddelete.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/insert.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/querySupported.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryEnabled.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryIndeterm.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryState.py"></script>
+ <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryValue.py"></script>
+
+ <script type="text/javascript" src="lib/richtext2/currentStatus.js"></script>
+ <script type="text/javascript" src="lib/richtext2/platformFailures.js"></script>
+
+ <!-- Do something -->
+ <script type="text/javascript">
+ // Set this constant to true in order to get the current status of the test suite.
+ // This is useful for updating the currentStatus.js file when an editor bug is fixed.
+ const UPDATE_TEST_RESULTS = false;
+
+ // some tests (at least RTE2-QE_PASTE_TEXT-1) require clipboard data
+ function startTest() {
+ SimpleTest.waitForClipboard("foo",
+ function() {
+ SpecialPowers.clipboardCopyString("foo");
+ },
+ runTest,
+ function() {
+ ok(false, "Failed to copy a string to the clipboard");
+ SimpleTest.finish();
+ }
+ );
+ }
+
+ /* eslint-disable-next-line complexity */
+ function runTest() {
+ initVariables();
+ initEditorDocs();
+
+ // These are all globals in the js and py files above */
+ /* eslint-disable no-undef */
+ const tests = [
+ SELECTION_TESTS,
+ APPLY_TESTS,
+ APPLY_TESTS_CSS,
+ CHANGE_TESTS,
+ CHANGE_TESTS_CSS,
+ UNAPPLY_TESTS,
+ UNAPPLY_TESTS_CSS,
+ DELETE_TESTS,
+ FORWARDDELETE_TESTS,
+ INSERT_TESTS,
+ QUERYSUPPORTED_TESTS,
+ QUERYENABLED_TESTS,
+ QUERYINDETERM_TESTS,
+ QUERYSTATE_TESTS,
+ QUERYVALUE_TESTS,
+ ];
+ /* eslint-enable no-undef */
+
+ for (let i = 0; i < tests.length; ++i) {
+ runTestSuite(tests[i]);
+ }
+
+ // Below alert is just a simple demonstration on how to access the test results.
+ // Note that we only ran UNAPPLY tests above, so we have only results from that test set.
+ //
+ // The 'results' structure is as follows:
+ //
+ // results structure containing all results
+ // [<suite ID>] structure containing the results for the given suite *)
+ // .count number of tests in the given suite
+ // .valscore sum of all test value results (HTML or query value)
+ // .selscore sum of all selection results (HTML tests only)
+ // [<class ID>] structure containing the results for the given class **)
+ // .count number of tests in the given suite
+ // .valscore sum of all test value results (HTML or query value)
+ // .selscore sum of all selection results (HTML tests only)
+ // [<test ID>] structure containing the reults for a given test ***)
+ // .valscore value score (0 or 1), minimum over all containers
+ // .selscore selection score (0 or 1), minimum over all containers (HTML tests only)
+ // .valresult worst test value result (integer, see variables.js)
+ // .selresult worst selection result (integer, see variables.js)
+ // [<cont. ID>] structure containing the results of the test for a given container ****)
+ // .valscore value score (0 or 1)
+ // .selscore selection score (0 or 1)
+ // .valresult value result (integer, see variables.js)
+ // .selresult selection result (integer, see variables.js)
+ // .output output string (mainly for use by the online version)
+ // .innerHTML inner HTML of the testing container (<div> or <body>) after the test
+ // .outerHTML outer HTML of the testing container (<div> or <body>) after the test
+ // .bodyInnerHTML inner HTML of the <body> after the test
+ // .bodyOuterHTML outer HTML of the <body> after the test
+ //
+ // *) <suite ID>: a 1-3 character ID, e.g. UNAPPLY_TESTS.id, or 'U' (both referring the same suite)
+ // **) <class ID>: one of 'Proposed', 'RFC' or 'Finalized'
+ // ***) <test ID>: the ID of the test, without the leading 'RTE2-<suite ID>_' part
+ // ****) <container ID>: one of 'div' (test within a <div contenteditable="true">)
+ // 'dM' (test with designMode = 'on')
+ // 'body' (test within a <body contenteditable="true">)
+
+ if (UPDATE_TEST_RESULTS) {
+ let newKnownFailures = {value: {}, select: {}};
+ for (let i = 0; i < tests.length; ++i) {
+ let category = tests[i];
+ for (let group in results[category.id]) {
+ switch (group) {
+ // Skip the known properties
+ case "count":
+ case "valscore":
+ case "selscore":
+ case "time":
+ break;
+ default:
+ for (let test_id in results[category.id][group]) {
+ switch (test_id) {
+ // Skip the known properties
+ case "count":
+ case "valscore":
+ case "selscore":
+ break;
+ default:
+ for (let structure in results[category.id][group][test_id]) {
+ switch (structure) {
+ // Only look at each test structure
+ case "dM":
+ case "body":
+ case "div":
+ if (!results[category.id][group][test_id][structure].valscore) {
+ newKnownFailures.value[category.id + "-" + group + "-" + test_id + "-" + structure] = true;
+ }
+ if (!results[category.id][group][test_id][structure].selscore) {
+ newKnownFailures.select[category.id + "-" + group + "-" + test_id + "-" + structure] = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ var resultContainer = document.getElementById("results");
+ resultContainer.style.display = "";
+ resultContainer.textContent = JSON.stringify(newKnownFailures);
+ } else {
+ for (let i = 0; i < tests.length; ++i) {
+ let category = tests[i];
+ for (let group in results[category.id]) {
+ switch (group) {
+ // Skip the known properties
+ case "count":
+ case "valscore":
+ case "selscore":
+ case "time":
+ break;
+ default:
+ for (let test_id in results[category.id][group]) {
+ switch (test_id) {
+ // Skip the known properties
+ case "count":
+ case "valscore":
+ case "selscore":
+ break;
+ default:
+ for (let structure in results[category.id][group][test_id]) {
+ switch (structure) {
+ // Only look at each test structure
+ case "dM":
+ case "body":
+ case "div":
+ var row = results[category.id][group][test_id][structure];
+ var testName = [category.id, group, test_id, structure].join("-");
+ (knownFailures.value[testName] || platformFailures.value[testName] ? todo_is : is)(
+ row.valscore, 1, "Browserscope richtext2 value: " + testName);
+ (knownFailures.select[testName] || platformFailures.select[testName] ? todo_is : is)(
+ row.selscore, 1, "Browserscope richtext2 selection: " + testName);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ // Running all of the tests can take a long time, try to account for it
+ SimpleTest.requestLongerTimeout(5);
+ </script>
+</head>
+
+<body onload="startTest()">
+ <iframe name="iframe-dM" id="iframe-dM" src="lib/richtext2/richtext2/static/editable-dM.html"></iframe>
+ <iframe name="iframe-body" id="iframe-body" src="lib/richtext2/richtext2/static/editable-body.html"></iframe>
+ <iframe name="iframe-div" id="iframe-div" src="lib/richtext2/richtext2/static/editable-div.html"></iframe>
+ <pre id="results" style="display: none"></pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/bug527935.html b/editor/libeditor/tests/bug527935.html
new file mode 100644
index 0000000000..1731734d29
--- /dev/null
+++ b/editor/libeditor/tests/bug527935.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<div id="content">
+ <iframe id="formTarget" name="formTarget"></iframe>
+ <form action="bug527935_2.html" target="formTarget">
+ <input name="test" id="initValue"><input type="submit">
+ </form>
+</div>
+</body>
+</html
diff --git a/editor/libeditor/tests/bug527935_2.html b/editor/libeditor/tests/bug527935_2.html
new file mode 100644
index 0000000000..96af0721d4
--- /dev/null
+++ b/editor/libeditor/tests/bug527935_2.html
@@ -0,0 +1 @@
+<html><body>dummy page</body></html>
diff --git a/editor/libeditor/tests/chrome.toml b/editor/libeditor/tests/chrome.toml
new file mode 100644
index 0000000000..a12dd64a76
--- /dev/null
+++ b/editor/libeditor/tests/chrome.toml
@@ -0,0 +1,31 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+
+["test_bug489202.xhtml"]
+
+["test_bug599983.xhtml"]
+
+["test_bug607584.xhtml"]
+
+["test_bug616590.xhtml"]
+
+["test_bug780908.xhtml"]
+
+["test_bug1397412.xhtml"]
+
+["test_can_undo_after_setting_value.xhtml"]
+
+["test_contenteditable_text_input_handling.html"]
+
+["test_cut_copy_delete_command_enabled.xhtml"]
+
+["test_htmleditor_keyevent_handling.html"]
+
+["test_pasteImgTextarea.xhtml"]
+support-files = ["green.png"]
+
+["test_texteditor_keyevent_handling.html"]
+skip-if = [
+ "debug && os == 'win'", # Bug 1116205, leaks on windows debug
+ "os == 'linux'", # Fails delete key on linux
+]
diff --git a/editor/libeditor/tests/data/cfhtml-chromium.txt b/editor/libeditor/tests/data/cfhtml-chromium.txt
new file mode 100644
index 0000000000..7e02537157
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-chromium.txt
Binary files differ
diff --git a/editor/libeditor/tests/data/cfhtml-firefox.txt b/editor/libeditor/tests/data/cfhtml-firefox.txt
new file mode 100644
index 0000000000..cc686d8562
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-firefox.txt
Binary files differ
diff --git a/editor/libeditor/tests/data/cfhtml-ie.txt b/editor/libeditor/tests/data/cfhtml-ie.txt
new file mode 100644
index 0000000000..a30bc5295e
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-ie.txt
Binary files differ
diff --git a/editor/libeditor/tests/data/cfhtml-nocontext.txt b/editor/libeditor/tests/data/cfhtml-nocontext.txt
new file mode 100644
index 0000000000..aa48822277
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-nocontext.txt
@@ -0,0 +1,18 @@
+Version:0.9
+StartHTML:-1
+EndHTML:-1
+StartFragment:0000000111
+EndFragment:0000000246
+<!--StartFragment-->
+<html>
+ <head>
+ <title>Test</title>
+
+ </head>
+ <body>
+ <p>
+ 3.<b>1415926535897932</b>
+ </p>
+ </body>
+</html>
+<!--EndFragment-->
diff --git a/editor/libeditor/tests/data/cfhtml-ooo.txt b/editor/libeditor/tests/data/cfhtml-ooo.txt
new file mode 100644
index 0000000000..0bcf7616ef
--- /dev/null
+++ b/editor/libeditor/tests/data/cfhtml-ooo.txt
Binary files differ
diff --git a/editor/libeditor/tests/file_bug289384-1.html b/editor/libeditor/tests/file_bug289384-1.html
new file mode 100644
index 0000000000..42b7a4da49
--- /dev/null
+++ b/editor/libeditor/tests/file_bug289384-1.html
@@ -0,0 +1 @@
+<a href="file_bug289384-2.html">link</a>
diff --git a/editor/libeditor/tests/file_bug289384-2.html b/editor/libeditor/tests/file_bug289384-2.html
new file mode 100644
index 0000000000..0356626279
--- /dev/null
+++ b/editor/libeditor/tests/file_bug289384-2.html
@@ -0,0 +1 @@
+<body contenteditable onload='opener.continueTest(window);'>foo bar</body>
diff --git a/editor/libeditor/tests/file_bug549262.html b/editor/libeditor/tests/file_bug549262.html
new file mode 100644
index 0000000000..36d2d26aae
--- /dev/null
+++ b/editor/libeditor/tests/file_bug549262.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div style="height: 25vh"></div>
+ <a href="">test</a>
+ <div style="height: 25vh"></div>
+ <div id="editor" contenteditable="true">abc</div>
+ <div style="height: 20000px;"></div>
+ </body>
+</html>
diff --git a/editor/libeditor/tests/file_bug586662.html b/editor/libeditor/tests/file_bug586662.html
new file mode 100644
index 0000000000..2989531975
--- /dev/null
+++ b/editor/libeditor/tests/file_bug586662.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div style="height: 20000px;"></div>
+ <textarea id="editor"></textarea>
+ </body>
+</html>
diff --git a/editor/libeditor/tests/file_bug611182.html b/editor/libeditor/tests/file_bug611182.html
new file mode 100644
index 0000000000..cc9e3e3765
--- /dev/null
+++ b/editor/libeditor/tests/file_bug611182.html
@@ -0,0 +1 @@
+<html><body>foo bar</body></html>
diff --git a/editor/libeditor/tests/file_bug611182.sjs b/editor/libeditor/tests/file_bug611182.sjs
new file mode 100644
index 0000000000..772aedef74
--- /dev/null
+++ b/editor/libeditor/tests/file_bug611182.sjs
@@ -0,0 +1,254 @@
+// SJS file for test_bug611182.html
+"use strict";
+
+const TESTS = [
+ {
+ ct: "text/html",
+ val: "<html contenteditable>fooz bar</html>",
+ },
+ {
+ ct: "text/html",
+ val: "<html contenteditable><body>fooz bar</body></html>",
+ },
+ {
+ ct: "text/html",
+ val: "<body contenteditable>fooz bar</body>",
+ },
+ {
+ ct: "text/html",
+ val: "<body contenteditable><p>fooz bar</p></body>",
+ },
+ {
+ ct: "text/html",
+ val: "<body contenteditable><div>fooz bar</div></body>",
+ },
+ {
+ ct: "text/html",
+ val: "<body contenteditable><span>fooz bar</span></body>",
+ },
+ {
+ ct: "text/html",
+ val: "<p contenteditable style='outline:none'>fooz bar</p>",
+ },
+ {
+ ct: "text/html",
+ val: "<!DOCTYPE html><html><body contenteditable>fooz bar</body></html>",
+ },
+ {
+ ct: "text/html",
+ val: "<!DOCTYPE html><html contenteditable><body>fooz bar</body></html>",
+ },
+ {
+ ct: "application/xhtml+xml",
+ val: '<html xmlns="http://www.w3.org/1999/xhtml"><body contenteditable="true">fooz bar</body></html>',
+ },
+ {
+ ct: "application/xhtml+xml",
+ val: '<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true"><body>fooz bar</body></html>',
+ },
+ {
+ ct: "text/html",
+ val: "<body onload=\"document.designMode='on'\">fooz bar</body>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "var old = document.body;" +
+ "old.parentNode.removeChild(old);" +
+ "var r = document.documentElement;" +
+ 'var b = document.createElement("body");' +
+ "r.appendChild(b);" +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ 'b.contentEditable = "true";' +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "var old = document.body;" +
+ "old.parentNode.removeChild(old);" +
+ "var r = document.documentElement;" +
+ 'var b = document.createElement("body");' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ 'b.contentEditable = "true";' +
+ "r.appendChild(b);" +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "var old = document.body;" +
+ "old.parentNode.removeChild(old);" +
+ "var r = document.documentElement;" +
+ 'var b = document.createElement("body");' +
+ "r.appendChild(b);" +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ 'b.setAttribute("contenteditable", "true");' +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "var old = document.body;" +
+ "old.parentNode.removeChild(old);" +
+ "var r = document.documentElement;" +
+ 'var b = document.createElement("body");' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ 'b.setAttribute("contenteditable", "true");' +
+ "r.appendChild(b);" +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "var old = document.body;" +
+ "old.parentNode.removeChild(old);" +
+ "var r = document.documentElement;" +
+ 'var b = document.createElement("body");' +
+ "r.appendChild(b);" +
+ 'b.contentEditable = "true";' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "var old = document.body;" +
+ "old.parentNode.removeChild(old);" +
+ "var r = document.documentElement;" +
+ 'var b = document.createElement("body");' +
+ 'b.contentEditable = "true";' +
+ "r.appendChild(b);" +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "var old = document.body;" +
+ "old.parentNode.removeChild(old);" +
+ "var r = document.documentElement;" +
+ 'var b = document.createElement("body");' +
+ "r.appendChild(b);" +
+ 'b.setAttribute("contenteditable", "true");' +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "var old = document.body;" +
+ "old.parentNode.removeChild(old);" +
+ "var r = document.documentElement;" +
+ 'var b = document.createElement("body");' +
+ 'b.setAttribute("contenteditable", "true");' +
+ "r.appendChild(b);" +
+ 'b.appendChild(document.createTextNode("fooz bar"));' +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "document.open();" +
+ 'document.write("<body contenteditable>fooz bar</body>");' +
+ "document.close();" +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "data:text/html,<html><script>" +
+ "onload = function() {" +
+ "document.open();" +
+ 'document.write("<body contenteditable><div>fooz bar</div></body>");' +
+ "document.close();" +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "document.open();" +
+ 'document.write("<body contenteditable><span>fooz bar</span></body>");' +
+ "document.close();" +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "document.open();" +
+ 'document.write("<p contenteditable style=\\"outline: none\\">fooz bar</p>");' +
+ "document.close();" +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "document.open();" +
+ 'document.write("<html contenteditable>fooz bar</html>");' +
+ "document.close();" +
+ "};" +
+ "</script><body></body></html>",
+ },
+ {
+ ct: "text/html",
+ val:
+ "<html><script>" +
+ "onload = function() {" +
+ "document.open();" +
+ 'document.write("<html contenteditable><body>fooz bar</body></html>");' +
+ "document.close();" +
+ "};" +
+ "</script><body></body></html>",
+ },
+];
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ let query = request.queryString;
+ if (query === "queryTotalTests") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(TESTS.length);
+ return;
+ }
+
+ var curTest = TESTS[query];
+ response.setHeader("Content-Type", curTest.ct, false);
+ response.write(curTest.val);
+}
diff --git a/editor/libeditor/tests/file_bug635636.xhtml b/editor/libeditor/tests/file_bug635636.xhtml
new file mode 100644
index 0000000000..18a8c50aa6
--- /dev/null
+++ b/editor/libeditor/tests/file_bug635636.xhtml
@@ -0,0 +1,3 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<div>1</div>
+</html>
diff --git a/editor/libeditor/tests/file_bug635636_2.html b/editor/libeditor/tests/file_bug635636_2.html
new file mode 100644
index 0000000000..bf0c8101f9
--- /dev/null
+++ b/editor/libeditor/tests/file_bug635636_2.html
@@ -0,0 +1 @@
+<html><body>2</body></html>
diff --git a/editor/libeditor/tests/file_bug674770-1.html b/editor/libeditor/tests/file_bug674770-1.html
new file mode 100644
index 0000000000..3460e3d6a7
--- /dev/null
+++ b/editor/libeditor/tests/file_bug674770-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE>
+<script>
+ localStorage.clicked = "true";
+ close();
+</script>
diff --git a/editor/libeditor/tests/file_bug795418-2.sjs b/editor/libeditor/tests/file_bug795418-2.sjs
new file mode 100644
index 0000000000..59aecd6f95
--- /dev/null
+++ b/editor/libeditor/tests/file_bug795418-2.sjs
@@ -0,0 +1,10 @@
+// SJS file for test_bug795418-2.html
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write(
+ "<html contenteditable='' xmlns='http://www.w3.org/1999/xhtml'><span>AB</span></html>"
+ );
+}
diff --git a/editor/libeditor/tests/file_bug915962.html b/editor/libeditor/tests/file_bug915962.html
new file mode 100644
index 0000000000..85c5139d3b
--- /dev/null
+++ b/editor/libeditor/tests/file_bug915962.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <button>Button</button>
+ <img src="green.png" usemap="#map">
+ <map name="map">
+ <!-- This URL ensures that the link doesn't get clicked, since
+ mochitests cannot access the outside network. -->
+ <area shape="rect" coords="0,0,10,10" href="https://youtube.com/">
+ </map>
+ <div style="height: 20000px;" tabindex="-1"><hr></div>
+ </body>
+</html>
diff --git a/editor/libeditor/tests/file_bug966155.html b/editor/libeditor/tests/file_bug966155.html
new file mode 100644
index 0000000000..04f55a9188
--- /dev/null
+++ b/editor/libeditor/tests/file_bug966155.html
@@ -0,0 +1 @@
+<input><iframe onload="contentDocument.designMode = 'on';">
diff --git a/editor/libeditor/tests/file_bug966552.html b/editor/libeditor/tests/file_bug966552.html
new file mode 100644
index 0000000000..5061c2e40d
--- /dev/null
+++ b/editor/libeditor/tests/file_bug966552.html
@@ -0,0 +1 @@
+<body onload="document.designMode='on'">test</body>
diff --git a/editor/libeditor/tests/file_sanitizer_on_paste.sjs b/editor/libeditor/tests/file_sanitizer_on_paste.sjs
new file mode 100644
index 0000000000..6d83609216
--- /dev/null
+++ b/editor/libeditor/tests/file_sanitizer_on_paste.sjs
@@ -0,0 +1,19 @@
+function handleRequest(request, response) {
+ if (request.queryString.includes("report")) {
+ response.setHeader("Content-Type", "text/javascript", false);
+ if (getState("loaded") == "loaded") {
+ response.write(
+ "ok(false, 'There was an attempt to preload the image.');"
+ );
+ } else {
+ response.write("ok(true, 'There was no attempt to preload the image.');");
+ }
+ response.write("SimpleTest.finish();");
+ } else {
+ setState("loaded", "loaded");
+ response.setHeader("Content-Type", "image/svg", false);
+ response.write(
+ "<svg xmlns='http://www.w3.org/2000/svg'>Not supposed to load this</svg>"
+ );
+ }
+}
diff --git a/editor/libeditor/tests/file_select_all_without_body.html b/editor/libeditor/tests/file_select_all_without_body.html
new file mode 100644
index 0000000000..06b7e6685c
--- /dev/null
+++ b/editor/libeditor/tests/file_select_all_without_body.html
@@ -0,0 +1,38 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function is(aLeft, aRight, aMessage) {
+ window.opener.SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function unload() {
+ window.opener.SimpleTest.finish();
+}
+
+function boom() {
+ var root = document.documentElement;
+ while (root.firstChild) {
+ root.firstChild.remove();
+ }
+ root.appendChild(document.createTextNode("Mozilla"));
+ root.focus();
+ let cespan = document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ cespan.setAttributeNS(null, "contenteditable", "true");
+ root.appendChild(cespan);
+ try {
+ document.execCommand("selectAll", false, null);
+ } catch (e) { }
+
+ is(window.getSelection().toString(), "Mozilla",
+ "The nodes are not selected");
+
+ window.close();
+}
+
+window.opener.SimpleTest.waitForFocus(boom, window);
+
+</script></head>
+
+<body onunload="unload();"></body>
+</html>
diff --git a/editor/libeditor/tests/green.png b/editor/libeditor/tests/green.png
new file mode 100644
index 0000000000..0aaec20932
--- /dev/null
+++ b/editor/libeditor/tests/green.png
Binary files differ
diff --git a/editor/libeditor/tests/mochitest.toml b/editor/libeditor/tests/mochitest.toml
new file mode 100644
index 0000000000..5af13503f7
--- /dev/null
+++ b/editor/libeditor/tests/mochitest.toml
@@ -0,0 +1,628 @@
+[DEFAULT]
+prefs = [
+ "apz.zoom-to-focused-input.enabled=false",
+ "ui.dragThresholdX=4", # Bug 1873142
+ "ui.dragThresholdY=4", # Bug 1873142
+]
+
+support-files = ["green.png"]
+
+["test_CF_HTML_clipboard.html"]
+skip-if = ["os != 'mac'"] # bug 574005
+support-files = [
+ "data/cfhtml-chromium.txt",
+ "data/cfhtml-firefox.txt",
+ "data/cfhtml-ie.txt",
+ "data/cfhtml-ooo.txt",
+ "data/cfhtml-nocontext.txt",
+]
+
+["test_abs_positioner_appearance.html"]
+
+["test_abs_positioner_hidden_during_dragging.html"]
+skip-if = ["os == 'android'"] # Sync with test_abs_positioner_positioning_elements.html
+
+["test_abs_positioner_positioning_elements.html"]
+skip-if = [
+ "os == 'android'", # Bug 1525959
+ "xorigin", # Inconsistent pass/fail in opt and debug
+]
+
+["test_backspace_vs.html"]
+
+["test_bug46555.html"]
+
+["test_bug200416.html"]
+
+["test_bug289384.html"]
+skip-if = ["os != 'mac'"]
+support-files = [
+ "file_bug289384-1.html",
+ "file_bug289384-2.html",
+]
+
+["test_bug290026.html"]
+
+["test_bug291780.html"]
+
+["test_bug309731.html"]
+
+["test_bug316447.html"]
+
+["test_bug318065.html"]
+
+["test_bug332636.html"]
+support-files = ["test_bug332636.html^headers^"]
+
+["test_bug358033.html"]
+
+["test_bug372345.html"]
+
+["test_bug404320.html"]
+
+["test_bug408231.html"]
+skip-if = ["os == 'android'"]
+
+["test_bug410986.html"]
+
+["test_bug414526.html"]
+
+["test_bug417418.html"]
+
+["test_bug426246.html"]
+
+["test_bug430392.html"]
+
+["test_bug439808.html"]
+
+["test_bug442186.html"]
+
+["test_bug455992.html"]
+
+["test_bug456244.html"]
+
+["test_bug460740.html"]
+
+["test_bug471319.html"]
+
+["test_bug471722.html"]
+
+["test_bug478725.html"]
+
+["test_bug480647.html"]
+
+["test_bug480972.html"]
+
+["test_bug483651.html"]
+
+["test_bug490879.html"]
+skip-if = [
+ "os == 'android'", # bug 1299578
+ "headless",
+]
+
+["test_bug502673.html"]
+
+["test_bug514156.html"]
+
+["test_bug520189.html"]
+
+["test_bug525389.html"]
+
+["test_bug537046.html"]
+
+["test_bug549262.html"]
+support-files = [
+ "file_bug549262.html",
+ "!/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+]
+
+["test_bug550434.html"]
+
+["test_bug551704.html"]
+
+["test_bug552782.html"]
+
+["test_bug567213.html"]
+
+["test_bug569988.html"]
+skip-if = ["os == 'android'"]
+
+["test_bug570144.html"]
+
+["test_bug578771.html"]
+
+["test_bug586662.html"]
+skip-if = ["true"] # bug 1376382
+support-files = ["file_bug586662.html"]
+
+["test_bug590554.html"]
+
+["test_bug592592.html"]
+
+["test_bug596001.html"]
+
+["test_bug596506.html"]
+
+["test_bug597331.html"]
+
+["test_bug597784.html"]
+
+["test_bug599322.html"]
+
+["test_bug599983.html"]
+
+["test_bug600570.html"]
+
+["test_bug603556.html"]
+
+["test_bug604532.html"]
+
+["test_bug607584.html"]
+
+["test_bug611182.html"]
+support-files = [
+ "file_bug611182.html",
+ "file_bug611182.sjs",
+]
+
+["test_bug612128.html"]
+
+["test_bug612447.html"]
+
+["test_bug622371.html"]
+
+["test_bug625452.html"]
+
+["test_bug629172.html"]
+skip-if = [
+ "os == 'android'",
+ "verify && os == 'mac'", # Due to resizer rendring issue of macOS
+]
+
+["test_bug629845.html"]
+
+["test_bug635636.html"]
+support-files = [
+ "file_bug635636.xhtml",
+ "file_bug635636_2.html",
+]
+
+["test_bug638596.html"]
+
+["test_bug641466.html"]
+
+["test_bug645914.html"]
+
+["test_bug646194.html"]
+
+["test_bug668599.html"]
+
+["test_bug674770-1.html"]
+skip-if = ["os == 'android'"]
+support-files = ["file_bug674770-1.html"]
+
+["test_bug674770-2.html"]
+skip-if = ["os == 'android'"]
+
+["test_bug674861.html"]
+
+["test_bug676401.html"]
+
+["test_bug677752.html"]
+
+["test_bug681229.html"]
+
+["test_bug686203.html"]
+
+["test_bug692520.html"]
+
+["test_bug697842.html"]
+
+["test_bug725069.html"]
+
+["test_bug735059.html"]
+
+["test_bug738366.html"]
+
+["test_bug740784.html"]
+
+["test_bug742261.html"]
+
+["test_bug757371.html"]
+
+["test_bug757771.html"]
+
+["test_bug772796.html"]
+
+["test_bug773262.html"]
+
+["test_bug780035.html"]
+
+["test_bug787432.html"]
+
+["test_bug790475.html"]
+
+["test_bug795418-2.html"]
+support-files = ["file_bug795418-2.sjs"]
+
+["test_bug795418-3.html"]
+
+["test_bug795418-4.html"]
+
+["test_bug795418-5.html"]
+
+["test_bug795418-6.html"]
+
+["test_bug795418.html"]
+
+["test_bug795785.html"]
+support-files = ["!/gfx/layers/apz/test/mochitest/apz_test_utils.js"]
+
+["test_bug796839.html"]
+
+["test_bug830600.html"]
+skip-if = ["os == 'android'"]
+
+["test_bug832025.html"]
+
+["test_bug850043.html"]
+
+["test_bug857487.html"]
+
+["test_bug858918.html"]
+
+["test_bug915962.html"]
+support-files = [
+ "file_bug915962.html",
+ "!/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+]
+
+["test_bug966155.html"]
+skip-if = ["os != 'win'"]
+support-files = ["file_bug966155.html"]
+
+["test_bug966552.html"]
+skip-if = ["os != 'win'"]
+support-files = ["file_bug966552.html"]
+
+["test_bug974309.html"]
+
+["test_bug998188.html"]
+
+["test_bug1026397.html"]
+
+["test_bug1053048.html"]
+
+["test_bug1068979.html"]
+
+["test_bug1094000.html"]
+
+["test_bug1102906.html"]
+skip-if = ["os == 'android'"]
+
+["test_bug1109465.html"]
+
+["test_bug1130651.html"]
+
+["test_bug1140105.html"]
+
+["test_bug1140617.html"]
+
+["test_bug1151186.html"]
+skip-if = ["os == 'win' && ccov && xorigin"] # high frequency intermittent
+
+["test_bug1153237.html"]
+
+["test_bug1162952.html"]
+
+["test_bug1181130-1.html"]
+
+["test_bug1181130-2.html"]
+
+["test_bug1186799.html"]
+
+["test_bug1230473.html"]
+
+["test_bug1247483.html"]
+
+["test_bug1248128.html"]
+
+["test_bug1248185.html"]
+
+["test_bug1248186.html"]
+
+["test_bug1250010.html"]
+
+["test_bug1257363.html"]
+
+["test_bug1258085.html"]
+
+["test_bug1268736.html"]
+
+["test_bug1270235.html"]
+
+["test_bug1306532.html"]
+skip-if = ["headless"]
+
+["test_bug1310912.html"]
+
+["test_bug1314790.html"]
+
+["test_bug1315065.html"]
+
+["test_bug1316302.html"]
+
+["test_bug1328023.html"]
+
+["test_bug1330796.html"]
+
+["test_bug1332876.html"]
+
+["test_bug1352799.html"]
+
+["test_bug1355792.html"]
+
+["test_bug1358025.html"]
+
+["test_bug1361008.html"]
+
+["test_bug1361052.html"]
+
+["test_bug1385905.html"]
+
+["test_bug1390562.html"]
+
+["test_bug1394758.html"]
+
+["test_bug1399722.html"]
+
+["test_bug1406726.html"]
+
+["test_bug1409520.html"]
+
+["test_bug1425997.html"]
+
+["test_bug1543312.html"]
+
+["test_bug1568996.html"]
+
+["test_bug1574596.html"]
+skip-if = ["os == 'android'"] #Bug 1575739
+
+["test_bug1581337.html"]
+
+["test_bug1619852.html"]
+
+["test_bug1620778.html"]
+
+["test_bug1649005.html"]
+
+["test_bug1659276.html"]
+
+["test_bug1704381.html"]
+
+["test_cannot_undo_after_reinitializing_editor.html"]
+
+["test_caret_move_in_vertical_content.html"]
+
+["test_cmd_absPos.html"]
+
+["test_cmd_backgroundColor.html"]
+
+["test_cmd_fontFace_with_empty_string.html"]
+
+["test_cmd_fontFace_with_tt.html"]
+
+["test_cmd_increaseFont.html"]
+
+["test_cmd_paragraphState.html"]
+
+["test_composition_event_created_in_chrome.html"]
+
+["test_composition_with_highlight_in_texteditor.html"]
+
+["test_contenteditable_copy_empty_selection.html"]
+
+["test_contenteditable_focus.html"]
+
+["test_cut_copy_delete_command_enabled.html"]
+
+["test_cut_copy_password.html"]
+
+["test_defaultParagraphSeparatorBR_between_blocks.html"]
+
+["test_doc_scrollbar_toggled_designMode_on_mousedown.html"]
+skip-if = ["os == 'android'"] # Needs interaction with the scrollbar
+
+["test_dom_input_event_on_htmleditor.html"]
+
+["test_dom_input_event_on_texteditor.html"]
+
+["test_dragdrop.html"]
+
+["test_execCommandPaste_noTarget.html"]
+
+["test_focus_caret_navigation_between_nested_editors.html"]
+
+["test_focused_document_element_becoming_editable.html"]
+
+["test_handle_new_lines.html"]
+
+["test_htmleditor_tab_key_handling.html"]
+
+["test_htmleditor_toggle_text_direction.html"]
+
+["test_initial_selection_and_caret_of_designMode.html"]
+
+["test_inlineTableEditing.html"]
+
+["test_inline_style_cache.html"]
+
+["test_insertHTML_starting_with_multiple_comment_nodes.html"]
+
+["test_insertParagraph_in_h2_and_li.html"]
+
+["test_insertParagraph_in_inline_editing_host.html"]
+
+["test_insertText_around_text_node_in_plaintext_mode.html"]
+
+["test_join_split_node_direction_change_command.html"]
+
+["test_keypress_untrusted_event.html"]
+
+["test_label_contenteditable.html"]
+
+["test_middle_click_paste.html"]
+skip-if = ["headless"]
+
+["test_native_key_bindings_in_shadow.html"]
+skip-if = ["os != 'linux' && os != 'mac'"] # Depends on NativeKeyBindings used only on Linux and macOS
+
+["test_nested_editor.html"]
+
+["test_new_plaintext_mail_with_plaintext_signature.html"]
+
+["test_nsIEditorMailSupport_insertAsCitedQuotation.html"]
+
+["test_nsIEditorMailSupport_insertTextWithQuotations.html"]
+skip-if = ["xorigin"] # Testing internal API for comm-central
+
+["test_nsIEditor_beginningOfDocument.html"]
+
+["test_nsIEditor_canUndo_canRedo.html"]
+
+["test_nsIEditor_clearUndoRedo.html"]
+
+["test_nsIEditor_deleteNode.html"]
+
+["test_nsIEditor_documentCharacterSet.html"]
+
+["test_nsIEditor_documentIsEmpty.html"]
+
+["test_nsIEditor_insertLineBreak.html"]
+
+["test_nsIEditor_insertNode.html"]
+
+["test_nsIEditor_isSelectionEditable.html"]
+
+["test_nsIEditor_outputToString.html"]
+
+["test_nsIEditor_undoAll.html"]
+
+["test_nsIEditor_undoRedoEnabled.html"]
+
+["test_nsIHTMLEditor_getElementOrParentByTagName.html"]
+
+["test_nsIHTMLEditor_getParagraphState.html"]
+
+["test_nsIHTMLEditor_getSelectedElement.html"]
+
+["test_nsIHTMLEditor_insertElementAtSelection.html"]
+
+["test_nsIHTMLEditor_removeInlineProperty.html"]
+
+["test_nsIHTMLEditor_selectElement.html"]
+
+["test_nsIHTMLEditor_setBackgroundColor.html"]
+
+["test_nsIHTMLObjectResizer_hideResizers.html"]
+
+["test_nsITableEditor_deleteTableCell.html"]
+
+["test_nsITableEditor_deleteTableCellContents.html"]
+
+["test_nsITableEditor_deleteTableColumn.html"]
+
+["test_nsITableEditor_deleteTableRow.html"]
+
+["test_nsITableEditor_getCellAt.html"]
+
+["test_nsITableEditor_getCellDataAt.html"]
+
+["test_nsITableEditor_getCellIndexes.html"]
+
+["test_nsITableEditor_getFirstRow.html"]
+
+["test_nsITableEditor_getFirstSelectedCellInTable.html"]
+
+["test_nsITableEditor_getSelectedCells.html"]
+
+["test_nsITableEditor_getSelectedCellsType.html"]
+
+["test_nsITableEditor_getSelectedOrParentTableElement.html"]
+
+["test_nsITableEditor_getTableSize.html"]
+
+["test_nsITableEditor_insertTableCell.html"]
+
+["test_nsITableEditor_insertTableColumn.html"]
+
+["test_nsITableEditor_insertTableRow.html"]
+
+["test_password_input_with_unmasked_range.html"]
+
+["test_password_paste.html"]
+
+["test_password_per_word_operation.html"]
+
+["test_password_unmask_API.html"]
+
+["test_pasteImgFromTransferable.html"]
+
+["test_pasteImgTextarea.html"]
+
+["test_paste_as_quote_in_text_control.html"]
+
+["test_paste_no_formatting.html"]
+
+["test_paste_redirect_focus_in_paste_event_listener.html"]
+
+["test_pasting_in_root_element.xhtml"]
+
+["test_pasting_in_temporarily_created_div_outside_body.html"]
+
+["test_pasting_table_rows.html"]
+skip-if = ["headless"] # The test calls `synthesizeKey`, see bug 1669923.
+
+["test_pasting_text_longer_than_maxlength.html"]
+
+["test_resizers_appearance.html"]
+
+["test_resizers_resizing_elements.html"]
+skip-if = ["verify && debug && os == 'win'"] # bug 1485293
+
+["test_root_element_replacement.html"]
+
+["test_sanitizer_on_paste.html"]
+support-files = ["file_sanitizer_on_paste.sjs"]
+
+["test_select_all_without_body.html"]
+support-files = ["file_select_all_without_body.html"]
+
+["test_selection_move_commands.html"]
+support-files = ["!/gfx/layers/apz/test/mochitest/apz_test_utils.js"]
+
+["test_setting_value_longer_than_maxlength_with_setUserInput.html"]
+
+["test_spellcheck_pref.html"]
+skip-if = ["os == 'android'"]
+
+["test_state_change_on_reframe.html"]
+
+["test_textarea_value_not_include_cr.html"]
+
+["test_texteditor_textnode.html"]
+
+["test_texteditor_tripleclick_setvalue.html"]
+
+["test_texteditor_wrapping_long_line.html"]
+
+["test_typing_at_edge_of_anchor.html"]
+
+["test_undo_after_spellchecker_replaces_word.html"]
+skip-if = ["os == 'android'"]
+
+["test_undo_redo_stack_after_setting_value.html"]
+
+["test_undo_with_editingui.html"]
diff --git a/editor/libeditor/tests/test_CF_HTML_clipboard.html b/editor/libeditor/tests/test_CF_HTML_clipboard.html
new file mode 100644
index 0000000000..51b0e03fed
--- /dev/null
+++ b/editor/libeditor/tests/test_CF_HTML_clipboard.html
@@ -0,0 +1,149 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=572642
+-->
+<head>
+ <title>Test for Bug 572642</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572642">Mozilla Bug 572642</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor1" contenteditable="true"></div>
+ <iframe id="editor2"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 572642 **/
+
+function copyCF_HTML(cfhtml, success, failure) {
+ const Cc = SpecialPowers.Cc;
+ const Ci = SpecialPowers.Ci;
+ const CF_HTML = "application/x-moz-nativehtml";
+
+ function getLoadContext() {
+ return SpecialPowers.wrap(window)
+ .docShell
+ .QueryInterface(Ci.nsILoadContext);
+ }
+
+ var cb = SpecialPowers.Services.clipboard;
+
+ var counter = 0;
+ function copyCF_HTML_worker(successFn, failureFn) {
+ if (++counter > 50) {
+ ok(false, "Timed out while polling clipboard for pasted data");
+ failure();
+ return;
+ }
+
+ var flavors = [CF_HTML];
+ if (!cb.hasDataMatchingFlavors(flavors, cb.kGlobalClipboard)) {
+ setTimeout(function() { copyCF_HTML_worker(successFn, failureFn); }, 100);
+ return;
+ }
+
+ var trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ trans.addDataFlavor(CF_HTML);
+ cb.getData(trans, cb.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext);
+ var data = SpecialPowers.createBlankObject();
+ try {
+ trans.getTransferData(CF_HTML, data);
+ data = SpecialPowers.wrap(data).value.QueryInterface(Ci.nsISupportsCString).data;
+ } catch (e) {
+ setTimeout(function() { copyCF_HTML_worker(successFn, failureFn); }, 100);
+ return;
+ }
+ success();
+ }
+
+ var trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ trans.addDataFlavor(CF_HTML);
+ var data = Cc["@mozilla.org/supports-cstring;1"].
+ createInstance(Ci.nsISupportsCString);
+ data.data = cfhtml;
+ trans.setTransferData(CF_HTML, data);
+ cb.setData(trans, null, cb.kGlobalClipboard);
+ copyCF_HTML_worker(success, failure);
+}
+
+function loadCF_HTMLdata(filename) {
+ var req = new XMLHttpRequest();
+ req.open("GET", filename, false);
+ req.overrideMimeType("text/plain; charset=x-user-defined");
+ req.send(null);
+ is(req.status, 200, "Could not read the binary file " + filename);
+ return req.responseText;
+}
+
+var gTests = [
+ // Copied from Firefox
+ {fileName: "cfhtml-firefox.txt", expected: "Firefox"},
+ // Copied from OpenOffice.org
+ {fileName: "cfhtml-ooo.txt", expected: "hello"},
+ // Copied from IE
+ {fileName: "cfhtml-ie.txt", expected: "browser"},
+ // Copied from Chromium
+ {fileName: "cfhtml-chromium.txt", expected: "Pacific"},
+ // CF_HTML with no context specified (StartHTML and EndHTML set to -1)
+ {fileName: "cfhtml-nocontext.txt", expected: "3.1415926535897932"},
+];
+var gTestIndex = 0;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("It's a legacy test.");
+
+for (var i = 0; i < gTests.length; ++i) {
+ gTests[i].data = loadCF_HTMLdata("data/" + gTests[i].fileName);
+}
+
+function runTest() {
+ var test = gTests[gTestIndex++];
+
+ copyCF_HTML(test.data, function() {
+ // contenteditable
+ var contentEditable = document.getElementById("editor1");
+ contentEditable.innerHTML = "";
+ contentEditable.focus();
+ synthesizeKey("v", {accelKey: true});
+ isnot(contentEditable.textContent.indexOf(test.expected), -1,
+ "Paste operation for " + test.fileName + " should be successful in contenteditable");
+
+ // designMode
+ var iframe = document.getElementById("editor2");
+ iframe.addEventListener("load", function() {
+ var doc = iframe.contentDocument;
+ var win = doc.defaultView;
+ setTimeout(function() {
+ win.addEventListener("focus", function() {
+ doc.designMode = "on";
+ synthesizeKey("v", {accelKey: true}, win);
+ isnot(doc.body.textContent.indexOf(test.expected), -1,
+ "Paste operation for " + test.fileName + " should be successful in designMode");
+
+ if (gTestIndex == gTests.length)
+ SimpleTest.finish();
+ else
+ runTest();
+ }, {once: true});
+ win.focus();
+ }, 0);
+ }, {once: true});
+ iframe.srcdoc = "foo";
+ }, SimpleTest.finish);
+}
+
+SimpleTest.waitForFocus(runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_abs_positioner_appearance.html b/editor/libeditor/tests/test_abs_positioner_appearance.html
new file mode 100644
index 0000000000..c516b9c511
--- /dev/null
+++ b/editor/libeditor/tests/test_abs_positioner_appearance.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for absolute positioner appearance</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="editor" contenteditable></div>
+<div id="clickaway" style="width: 3px; height: 3px;"></div>
+<img src="green.png"><!-- for ensuring to load the image at first test of <img> case -->
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ async function waitForSelectionChange() {
+ return new Promise(resolve => {
+ document.addEventListener("selectionchange", () => {
+ resolve();
+ }, {once: true});
+ });
+ }
+
+ let editor = document.getElementById("editor");
+ let outOfEditor = document.getElementById("clickaway");
+
+ async function testIfAppears() {
+ const kTests = [
+ { description: "absolute positioned <div>",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; top: 50px; left: 50px;\">positioned</div>",
+ movable: true,
+ },
+ { description: "fixed positioned <div>",
+ innerHTML: "<div id=\"target\" style=\"position: fixed; top: 50px; left: 50px;\">positioned</div>",
+ movable: false,
+ },
+ { description: "relative positioned <div>",
+ innerHTML: "<div id=\"target\" style=\"position: relative; top: 50px; left: 50px;\">positioned</div>",
+ movable: false,
+ },
+ ];
+
+ for (const kTest of kTests) {
+ const kDescription = "testIfAppears, " + kTest.description + ": ";
+ editor.innerHTML = kTest.innerHTML;
+ let target = document.getElementById("target");
+
+ document.execCommand("enableAbsolutePositionEditing", false, false);
+ ok(!document.queryCommandState("enableAbsolutePositionEditing"),
+ kDescription + "Absolute positioned element editor should be disabled by the call of execCommand");
+
+ synthesizeMouseAtCenter(outOfEditor, {});
+ let promiseSelectionChangeEvent1 = waitForSelectionChange();
+ synthesizeMouseAtCenter(target, {});
+ await promiseSelectionChangeEvent1;
+
+ ok(!target.hasAttribute("_moz_abspos"),
+ kDescription + "While enableAbsolutePositioner is disabled, positioner shouldn't appear");
+
+ document.execCommand("enableAbsolutePositionEditing", false, true);
+ ok(document.queryCommandState("enableAbsolutePositionEditing"),
+ kDescription + "Absolute positioned element editor should be enabled by the call of execCommand");
+
+ synthesizeMouseAtCenter(outOfEditor, {});
+ let promiseSelectionChangeEvent2 = waitForSelectionChange();
+ synthesizeMouseAtCenter(target, {});
+ await promiseSelectionChangeEvent2;
+
+ is(target.hasAttribute("_moz_abspos"), kTest.movable,
+ kDescription + (kTest.movable ? "While enableAbsolutePositionEditing is enabled, positioner should appear" :
+ "Even while enableAbsolutePositionEditing is enabled, positioner shouldn't appear"));
+
+ document.execCommand("enableAbsolutePositionEditing", false, false);
+ ok(!target.hasAttribute("_moz_abspos"),
+ kDescription + "When enableAbsolutePositionEditing is disabled even while positioner is visible, positioner should disappear");
+
+ document.execCommand("enableAbsolutePositionEditing", false, true);
+ is(target.hasAttribute("_moz_abspos"), kTest.movable,
+ kDescription + (kTest.movable ?
+ "When enableAbsolutePositionEditing is enabled when absolute positioned element is selected, positioner should appear" :
+ "Even if enableAbsolutePositionEditing is enabled when static positioned element is selected, positioner shouldn't appear"));
+ }
+ }
+
+ async function testStyle() {
+ // See HTMLEditor::GetTemporaryStyleForFocusedPositionedElement().
+ const kTests = [
+ { description: "background-color: transparent; color: white;",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; " +
+ "top: 50%; left: 50%; " +
+ "background-color: transparent; " +
+ "color: white;\">positioned</div>",
+ value: "black",
+ },
+ { description: "background-color: transparent; color: black;",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; " +
+ "top: 50%; left: 50%; " +
+ "background-color: transparent; " +
+ "color: black;\">positioned</div>",
+ value: "white",
+ },
+ { description: "background-color: black; color: white;",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; " +
+ "top: 50%; left: 50%; " +
+ "background-color: black; " +
+ "color: white;\">positioned</div>",
+ value: "",
+ },
+ { description: "background-color: white; color: black;",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; " +
+ "top: 50%; left: 50%; " +
+ "background-color: white; " +
+ "color: black;\">positioned</div>",
+ value: "",
+ },
+ { description: "background-image: green.png; background-color: black; color: white;",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; " +
+ "top: 50%; left: 50%; " +
+ "background-image: green.png; " +
+ "background-color: black; " +
+ "color: white;\">positioned</div>",
+ value: "",
+ },
+ { description: "background-image: green.png; background-color: white; color: black;",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; " +
+ "top: 50%; left: 50%; " +
+ "background-image: green.png; " +
+ "background-color: white; " +
+ "color: black;\">positioned</div>",
+ value: "",
+ },
+ { description: "background-image: green.png;",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; " +
+ "top: 50%; left: 50%; " +
+ "background-image: green.png;\">positioned</div>",
+ value: "white", // XXX Why? background-image is not "none"...
+ },
+ ];
+
+ document.execCommand("enableAbsolutePositionEditing", false, true);
+ ok(document.queryCommandState("enableAbsolutePositionEditing"),
+ "testStyle, Absolute positioned element editor should be enabled by the call of execCommand");
+
+ for (const kTest of kTests) {
+ const kDescription = "testStyle, " + kTest.description + ": ";
+
+ editor.innerHTML = kTest.innerHTML;
+ let target = document.getElementById("target");
+
+ synthesizeMouseAtCenter(outOfEditor, {});
+ let promiseSelectionChangeEvent = waitForSelectionChange();
+ synthesizeMouseAtCenter(target, {});
+ await promiseSelectionChangeEvent;
+
+ is(target.getAttribute("_moz_abspos"), kTest.value,
+ kDescription + "The value of _moz_abspos attribute is unexpected");
+ }
+ }
+
+ await testIfAppears();
+ await testStyle();
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_abs_positioner_hidden_during_dragging.html b/editor/libeditor/tests/test_abs_positioner_hidden_during_dragging.html
new file mode 100644
index 0000000000..3badd4c3b3
--- /dev/null
+++ b/editor/libeditor/tests/test_abs_positioner_hidden_during_dragging.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Drag absolutely positioned element to crash</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<div
+ contenteditable
+ style="
+ border: blue 1px solid;
+ margin: 20px;
+ "
+>
+ <div
+ style="
+ border: red 1px dashed;
+ background-color: rgba(255, 0, 0, 0.3);
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ overflow: auto;
+ "
+ >
+ This is absolutely positioned element.
+ </div>
+ <p>This is static positioned paragraph #1</p>
+ <p>This is static positioned paragraph #2</p>
+ <p>This is static positioned paragraph #3</p>
+ <p>This is static positioned paragraph #4</p>
+ <p>This is static positioned paragraph #5</p>
+ <p>This is static positioned paragraph #6</p>
+ <p>This is static positioned paragraph #7</p>
+</div>
+<script>
+"use strict";
+
+document.execCommand("enableAbsolutePositionEditing", false, true);
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ disableNonTestMouseEvents(true);
+ try {
+ document.querySelector("div[contenteditable").focus();
+
+ function promiseSelectionChange() {
+ return new Promise(resolve => {
+ document.addEventListener("selectionchange", () => {
+ resolve();
+ }, {once: true});
+ });
+ }
+
+ let absContainer = document.querySelector("div > div");
+ let rect = absContainer.getBoundingClientRect();
+ // We still don't have a way to retrieve the grabber. Therefore, we need
+ // to compute a point in the grabber from the absolutely positioned
+ // element's top-left coordinates.
+ const kOffsetX = 18;
+ const kOffsetY = -7;
+ let waitForSelectionChange = promiseSelectionChange();
+ synthesizeMouseAtCenter(absContainer, {});
+ await waitForSelectionChange;
+ synthesizeMouse(absContainer, kOffsetX, kOffsetY, {type: "mousedown"});
+ ok(absContainer.hasAttribute("_moz_abspos"), "Mousedown on the grabber should make it in drag mode");
+ synthesizeMouseAtPoint(100, 100, {type: "mousemove"});
+ synthesizeMouseAtPoint(100, 100, {type: "mouseup"});
+ isnot(absContainer.getBoundingClientRect().x, rect.x,
+ "The absolutely positioned container should be moved along x-axis");
+ isnot(absContainer.getBoundingClientRect().y, rect.y,
+ "The absolutely positioned container should be moved along y-axis");
+
+ rect = absContainer.getBoundingClientRect();
+ synthesizeMouse(absContainer, kOffsetX, kOffsetY, {type: "mousedown"});
+ ok(absContainer.hasAttribute("_moz_abspos"), "Mousedown on the grabber should make it in drag mode again");
+ document.execCommand("enableAbsolutePositionEditing", false, false);
+ ok(!absContainer.hasAttribute("_moz_abspos"), "Disabling the grabber makes it not in drag mode (before mouse move)");
+ synthesizeMouseAtPoint(50, 50, {type: "mousemove"});
+ synthesizeMouseAtPoint(50, 50, {type: "mouseup"});
+ is(absContainer.getBoundingClientRect().x, rect.x,
+ "The absolutely positioned container shouldn't be moved along x-axis due to the UI is killed by the web app (before mouse move)");
+ is(absContainer.getBoundingClientRect().y, rect.y,
+ "The absolutely positioned container shouldn't be moved along y-axis due to the UI is killed by the web app (before mouse move)");
+ document.execCommand("enableAbsolutePositionEditing", false, true);
+
+ rect = absContainer.getBoundingClientRect();
+ synthesizeMouse(absContainer, kOffsetX, kOffsetY, {type: "mousedown"});
+ ok(absContainer.hasAttribute("_moz_abspos"), "Mousedown on the grabber should make it in drag mode again");
+ document.execCommand("enableAbsolutePositionEditing", false, false);
+ synthesizeMouseAtPoint(50, 50, {type: "mousemove"});
+ ok(!absContainer.hasAttribute("_moz_abspos"), "Disabling the grabber makes it not in drag mode (during mouse move)");
+ synthesizeMouseAtPoint(50, 50, {type: "mousemove"});
+ synthesizeMouseAtPoint(50, 50, {type: "mouseup"});
+ is(absContainer.getBoundingClientRect().x, rect.x,
+ "The absolutely positioned container shouldn't be moved along x-axis due to the UI is killed by the web app (during mouse move)");
+ is(absContainer.getBoundingClientRect().y, rect.y,
+ "The absolutely positioned container shouldn't be moved along y-axis due to the UI is killed by the web app (during mouse move)");
+ } finally {
+ disableNonTestMouseEvents(false);
+ SimpleTest.finish();
+ }
+});
+</script>
diff --git a/editor/libeditor/tests/test_abs_positioner_positioning_elements.html b/editor/libeditor/tests/test_abs_positioner_positioning_elements.html
new file mode 100644
index 0000000000..45342933ba
--- /dev/null
+++ b/editor/libeditor/tests/test_abs_positioner_positioning_elements.html
@@ -0,0 +1,196 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for positioners of absolute positioned elements</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #target {
+ background-color: green;
+ }
+ </style>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" contenteditable style="height: 200px; width: 200px;"></div>
+<div id="clickaway" style="position: absolute; top: 250px; width: 10px; height: 10px; z-index: 100;"></div>
+<img src="green.png"><!-- for ensuring to load the image at first test of <img> case -->
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ document.execCommand("enableAbsolutePositionEditing", false, true);
+ ok(document.queryCommandState("enableAbsolutePositionEditing"),
+ "Absolute positioned element editor should be enabled by the call of execCommand");
+
+ let outOfEditor = document.getElementById("clickaway");
+
+ function cancel(e) { e.stopPropagation(); }
+ let content = document.getElementById("content");
+ content.addEventListener("mousedown", cancel);
+ content.addEventListener("mousemove", cancel);
+ content.addEventListener("mouseup", cancel);
+
+ async function waitForSelectionChange() {
+ return new Promise(resolve => {
+ document.addEventListener("selectionchange", () => {
+ resolve();
+ }, {once: true});
+ });
+ }
+
+ async function doTest(aDescription, aInnerHTML) {
+ content.innerHTML = aInnerHTML;
+ let description = aDescription + ": ";
+ let target = document.getElementById("target");
+ target.style.position = "absolute";
+
+ async function testPositioner(aDeltaX, aDeltaY) {
+ ok(true, description + "testPositioner(" + [aDeltaX, aDeltaY].join(", ") + ")");
+
+ // Reset the position of the target.
+ target.style.top = "50px";
+ target.style.left = "50px";
+
+ // Click on the target to show the positioner.
+ let promiseSelectionChangeEvent = waitForSelectionChange();
+ synthesizeMouseAtCenter(target, {});
+ await promiseSelectionChangeEvent;
+
+ let rect = target.getBoundingClientRect();
+
+ ok(target.hasAttribute("_moz_abspos"),
+ description + "While enableAbsolutePositionEditing is enabled, the positioner should appear");
+
+ // left is abs positioned element's left + margin-left + border-left-width + 12.
+ // XXX Perhaps, we need to add border-left-width here if you add new test to have thick border.
+ const kPositionerX = 18;
+ // top is abs positioned element's top + margin-top + border-top-width - 14.
+ // XXX Perhaps, we need to add border-top-width here if you add new test to have thick border.
+ const kPositionerY = -7;
+
+ let beforeInputEventExpected = true;
+ let beforeInputFired = false;
+ let inputEventExpected = true;
+ let inputFired = false;
+ function onBeforeInput(aEvent) {
+ beforeInputFired = true;
+ aEvent.preventDefault(); // For making sure this preventDefault() call does not cancel the operation.
+ if (!beforeInputEventExpected) {
+ ok(false, '"beforeinput" event should not be fired after stopping resizing');
+ return;
+ }
+ ok(aEvent instanceof InputEvent,
+ '"beforeinput" event for position changing of absolute position should be dispatched with InputEvent interface');
+ is(aEvent.cancelable, false,
+ '"beforeinput" event for position changing of absolute position container should not be cancelable');
+ is(aEvent.bubbles, true,
+ '"beforeinput" event for position changing of absolute position should always bubble');
+ is(aEvent.inputType, "",
+ 'inputType of "beforeinput" event for position changing of absolute position should be empty string');
+ is(aEvent.data, null,
+ 'data of "beforeinput" event for position changing of absolute position should be null');
+ is(aEvent.dataTransfer, null,
+ 'dataTransfer of "beforeinput" event for position changing of absolute position should be null');
+ let targetRanges = aEvent.getTargetRanges();
+ let selection = document.getSelection();
+ is(targetRanges.length, selection.rangeCount,
+ 'getTargetRanges() of "beforeinput" event for position changing of absolute position should return selection ranges');
+ if (targetRanges.length === selection.rangeCount) {
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ is(targetRanges[i].startContainer, range.startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match`);
+ is(targetRanges[i].startOffset, range.startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match`);
+ is(targetRanges[i].endContainer, range.endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match`);
+ is(targetRanges[i].endOffset, range.endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match`);
+ }
+ }
+ }
+ function onInput(aEvent) {
+ inputFired = true;
+ if (!inputEventExpected) {
+ ok(false, '"input" event should not be fired after stopping resizing');
+ return;
+ }
+ ok(aEvent instanceof InputEvent,
+ '"input" event for position changing of absolute position container should be dispatched with InputEvent interface');
+ is(aEvent.cancelable, false,
+ '"input" event for position changing of absolute position container should be never cancelable');
+ is(aEvent.bubbles, true,
+ '"input" event for position changing of absolute position should always bubble');
+ is(aEvent.inputType, "",
+ 'inputType of "input" event for position changing of absolute position should be empty string');
+ is(aEvent.data, null,
+ 'data of "input" event for position changing of absolute position should be null');
+ is(aEvent.dataTransfer, null,
+ 'dataTransfer of "input" event for position changing of absolute position should be null');
+ is(aEvent.getTargetRanges().length, 0,
+ 'getTargetRanges() of "input" event for position changing of absolute position should return empty array');
+ }
+
+ content.addEventListener("beforeinput", onBeforeInput);
+ content.addEventListener("input", onInput);
+
+ // Click on the positioner.
+ synthesizeMouse(target, kPositionerX, kPositionerY, {type: "mousedown"});
+ // Drag it delta pixels.
+ synthesizeMouse(target, kPositionerX + aDeltaX, kPositionerY + aDeltaY, {type: "mousemove"});
+ // Release the mouse button
+ synthesizeMouse(target, kPositionerX + aDeltaX, kPositionerY + aDeltaY, {type: "mouseup"});
+
+ ok(beforeInputFired, `${description}"beforeinput" event should be fired by moving absolute position container`);
+ ok(inputFired, `${description}"input" event should be fired by moving absolute position container`);
+
+ beforeInputEventExpected = false;
+ inputEventExpected = false;
+
+ // Move the mouse delta more pixels to the same direction to make sure that the
+ // positioning operation has stopped.
+ synthesizeMouse(target, kPositionerX + aDeltaX * 2, kPositionerY + aDeltaY * 2, {type: "mousemove"});
+ // Click outside of the image to hide the positioner.
+ synthesizeMouseAtCenter(outOfEditor, {});
+
+ content.removeEventListener("beforeinput", onBeforeInput);
+ content.removeEventListener("input", onInput);
+
+ // Get the new dimensions for the absolute positioned element.
+ let newRect = target.getBoundingClientRect();
+ isfuzzy(newRect.x, rect.x + aDeltaX, 1, description + "The left should be increased by " + aDeltaX + " pixels");
+ isfuzzy(newRect.y, rect.y + aDeltaY, 1, description + "The top should be increased by " + aDeltaY + "pixels");
+ }
+
+ await testPositioner( 10, 10);
+ await testPositioner( 10, -10);
+ await testPositioner(-10, 10);
+ await testPositioner(-10, -10);
+ }
+
+ const kTests = [
+ { description: "Positioner for <img>",
+ innerHTML: "<img id=\"target\" src=\"green.png\">",
+ },
+ { description: "Positioner for <table>",
+ innerHTML: "<table id=\"target\" border><tr><td>cell</td><td>cell</td></tr></table>",
+ },
+ { description: "Positioner for <div>",
+ innerHTML: "<div id=\"target\">div element</div>",
+ },
+ ];
+
+ for (const kTest of kTests) {
+ await doTest(kTest.description, kTest.innerHTML);
+ }
+ content.innerHTML = "";
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_backspace_vs.html b/editor/libeditor/tests/test_backspace_vs.html
new file mode 100644
index 0000000000..d52d83f79e
--- /dev/null
+++ b/editor/libeditor/tests/test_backspace_vs.html
@@ -0,0 +1,128 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1216427
+-->
+<head>
+ <title>Test for Bug 1216427</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1216427">Mozilla Bug 1216427</a>
+<p id="display"></p>
+<div id="content">
+ <div id="edit1" contenteditable="true">a&#x263a;&#xfe0f;b</div><!-- BMP symbol with VS16 -->
+ <div id="edit2" contenteditable="true">a&#x1f310;&#xfe0e;b</div><!-- plane 1 symbol with VS15 -->
+ <div id="edit3" contenteditable="true">a&#x3402;&#xE0100;b</div><!-- BMP ideograph with VS17 -->
+ <div id="edit4" contenteditable="true">a&#x20000;&#xE0101;b</div><!-- SMP ideograph with VS18 -->
+ <div id="edit5" contenteditable="true">a&#x263a;&#xfe01;&#xfe02;&#xfe03;b</div><!-- BMP symbol with extra VSes -->
+ <div id="edit6" contenteditable="true">a&#x20000;&#xE0100;&#xE0101;&#xE0102;b</div><!-- SMP symbol with extra VSes -->
+ <!-- The Regional Indicator combinations here were supported by Apple Color Emoji
+ even prior to the major extension of coverage in the 10.10.5 timeframe. -->
+ <div id="edit7" contenteditable="true">a&#x1F1E8;&#x1F1F3;b</div><!-- Regional Indicator flag: CN -->
+ <div id="edit8" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;b</div><!-- two RI flags: CN, DE -->
+ <div id="edit9" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;b</div><!-- three RI flags: CN, DE, ES -->
+ <div id="edit10" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;&#x1F1EB;&#x1F1F7;b</div><!-- four RI flags: CN, DE, ES, FR -->
+ <div id="edit11" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;&#x1F1EB;&#x1F1F7;&#x1F1EC;&#x1F1E7;b</div><!-- five RI flags: CN, DE, ES, FR, GB -->
+
+ <div id="edit1b" contenteditable="true">a&#x263a;&#xfe0f;b</div><!-- BMP symbol with VS16 -->
+ <div id="edit2b" contenteditable="true">a&#x1f310;&#xfe0e;b</div><!-- plane 1 symbol with VS15 -->
+ <div id="edit3b" contenteditable="true">a&#x3402;&#xE0100;b</div><!-- BMP ideograph with VS17 -->
+ <div id="edit4b" contenteditable="true">a&#x20000;&#xE0101;b</div><!-- SMP ideograph with VS18 -->
+ <div id="edit5b" contenteditable="true">a&#x263a;&#xfe01;&#xfe02;&#xfe03;b</div><!-- BMP symbol with extra VSes -->
+ <div id="edit6b" contenteditable="true">a&#x20000;&#xE0100;&#xE0101;&#xE0102;b</div><!-- SMP symbol with extra VSes -->
+ <div id="edit7b" contenteditable="true">a&#x1F1E8;&#x1F1F3;b</div><!-- Regional Indicator flag: CN -->
+ <div id="edit8b" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;b</div><!-- two RI flags: CN, DE -->
+ <div id="edit9b" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;b</div><!-- three RI flags: CN, DE, ES -->
+ <div id="edit10b" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;&#x1F1EB;&#x1F1F7;b</div><!-- four RI flags: CN, DE, ES, FR -->
+ <div id="edit11b" contenteditable="true">a&#x1F1E8;&#x1F1F3;&#x1F1E9;&#x1F1EA;&#x1F1EA;&#x1F1F8;&#x1F1EB;&#x1F1F7;&#x1F1EC;&#x1F1E7;b</div><!-- five RI flags: CN, DE, ES, FR, GB -->
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1216427 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+function test(edit, bsCount) {
+ edit.focus();
+ var sel = window.getSelection();
+ sel.collapse(edit.childNodes[0], edit.textContent.length - 1);
+ for (let i = 0; i < bsCount; ++i) {
+ synthesizeKey("KEY_Backspace");
+ }
+ is(edit.textContent, "ab", "The backspace key should delete the characters correctly");
+}
+
+function testWithMove(edit, offset, bsCount) {
+ edit.focus();
+ var sel = window.getSelection();
+ sel.collapse(edit.childNodes[0], 0);
+ var i;
+ for (i = 0; i < offset; ++i) {
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_ArrowRight");
+ }
+ synthesizeKey("KEY_Backspace", {repeat: bsCount});
+ is(edit.textContent, "ab", "The backspace key should delete the characters correctly");
+}
+
+function runTest() {
+ /* test backspace-deletion of the middle character(s) */
+ test(document.getElementById("edit1"), 1);
+ test(document.getElementById("edit2"), 1);
+ test(document.getElementById("edit3"), 1);
+ test(document.getElementById("edit4"), 1);
+ test(document.getElementById("edit5"), 1);
+ test(document.getElementById("edit6"), 1);
+
+ /*
+ * Tests with Regional Indicator flags: these behave differently depending
+ * whether an emoji font is present, as ligated flags are edited as single
+ * characters whereas non-ligated RI characters act individually.
+ *
+ * For now, only rely on such an emoji font on OS X 10.7+. (Note that the
+ * Segoe UI Emoji font on Win8.1 and Win10 does not implement Regional
+ * Indicator flags.)
+ *
+ * Once the Firefox Emoji font is ready, we can load that via @font-face
+ * and expect these tests to work across all platforms.
+ */
+ let hasEmojiFont =
+ (navigator.platform.indexOf("Mac") == 0 &&
+ /10\.([7-9]|[1-9][0-9])/.test(navigator.oscpu));
+
+ if (hasEmojiFont) {
+ test(document.getElementById("edit7"), 1);
+ test(document.getElementById("edit8"), 2);
+ test(document.getElementById("edit9"), 3);
+ test(document.getElementById("edit10"), 4);
+ test(document.getElementById("edit11"), 5);
+ }
+
+ /* extra tests with the use of RIGHT and LEFT to get to the right place */
+ testWithMove(document.getElementById("edit1b"), 2, 1);
+ testWithMove(document.getElementById("edit2b"), 2, 1);
+ testWithMove(document.getElementById("edit3b"), 2, 1);
+ testWithMove(document.getElementById("edit4b"), 2, 1);
+ testWithMove(document.getElementById("edit5b"), 2, 1);
+ testWithMove(document.getElementById("edit6b"), 2, 1);
+ if (hasEmojiFont) {
+ testWithMove(document.getElementById("edit7b"), 2, 1);
+ testWithMove(document.getElementById("edit8b"), 3, 2);
+ testWithMove(document.getElementById("edit9b"), 4, 3);
+ testWithMove(document.getElementById("edit10b"), 5, 4);
+ testWithMove(document.getElementById("edit11b"), 6, 5);
+ }
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1026397.html b/editor/libeditor/tests/test_bug1026397.html
new file mode 100644
index 0000000000..ef54befe57
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1026397.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1026397
+-->
+<head>
+ <title>Test for Bug 1026397</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1026397">Mozilla Bug 1026397</a>
+<p id="display"></p>
+<div id="content">
+<input id="input">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1026397 **/
+SimpleTest.waitForExplicitFinish();
+
+function runTests() {
+ var input = document.getElementById("input");
+ input.focus();
+
+ function doTest(aMaxLength, aInitialValue, aCaretOffset,
+ aInsertString, aExpectedValueDuringComposition,
+ aExpectedValueAfterCompositionEnd, aAdditionalExplanation) {
+ input.value = aInitialValue;
+ var maxLengthStr = "";
+ if (aMaxLength >= 0) {
+ input.maxLength = aMaxLength;
+ maxLengthStr = aMaxLength.toString();
+ } else {
+ input.removeAttribute("maxlength");
+ maxLengthStr = "not specified";
+ }
+ input.selectionStart = input.selectionEnd = aCaretOffset;
+ if (aAdditionalExplanation) {
+ aAdditionalExplanation = " " + aAdditionalExplanation;
+ } else {
+ aAdditionalExplanation = "";
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": aInsertString,
+ "clauses":
+ [
+ { "length": aInsertString.length, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": aInsertString.length, "length": 0 },
+ });
+ is(input.value, aExpectedValueDuringComposition,
+ "The value of input whose maxlength is " + maxLengthStr + " should be "
+ + aExpectedValueDuringComposition + " during composition" + aAdditionalExplanation);
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(input.value, aExpectedValueAfterCompositionEnd,
+ "The value of input whose maxlength is " + maxLengthStr + " should be "
+ + aExpectedValueAfterCompositionEnd + " after compositionend" + aAdditionalExplanation);
+ }
+
+ // maxlength hasn't been specified yet.
+ doTest(-1, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6");
+
+ // maxlength="1"
+ doTest(1, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "");
+
+ // maxlength="2"
+ doTest(2, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7");
+ doTest(2, "X", 1, "\uD842\uDFB7\u91CE\u5BB6", "X\uD842\uDFB7\u91CE\u5BB6", "X");
+ doTest(2, "Y", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6Y", "Y");
+
+ // maxlength="3"
+ doTest(3, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE");
+ doTest(3, "A", 1, "\uD842\uDFB7\u91CE\u5BB6", "A\uD842\uDFB7\u91CE\u5BB6", "A\uD842\uDFB7");
+ doTest(3, "B", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6B", "\uD842\uDFB7B");
+ doTest(3, "CD", 1, "\uD842\uDFB7\u91CE\u5BB6", "C\uD842\uDFB7\u91CE\u5BB6D", "CD");
+
+ // maxlength="4"
+ doTest(4, "EF", 1, "\uD842\uDFB7\u91CE\u5BB6", "E\uD842\uDFB7\u91CE\u5BB6F", "E\uD842\uDFB7F");
+ doTest(4, "GHI", 1, "\uD842\uDFB7\u91CE\u5BB6", "G\uD842\uDFB7\u91CE\u5BB6HI", "GHI");
+
+ // maxlength="1", inputting only high surrogate
+ doTest(1, "", 0, "\uD842", "\uD842", "\uD842", "even if input string is only a high surrogate");
+
+ // maxlength="1", inputting only low surrogate
+ doTest(1, "", 0, "\uDFB7", "\uDFB7", "\uDFB7", "even if input string is only a low surrogate");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTests);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1053048.html b/editor/libeditor/tests/test_bug1053048.html
new file mode 100644
index 0000000000..4f9df5e602
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1053048.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1053048
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1053048</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+
+ /** Test for Bug 1053048 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(runTests);
+
+ const nsISelectionListener = SpecialPowers.Ci.nsISelectionListener;
+
+ async function runTests() {
+ var textarea = SpecialPowers.wrap(document.getElementById("textarea"));
+ textarea.focus();
+
+ var editor = textarea.editor;
+ var selectionPrivate = editor.selection;
+
+ // Move caret to the end of the textarea
+ synthesizeMouse(textarea, 290, 10, {});
+ is(textarea.selectionStart, 3, "selectionStart should be 3 (after \"foo\")");
+ is(textarea.selectionEnd, 3, "selectionEnd should be 3 (after \"foo\")");
+
+ // This test **was** trying to check whether a selection listener which
+ // runs while an editor handles an edit action does not stop handling it.
+ // However, this selection listener caught previous selection change
+ // notification immediately before synthesizing the `Enter` key press
+ // unexpectedly. And now, selection listener may not run immediately after
+ // synthesizing the key press. So, we don't need to check whether a
+ // notification actually comes here.
+ let selectionListener = {
+ notifySelectionChanged(aDocument, aSelection, aReason, aAmount) {
+ ok(true, "selectionStart: " + textarea.selectionStart);
+ ok(true, "selectionEnd: " + textarea.selectionEnd);
+ },
+ };
+ selectionPrivate.addSelectionListener(selectionListener);
+ synthesizeKey("KEY_Enter");
+ is(textarea.selectionStart, 4, "selectionStart should be 4");
+ is(textarea.selectionEnd, 4, "selectionEnd should be 4");
+ is(textarea.value, "foo\n", "The line break should be appended");
+ selectionPrivate.removeSelectionListener(selectionListener);
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1053048">Mozilla Bug 1053048</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+
+<textarea id="textarea"
+ style="height: 100px; width: 300px; -moz-appearance: none"
+ spellcheck="false"
+ onkeydown="this.style.display='block'; this.style.height='200px';">foo</textarea>
+
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1068979.html b/editor/libeditor/tests/test_bug1068979.html
new file mode 100644
index 0000000000..189be35f91
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1068979.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1068979
+-->
+<head>
+ <title>Test for Bug 1068979</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1068979">Mozilla Bug 1068979</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor1" contenteditable="true">&#x1d400;</div>
+ <div id="editor2" contenteditable="true">a<u>&#x1d401;</u>b</div>
+ <div id="editor3" contenteditable="true">a&#x1d402;<u>b</u></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1068979 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ // Test backspacing over SMP characters pasted-in to a contentEditable
+ getSelection().selectAllChildren(document.getElementById("editor1"));
+ var ed1 = document.getElementById("editor1");
+ var ch1 = ed1.textContent;
+ ed1.focus();
+ synthesizeKey("C", {accelKey: true});
+ synthesizeKey("V", {accelKey: true});
+ synthesizeKey("V", {accelKey: true});
+ synthesizeKey("V", {accelKey: true});
+ synthesizeKey("V", {accelKey: true});
+ is(ed1.textContent, ch1 + ch1 + ch1 + ch1, "Should have four SMP characters");
+ sendKey("back_space");
+ is(ed1.textContent, ch1 + ch1 + ch1, "Three complete characters should remain");
+ sendKey("back_space");
+ is(ed1.textContent, ch1 + ch1, "Two complete characters should remain");
+ sendKey("back_space");
+ is(ed1.textContent, ch1, "Only one complete SMP character should remain");
+ ed1.blur();
+
+ // Test backspacing across an SMP character in a sub-element
+ getSelection().selectAllChildren(document.getElementById("editor2"));
+ var ed2 = document.getElementById("editor2");
+ ed2.focus();
+ sendKey("right");
+ sendKey("back_space");
+ sendKey("back_space");
+ is(ed2.textContent, "a", "Only the 'a' should remain");
+ ed2.blur();
+
+ // Test backspacing across an SMP character from a following sub-element
+ getSelection().selectAllChildren(document.getElementById("editor3"));
+ var ed3 = document.getElementById("editor3");
+ ed3.focus();
+ sendKey("right");
+ sendKey("left");
+ sendKey("back_space");
+ is(ed3.textContent, "ab", "The letters 'ab' should remain");
+ ed3.blur();
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1094000.html b/editor/libeditor/tests/test_bug1094000.html
new file mode 100644
index 0000000000..f41ebeeedb
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1094000.html
@@ -0,0 +1,147 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1094000
+-->
+<head>
+ <title>Test for Bug 1094000</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1094000">Mozilla Bug 1094000</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor0" contenteditable></div>
+ <div id="editor1" contenteditable><br></div>
+ <div id="editor2" contenteditable>a</div>
+ <div id="editor3" contenteditable>b</div>
+ <div id="editor4" contenteditable>c</div>
+ <div id="editor5" contenteditable>d</div>
+ <div id="editor6" contenteditable>e</div>
+ <div id="editor7" contenteditable><span>f</span></div>
+ <div id="editor8" contenteditable><span>g</span></div>
+ <div id="editor9" contenteditable><span>h</span></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1094000 **/
+
+SimpleTest.waitForExplicitFinish();
+
+const kIsLinux = navigator.platform.indexOf("Linux") == 0;
+
+function runTests() {
+ var editor0 = document.getElementById("editor0");
+ var editor1 = document.getElementById("editor1");
+ var editor2 = document.getElementById("editor2");
+ var editor3 = document.getElementById("editor3");
+ var editor4 = document.getElementById("editor4");
+ var editor5 = document.getElementById("editor5");
+ var editor6 = document.getElementById("editor6");
+ var editor7 = document.getElementById("editor7");
+ var editor8 = document.getElementById("editor8");
+ var editor9 = document.getElementById("editor9");
+
+ ok(Math.abs(editor1.getBoundingClientRect().height - editor0.getBoundingClientRect().height) <= 1,
+ "an editor having a <br> element and an empty editor should be same height");
+ ok(Math.abs(editor1.getBoundingClientRect().height - editor2.getBoundingClientRect().height) <= 1,
+ "an editor having only a <br> element and an editor having \"a\" should be same height");
+
+ editor2.focus();
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_Backspace");
+ is(editor2.innerHTML, "<br>",
+ "an editor which had \"a\" should have only <br> element after Backspace keypress");
+ ok(Math.abs(editor2.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was removed by Backspace key should have a place to put a caret");
+
+ editor3.focus();
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_Delete");
+ is(editor3.innerHTML, "<br>",
+ "an editor which had \"b\" should have only <br> element after Delete keypress");
+ ok(Math.abs(editor3.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was removed by Delete key should have a place to put a caret");
+
+ editor4.focus();
+ window.getSelection().selectAllChildren(editor4);
+ synthesizeKey("KEY_Backspace");
+ is(editor4.innerHTML, "<br>",
+ "an editor which had \"c\" should have only <br> element after removing selected text with Backspace key");
+ ok(Math.abs(editor4.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was selected and removed by Backspace key should have a place to put a caret");
+
+ editor5.focus();
+ window.getSelection().selectAllChildren(editor5);
+ synthesizeKey("KEY_Delete");
+ is(editor5.innerHTML, "<br>",
+ "an editor which had \"d\" should have only <br> element after removing selected text with Delete key");
+ ok(Math.abs(editor5.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was selected and removed by Delete key should have a place to put a caret");
+
+ editor6.focus();
+ window.getSelection().selectAllChildren(editor6);
+ synthesizeKey("x", {accelKey: true});
+ is(editor6.innerHTML, "<br>",
+ "an editor which had \"e\" should have only <br> element after removing selected text by \"Cut\"");
+ ok(Math.abs(editor6.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was selected and removed by \"Cut\" should have a place to put a caret");
+
+ editor7.focus();
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_Backspace");
+ is(
+ editor7.innerHTML,
+ SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible")
+ ? "<br>"
+ : "<span><br></span>",
+ "an editor which had \"f\" in a <span> element should have only <br> element after Backspace keypress"
+ );
+ ok(Math.abs(editor7.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was removed by Backspace key should have a place to put a caret");
+
+ editor8.focus();
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_Delete");
+ todo_is(editor8.innerHTML, "<br>",
+ "an editor which had \"g\" in a <span> element should have only <br> element after Delete keypress");
+ todo_isnot(
+ editor8.innerHTML,
+ SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible")
+ ? "<br><span></span>"
+ : "<span><br></span>",
+ "an editor which had \"g\" in a <span> element should have only <br> element after Delete keypress"
+ );
+ ok(Math.abs(editor8.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was removed by Backspace key should have a place to put a caret");
+
+ editor9.focus();
+ window.getSelection().selectAllChildren(editor9.querySelector("span"));
+ synthesizeKey("KEY_Backspace");
+ todo_is(editor9.innerHTML, "<br>",
+ "an editor which had \"h\" in a <span> element should have only <br> element after removing selected text with Backspace key");
+ todo_isnot(editor9.innerHTML, "<span><br></span>",
+ "an editor which had \"h\" in a <span> element should have only <br> element after removing selected text with Backspace key");
+ ok(Math.abs(editor9.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1,
+ "an editor whose content was removed by Backspace key should have a place to put a caret");
+
+ editor0.focus();
+ synthesizeKey("KEY_Backspace");
+ is(editor0.innerHTML, "",
+ "an empty editor should keep being empty even if Backspace key is pressed");
+ synthesizeKey("KEY_Delete");
+ is(editor0.innerHTML, "",
+ "an empty editor should keep being empty even if Delete key is pressed");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTests);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1102906.html b/editor/libeditor/tests/test_bug1102906.html
new file mode 100644
index 0000000000..26b0592637
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1102906.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1102906
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1102906</title>
+
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+
+ <script>
+ "use strict";
+
+ /* Test for Bug 1102906 */
+ /* The caret should be movable by using keyboard after drag-and-drop. */
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus( () => {
+ let content = document.getElementById("content");
+ let drag = document.getElementById("drag");
+ let selection = window.getSelection();
+
+ /* Perform drag-and-drop for an arbitrary content. The caret should be at
+ the end of the contenteditable. */
+ selection.selectAllChildren(drag);
+ synthesizeDrop(drag, content, {}, "copy");
+
+ let textContentAfterDrop = content.textContent;
+
+ /* Move the caret to the front of the contenteditable by using keyboard. */
+ for (let i = 0; i < content.textContent.length; ++i) {
+ sendKey("LEFT");
+ }
+ sendChar("!");
+
+ is(content.textContent, "!" + textContentAfterDrop,
+ "The exclamation mark should be inserted at the front.");
+
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1102906">Mozilla Bug 1102906</a>
+<div id="content" contenteditable="true"><span id="drag">Drag</span></div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1109465.html b/editor/libeditor/tests/test_bug1109465.html
new file mode 100644
index 0000000000..97bc6a71e9
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1109465.html
@@ -0,0 +1,65 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1109465
+-->
+<head>
+ <title>Test for Bug 1109465</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <textarea></textarea>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 1109465 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+
+ // Type foo\nbar and place the caret at the end of the last line
+ sendString("foo");
+ synthesizeKey("KEY_Enter");
+ sendString("bar");
+ synthesizeKey("KEY_ArrowUp");
+ is(t.selectionStart, 3, "Correct start of selection");
+ is(t.selectionEnd, 3, "Correct end of selection");
+
+ // Compose an IME string
+ var composingString = "\u306B";
+ // FYI: "compositionstart" will be dispatched automatically.
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": composingString,
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": 1, "length": 0 },
+ });
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(t.value, "foo\u306B\nbar", "Correct value after composition");
+
+ // Now undo to test that the transaction merger has correctly detected the
+ // IMETextTxn.
+ synthesizeKey("Z", {accelKey: true});
+ is(t.value, "foo\nbar", "Correct value after undo");
+
+ SimpleTest.finish();
+});
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1130651.html b/editor/libeditor/tests/test_bug1130651.html
new file mode 100644
index 0000000000..a9e1bad8c2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1130651.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Test for Bug 1130651</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1332876">Mozilla Bug 1332876</a>
+<div contenteditable>a b</div>
+<script>
+var div = document.querySelector("div");
+div.focus();
+getSelection().collapse(div.firstChild, 2);
+try {
+ document.execCommand("inserttext", false, "\n");
+ ok(true, "No exception thrown");
+} catch (e) {
+ ok(false, "Exception: " + e);
+}
+</script>
diff --git a/editor/libeditor/tests/test_bug1140105.html b/editor/libeditor/tests/test_bug1140105.html
new file mode 100644
index 0000000000..9096f4f99c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1140105.html
@@ -0,0 +1,64 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1140105
+-->
+<head>
+ <title>Test for Bug 1140105</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="display">
+</div>
+
+<div id="content" contenteditable><font face="Arial">1234567890</font></div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 1140105 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("content");
+ div.focus();
+ synthesizeMouseAtCenter(div, {});
+ synthesizeKey("KEY_ArrowLeft");
+
+ var sel = window.getSelection();
+ var selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be in text node");
+ is(selRange.endOffset, 9, "offset should be 9");
+
+ var firstHas = {};
+ var anyHas = {};
+ var allHas = {};
+ var editor = getEditor();
+
+ editor.getInlinePropertyWithAttrValue("font", "face", "Arial", firstHas, anyHas, allHas);
+ is(firstHas.value, true, "Test for Arial: firstHas: true expected");
+ is(anyHas.value, true, "Test for Arial: anyHas: true expected");
+ is(allHas.value, true, "Test for Arial: allHas: true expected");
+ editor.getInlinePropertyWithAttrValue("font", "face", "Courier", firstHas, anyHas, allHas);
+ is(firstHas.value, false, "Test for Courier: firstHas: false expected");
+ is(anyHas.value, false, "Test for Courier: anyHas: false expected");
+ is(allHas.value, false, "Test for Courier: allHas: false expected");
+
+ SimpleTest.finish();
+});
+
+function getEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ var editor = editingSession.getEditorForWindow(window);
+ editor.QueryInterface(Ci.nsIHTMLEditor);
+ return editor;
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1140617.html b/editor/libeditor/tests/test_bug1140617.html
new file mode 100644
index 0000000000..e8a9923489
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1140617.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<title>Mozilla Bug 1140617</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1140617"
+ target="_blank">Mozilla Bug 1140617</a>
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe>
+<img id="i" src="green.png">
+<script>
+function runTest() {
+ SpecialPowers.setCommandNode(window, document.getElementById("i"));
+ SpecialPowers.doCommand(window, "cmd_copyImageContents");
+
+ var e = document.getElementById("i1");
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(doc.body);
+ selection.collapseToEnd();
+
+ doc.execCommand("fontname", false, "Arial");
+ doc.execCommand("bold", false, null);
+ doc.execCommand("insertText", false, "12345");
+ doc.execCommand("paste", false, null);
+ doc.execCommand("insertText", false, "a");
+
+ is(doc.queryCommandValue("fontname"), "Arial", "Arial expected");
+ is(doc.queryCommandState("bold"), true, "Bold expected");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
diff --git a/editor/libeditor/tests/test_bug1151186.html b/editor/libeditor/tests/test_bug1151186.html
new file mode 100644
index 0000000000..049287a135
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1151186.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1151186</title>
+ <link rel=stylesheet href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script>
+ /** Test for Bug 1151186 **/
+ SimpleTest.waitForExplicitFinish();
+
+ // In this test, we want to check the IME enabled state in the `contenteditable`
+ // editor which is focused by `focus` event listener of the document.
+ // However, according to the random oranges filed as bug 1176038 and bug 1611360,
+ // `focus` event are sometimes not fired on the document and the reason is,
+ // the document sometimes not focused automatically. Therefore, this test
+ // will set focus the document when `focus` event is not fired until next
+ // macro task.
+ var focusEventFired = false;
+ function onFocus(event) {
+ is(event.target.nodeName, "#document", "focus event should be fired on the document node");
+ if (event.target != document) {
+ return;
+ }
+ focusEventFired = true;
+ document.getElementById("editor").focus();
+ SimpleTest.executeSoon(runTests);
+ }
+ document.addEventListener("focus", onFocus, {once: true});
+
+ // Register next macro task to check whether `focus` event of the document
+ // is fired as expected. If not, let's focus our window manually. Then,
+ // the `focus` event listener starts the test anyway.
+ setTimeout(() => {
+ if (focusEventFired) {
+ return; // We've gotten `focus` event as expected.
+ }
+ ok(!document.hasFocus(), "The document should not have focus yet");
+ info("Setting focus to the window forcibly...");
+ window.focus();
+ }, 0);
+
+ function runTests() {
+ let description = focusEventFired ?
+ "document got focused normally" :
+ "document got focused forcibly";
+ is(document.activeElement, document.getElementById("editor"),
+ `The div element should be focused (${description})`);
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ is(utils.IMEStatus, utils.IME_STATUS_ENABLED,
+ `IME should be enabled (${description})`);
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151186">Mozilla Bug 1151186</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<div id="editor" contenteditable="true"></div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1153237.html b/editor/libeditor/tests/test_bug1153237.html
new file mode 100644
index 0000000000..ea1863a059
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1153237.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1153237
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1153237</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Avoid platform selection differences
+ SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({
+ "set": [["layout.word_select.eat_space_to_next_word", true]],
+ }, runTests);
+ });
+
+ function runTests() {
+ var element = document.getElementById("editor");
+ var sel = window.getSelection();
+
+ element.focus();
+ is(sel.getRangeAt(0).startOffset, 0, "offset is zero");
+
+ SpecialPowers.doCommand(window, "cmd_selectRight2");
+ is(sel.toString(), "Some ",
+ "first word + space is selected: got '" + sel.toString() + "'");
+
+ SpecialPowers.doCommand(window, "cmd_selectRight2");
+ is(sel.toString(), "Some text",
+ "both words are selected: got '" + sel.toString() + "'");
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1153237">Mozilla Bug 1153237</a>
+<div id="editor" contenteditable>Some text</div><span></span>
+
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1162952.html b/editor/libeditor/tests/test_bug1162952.html
new file mode 100644
index 0000000000..0b8287f157
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1162952.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1162952
+-->
+<head>
+ <title>Test for Bug 1162952</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1162952">Mozilla Bug 1162952</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1162952 **/
+var userCallbackRun = false;
+
+document.addEventListener("keydown", function() {
+ // During a user callback, the commands should be enabled
+ userCallbackRun = true;
+ is(true, document.queryCommandEnabled("cut"));
+ is(true, document.queryCommandEnabled("copy"));
+});
+
+// Otherwise, they should be disabled
+is(false, document.queryCommandEnabled("cut"));
+is(false, document.queryCommandEnabled("copy"));
+
+// Fire a user callback
+sendString("A");
+
+ok(userCallbackRun, "User callback should've been run");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1181130-1.html b/editor/libeditor/tests/test_bug1181130-1.html
new file mode 100644
index 0000000000..0b9710a3b2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1181130-1.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1181130
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1181130</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181130">Mozilla Bug 1181130</a>
+<p id="display"></p>
+<div id="container" contenteditable="true">
+ editable div
+ <div id="noneditable" contenteditable="false">
+ non-editable div
+ <div id="editable" contenteditable="true">nested editable div</div>
+ </div>
+</div>
+<script type="application/javascript">
+/** Test for Bug 1181130 **/
+var container = document.getElementById("container");
+var noneditable = document.getElementById("noneditable");
+var editable = document.getElementById("editable");
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ synthesizeMouseAtCenter(noneditable, {});
+ ok(!document.getSelection().toString().includes("nested editable div"),
+ "Selection should not include non-editable content");
+
+ synthesizeMouseAtCenter(container, {});
+ ok(!document.getSelection().toString().includes("nested editable div"),
+ "Selection should not include non-editable content");
+
+ synthesizeMouseAtCenter(editable, {});
+ ok(!document.getSelection().toString().includes("nested editable div"),
+ "Selection should not include non-editable content");
+
+ SimpleTest.finish();
+});
+</script>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1181130-2.html b/editor/libeditor/tests/test_bug1181130-2.html
new file mode 100644
index 0000000000..fa091b71a0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1181130-2.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1181130
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1181130</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181130">Mozilla Bug 1181130</a>
+<p id="display"></p>
+<div id="container" contenteditable="true">
+ editable div
+ <div id="noneditable" contenteditable="false">
+ non-editable div
+ </div>
+</div>
+<script type="application/javascript">
+/** Test for Bug 1181130 **/
+var container = document.getElementById("container");
+var noneditable = document.getElementById("noneditable");
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ var nonHTMLElement = document.createElementNS("http://www.example.com", "element");
+ nonHTMLElement.innerHTML = '<div contenteditable="true">nested editable div</div>';
+ noneditable.appendChild(nonHTMLElement);
+
+ synthesizeMouseAtCenter(noneditable, {});
+ ok(!document.getSelection().toString().includes("nested editable div"),
+ "Selection should not include non-editable content");
+
+ SimpleTest.finish();
+});
+</script>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1186799.html b/editor/libeditor/tests/test_bug1186799.html
new file mode 100644
index 0000000000..f5f14eb9c0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1186799.html
@@ -0,0 +1,78 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1186799
+-->
+<head>
+ <title>Test for Bug 1186799</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="content">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1186799">Mozilla Bug 1186799</a>
+<p id="display"></p>
+<div id="content">
+ <span id="span">span</span>
+ <div id="editor" contenteditable></div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1186799 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var span = document.getElementById("span");
+ var editor = document.getElementById("editor");
+ editor.focus();
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": 1, "length": 0 },
+ });
+
+ ok(isThereIMESelection(), "There should be IME selection");
+
+ var compositionEnd = false;
+ editor.addEventListener("compositionend", function() { compositionEnd = true; }, true);
+
+ synthesizeMouseAtCenter(span, {});
+
+ ok(compositionEnd, "composition end should be fired at clicking outside of the editor");
+ ok(!isThereIMESelection(), "There should be no IME selection");
+
+ SimpleTest.finish();
+});
+
+function isThereIMESelection() {
+ var selCon = SpecialPowers.wrap(window).
+ docShell.
+ editingSession.
+ getEditorForWindow(window).
+ selectionController;
+ const kIMESelections = [
+ SpecialPowers.Ci.nsISelectionController.SELECTION_IME_RAWINPUT,
+ SpecialPowers.Ci.nsISelectionController.SELECTION_IME_SELECTEDRAWTEXT,
+ SpecialPowers.Ci.nsISelectionController.SELECTION_IME_CONVERTEDTEXT,
+ SpecialPowers.Ci.nsISelectionController.SELECTION_IME_SELECTEDCONVERTEDTEXT,
+ ];
+ for (var i = 0; i < kIMESelections.length; i++) {
+ var sel = selCon.getSelection(kIMESelections[i]);
+ if (sel && sel.rangeCount) {
+ return true;
+ }
+ }
+ return false;
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1230473.html b/editor/libeditor/tests/test_bug1230473.html
new file mode 100644
index 0000000000..7d1ec43bb0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1230473.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1230473
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1230473</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1230473">Mozilla Bug 1230473</a>
+<input id="input">
+<textarea id="textarea"></textarea>
+<div id="div" contenteditable></div>
+<script type="application/javascript">
+/** Test for Bug 1230473 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ function runTest(aEditor) {
+ function committer() {
+ aEditor.blur();
+ aEditor.focus();
+ }
+ function isNSEditableElement() {
+ return aEditor.tagName.toLowerCase() == "input" || aEditor.tagName.toLowerCase() == "textarea";
+ }
+ function value() {
+ return isNSEditableElement() ? aEditor.value : aEditor.textContent;
+ }
+ function isComposing() {
+ return isNSEditableElement() ? SpecialPowers.wrap(aEditor)
+ .editor
+ .composing :
+ SpecialPowers.wrap(window)
+ .docShell
+ .editor
+ .composing;
+ }
+ function clear() {
+ if (isNSEditableElement()) {
+ aEditor.value = "";
+ } else {
+ aEditor.textContent = "";
+ }
+ }
+
+ clear();
+
+ // FYI: Chrome commits composition if blur() and focus() are called during
+ // composition. But note that if they are called by compositionupdate
+ // listener, the behavior is unstable. On Windows, composition is
+ // canceled. On Linux and macOS, the composition is committed
+ // internally but the string keeps underlined. If they are called
+ // by input event listener, committed on any platforms though.
+ // On the other hand, Edge and Safari keeps composition even with
+ // calling both blur() and focus().
+
+ // Committing at compositionstart
+ aEditor.focus();
+ aEditor.addEventListener("compositionstart", committer, true);
+ synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 1, length: 0 }, key: { key: "a" }});
+ aEditor.removeEventListener("compositionstart", committer, true);
+ ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionstart event handler");
+ is(value(), "", "composition in " + aEditor.id + " shouldn't insert any text since it's committed at compositionstart");
+ clear();
+
+ // Committing at first compositionupdate
+ aEditor.focus();
+ aEditor.addEventListener("compositionupdate", committer, true);
+ synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 1, length: 0 }, key: { key: "a" }});
+ aEditor.removeEventListener("compositionupdate", committer, true);
+ ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionupdate event handler");
+ is(value(), "a", "composition in " + aEditor.id + " should have \"a\" since IME committed with it");
+ clear();
+
+ // Committing at second compositionupdate
+ aEditor.focus();
+ // FYI: "compositionstart" will be dispatched automatically.
+ synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 1, length: 0 }, key: { key: "a" }});
+ ok(isComposing(), "composition should be in " + aEditor.id + " before dispatching second compositionupdate");
+ is(value(), "a", "composition in " + aEditor.id + " should be 'a' before dispatching second compositionupdate");
+ aEditor.addEventListener("compositionupdate", committer, true);
+ synthesizeCompositionChange({ composition: { string: "ab", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }] },
+ caret: { start: 2, length: 0 }, key: { key: "b" }});
+ aEditor.removeEventListener("compositionupdate", committer, true);
+ ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionupdate event handler");
+ is(value(), "ab", "composition in " + aEditor.id + " should have \"ab\" since IME committed with it");
+ clear();
+ }
+ runTest(document.getElementById("input"));
+ runTest(document.getElementById("textarea"));
+ runTest(document.getElementById("div"));
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1247483.html b/editor/libeditor/tests/test_bug1247483.html
new file mode 100644
index 0000000000..c8b45b4439
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1247483.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 1247483</title>
+<style src="/tests/SimpleTest/test.css" type="text/css"></style>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+function runTest() {
+ // Copy content from table.
+ var selection = getSelection();
+ var startRange = document.createRange();
+ startRange.setStart(document.getElementById("start"), 0);
+ startRange.setEnd(document.getElementById("end"), 2);
+ selection.removeAllRanges();
+ selection.addRange(startRange);
+ SpecialPowers.wrap(document).execCommand("copy", false, null);
+
+ // Paste content into "pastecontainer"
+ var pasteContainer = document.getElementById("pastecontainer");
+ var pasteRange = document.createRange();
+ pasteRange.selectNodeContents(pasteContainer);
+ pasteRange.collapse(false);
+ selection.removeAllRanges();
+ selection.addRange(pasteRange);
+ SpecialPowers.wrap(document).execCommand("paste", false, null);
+
+ is(pasteContainer.querySelectorAll("td").length, 4, "4 <td> should be pasted.");
+
+ document.execCommand("undo", false, null);
+
+ is(pasteContainer.querySelectorAll("td").length, 0, "Undo should have remove the 4 pasted <td>.");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1247483">Mozilla Bug 1247483</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<div id="container" contenteditable="true">
+<table>
+ <tr id="start"><td>1 1</td><td>1 2</td></tr>
+ <tr id="end"><td>2 1</td><td>2 2</td></tr>
+</table>
+</div>
+
+<div id="pastecontainer" contenteditable="true">
+</div>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1248128.html b/editor/libeditor/tests/test_bug1248128.html
new file mode 100644
index 0000000000..30c39bcff8
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1248128.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248128
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248128</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.waitForFocus(function() {
+ var outer = document.querySelector("html");
+ ok(outer.scrollTop == 0, "scrollTop is zero: got " + outer.scrollTop);
+
+ var input = document.getElementById("testInput");
+ input.focus();
+
+ var scroll = outer.scrollTop;
+ ok(scroll > 0, "element has scrolled: new value " + scroll);
+
+ try {
+ SpecialPowers.doCommand(window, "cmd_moveLeft");
+ ok(false, "should not be able to do kMoveLeft");
+ } catch (e) {
+ ok(true, "unable to perform kMoveLeft");
+ }
+
+ ok(outer.scrollTop == scroll,
+ "scroll is unchanged: got " + outer.scrollTop + ", expected " + scroll);
+
+ // Make sure cmd_moveLeft isn't failing for some unrelated reason
+ sendString("a");
+ is(input.selectionStart, 1, "selectionStart after typing");
+ SpecialPowers.doCommand(window, "cmd_moveLeft");
+ is(input.selectionStart, 0, "selectionStart after move left");
+
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248128">Mozilla Bug 1248128</a>
+<div style="height: 2000px;"></div>
+<input type="text" id="testInput"></input>
+<div style="height: 200px;"></div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1248185.html b/editor/libeditor/tests/test_bug1248185.html
new file mode 100644
index 0000000000..d18b62b025
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1248185.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248185
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248185</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Avoid platform selection differences
+ SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({
+ "set": [["layout.word_select.eat_space_to_next_word", true]],
+ }, runTests);
+ });
+
+ function runTests() {
+ var editor = document.querySelector("#test");
+ editor.focus();
+
+ var sel = window.getSelection();
+
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_selectRight2");
+ ok(sel.toString() == "three ", "expected 'three ' to be selected");
+
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ SpecialPowers.doCommand(window, "cmd_moveRight2");
+ ok(sel.toString() == "", "expected empty selection");
+
+ SpecialPowers.doCommand(window, "cmd_selectLeft2");
+ ok(sel.toString() == "five", "expected 'five' to be selected");
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248185">Mozilla Bug 1248185</a>
+<body>
+<div style="font: 12px monospace; width: 45ch;">
+<span contenteditable="" id="test">blablablablablablablablablablablablablabla one two three four five</span>
+<div>
+<span>foo</span>
+</div>
+</div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1248186.html b/editor/libeditor/tests/test_bug1248186.html
new file mode 100644
index 0000000000..f23e905b65
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1248186.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248186</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.waitForFocus(function () {
+ let editor0 = document.getElementById("editor0");
+ let editor1 = document.getElementById("editor1");
+ let editor2 = document.getElementById("editor2");
+ editor0.focus();
+ for (let i = 0; i < 5; i++) {
+ synthesizeKey("KEY_ArrowRight");
+ is(document.activeElement, editor0, "hitting right should not de-focus the editor");
+ }
+ editor1.focus();
+ for (let i = 0; i < 5; i++) {
+ synthesizeKey("KEY_ArrowRight");
+ is(document.activeElement, editor1, "hitting right should not de-focus the editor");
+ }
+ editor2.focus();
+ for (let i = 0; i < 8; i++) {
+ synthesizeKey("KEY_ArrowRight");
+ is(document.activeElement, editor2, "hitting right should not de-focus the editor");
+ }
+ // make sure we don't get stuck at the end of the "foo" span
+ let selection = getSelection();
+ is(selection.focusNode.parentElement.id, "bar");
+ is(selection.focusOffset, 3);
+
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<div>
+ <span id="editor0" contenteditable>foo</span>
+ <span></span>
+</div>
+<div>
+ <span id="editor1" contenteditable><b>foo</b></span>
+ <span></span>
+</div>
+<span id="editor2" contenteditable>
+ <span>foo</span><br>
+ <span id="bar">bar</span></span>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1250010.html b/editor/libeditor/tests/test_bug1250010.html
new file mode 100644
index 0000000000..4113ecdb29
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1250010.html
@@ -0,0 +1,87 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1250010
+-->
+<head>
+ <title>Test for Bug 1250010</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="display">
+</div>
+
+<div id="test1" contenteditable><p><b><font color="red">1234567890</font></b></p></div>
+<div id="test2" contenteditable><p><tt>xyz</tt></p><p><tt><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAIAAABvrngfAAAAFklEQVQImWMwjWhCQwxECoW3oCHihAB0LyYv5/oAHwAAAABJRU5ErkJggg=="></tt></p></div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+function getImageDataURI() {
+ return document.getElementsByTagName("img")[0].getAttribute("src");
+}
+
+/** Test for Bug 1250010 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ // First test: Empty paragraph is split correctly.
+ var div = document.getElementById("test1");
+ div.focus();
+ synthesizeMouseAtCenter(div, {});
+
+ var sel = window.getSelection();
+ var selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 10, "offset should be 10");
+
+ synthesizeKey("KEY_Enter");
+ synthesizeKey("KEY_Enter");
+ sendString("b");
+ synthesizeKey("KEY_ArrowUp");
+ sendString("a");
+
+ is(div.innerHTML, "<p><b><font color=\"red\">1234567890</font></b></p>" +
+ "<p><b><font color=\"red\">a<br></font></b></p>" +
+ "<p><b><font color=\"red\">b<br></font></b></p>",
+ "unexpected HTML");
+
+ // Second test: Since we modified the code path that splits non-text nodes,
+ // test that this works, if the split node is not empty.
+ div = document.getElementById("test2");
+ div.focus();
+ synthesizeMouseAtCenter(div, {});
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 3, "offset should be 3");
+
+ // Move behind the image and press enter, insert an "A".
+ // That should insert a new empty paragraph with the "A" after what we have.
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_Enter");
+ sendString("A");
+
+ // TODO: The <br> in the new paragraph and the paragraph containing the <img>
+ // should be removed at typing "A" because they are not necessary
+ // anymore to make the paragraphs visible (bug 503838).
+ const expectedHTML =
+ "<p><tt>xyz</tt></p><p><tt><img src=\"" + getImageDataURI() + "\"><br></tt></p>" +
+ "<p><tt>A<br></tt></p>";
+ is(
+ div.innerHTML,
+ expectedHTML,
+ "Pressing Enter after the <img> should create new paragraph and which contain <tt> and new text should be inserted in it"
+ );
+
+ SimpleTest.finish();
+});
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1257363.html b/editor/libeditor/tests/test_bug1257363.html
new file mode 100644
index 0000000000..c059633483
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1257363.html
@@ -0,0 +1,180 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1257363
+-->
+<head>
+ <title>Test for Bug 1257363</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="display">
+</div>
+
+<div id="backspaceCSS" contenteditable><p style="color:red;">12345</p>67</div>
+<div id="backspace" contenteditable><p><font color="red">12345</font></p>67</div>
+<div id="deleteCSS" contenteditable><p style="color:red;">x</p></div>
+<div id="delete" contenteditable><p><font color="red">y</font></p></div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 1257363 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ // ***** Backspace test *****
+ var div = document.getElementById("backspaceCSS");
+ div.focus();
+ synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+ var sel = window.getSelection();
+ var selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 5, "offset should be 5");
+
+ // Return and backspace should take us to where we started.
+ synthesizeKey("KEY_Enter");
+ synthesizeKey("KEY_Backspace");
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 5, "offset should be 5");
+
+ // Add an "a" to the end of the paragraph.
+ sendString("a");
+
+ // Return and forward delete should take us to the following line.
+ synthesizeKey("KEY_Enter");
+ synthesizeKey("KEY_Delete");
+
+ // Add a "b" to the start.
+ sendString("b");
+
+ is(div.innerHTML, "<p style=\"color:red;\">12345a</p>b67",
+ "unexpected HTML");
+
+ // Let's repeat the whole thing, but a font tag instead of CSS.
+ // The behaviour is different since the font is carried over.
+ div = document.getElementById("backspace");
+ div.focus();
+ synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+ sel = window.getSelection();
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 5, "offset should be 5");
+
+ // Return and backspace should take us to where we started.
+ synthesizeKey("KEY_Enter");
+ synthesizeKey("KEY_Backspace");
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 5, "offset should be 5");
+
+ // Add an "a" to the end of the paragraph.
+ sendString("a");
+
+ // Return and forward delete should take us to the following line.
+ synthesizeKey("KEY_Enter");
+ synthesizeKey("KEY_Delete");
+
+ // Add a "b" to the start.
+ sendString("b");
+
+ // Here we get a somewhat ugly result since the red sticks.
+ is(div.innerHTML, "<p><font color=\"red\">12345a</font></p><font color=\"#ff0000\">b</font>67",
+ "unexpected HTML");
+
+ // ***** Delete test *****
+ div = document.getElementById("deleteCSS");
+ div.focus();
+ synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+ sel = window.getSelection();
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 1, "offset should be 1");
+
+ // left, enter should create a new empty paragraph before
+ // but leave the selection at the start of the existing paragraph.
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_Enter");
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+ is(selRange.endOffset, 0, "offset should be 0");
+ is(selRange.endContainer.nodeValue, "x", "we should be in the text node with the x");
+
+ // Now moving up into the new empty paragraph.
+ synthesizeKey("KEY_ArrowUp");
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "P", "selection should be the new empty paragraph");
+ is(selRange.endOffset, 0, "offset should be 0");
+
+ // Forward delete should now take us to where we started.
+ synthesizeKey("KEY_Delete");
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+ is(selRange.endOffset, 0, "offset should be 0");
+
+ // Add an "a" to the start of the paragraph.
+ sendString("a");
+
+ is(div.innerHTML, "<p style=\"color:red;\">ax</p>",
+ "unexpected HTML");
+
+ // Let's repeat the whole thing, but a font tag instead of CSS.
+ div = document.getElementById("delete");
+ div.focus();
+ synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+ sel = window.getSelection();
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+ is(selRange.endOffset, 1, "offset should be 1");
+
+ // left, enter should create a new empty paragraph before
+ // but leave the selection at the start of the existing paragraph.
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_Enter");
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+ is(selRange.endOffset, 0, "offset should be 0");
+ is(selRange.endContainer.nodeValue, "y", "we should be in the text node with the y");
+
+ // Now moving up into the new empty paragraph.
+ synthesizeKey("KEY_ArrowUp");
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "FONT", "selection should be the font tag");
+ is(selRange.endOffset, 0, "offset should be 0");
+ is(selRange.endContainer.parentNode.nodeName, "P", "the parent of the font should be a paragraph");
+
+ // Forward delete should now take us to where we started.
+ synthesizeKey("KEY_Delete");
+
+ selRange = sel.getRangeAt(0);
+ is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+ is(selRange.endOffset, 0, "offset should be 0");
+
+ // Add an "a" to the start of the paragraph.
+ sendString("a");
+
+ is(div.innerHTML, "<p><font color=\"red\">ay</font></p>",
+ "unexpected HTML");
+
+ SimpleTest.finish();
+});
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug1258085.html b/editor/libeditor/tests/test_bug1258085.html
new file mode 100644
index 0000000000..4d8e1ef888
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1258085.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Test for Bug 1258085</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<div contenteditable></div>
+<script>
+var div = document.querySelector("div");
+
+function reset() {
+ div.innerHTML = "x<br> y";
+ div.focus();
+ synthesizeKey("KEY_ArrowDown");
+}
+
+function checks(msg) {
+ is(div.innerHTML, "x<br><br>",
+ msg + ": Should add a second <br> to prevent collapse of first");
+ is(div.childNodes.length, 3, msg + ": No empty text nodes allowed");
+ ok(getSelection().isCollapsed, msg + ": Selection must be collapsed");
+ is(getSelection().focusNode, div, msg + ": Focus must be in div");
+ is(getSelection().focusOffset, 2,
+ msg + ": Focus must be between the two <br>s");
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ // Put selection after the "y" and backspace
+ reset();
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_Backspace");
+ checks("Collapsed backspace");
+
+ // Now do the same with delete
+ reset();
+ synthesizeKey("KEY_Delete");
+ checks("Collapsed delete");
+
+ // Forward selection
+ reset();
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true});
+ synthesizeKey("KEY_Backspace");
+ checks("Forward-selected backspace");
+
+ // Backward selection
+ reset();
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ synthesizeKey("KEY_Backspace");
+ checks("Backward-selected backspace");
+
+ // Make sure we're not deleting if the whitespace isn't actually collapsed
+ div.style.whiteSpace = "pre-wrap";
+ reset();
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_Backspace");
+ is(div.innerHTML, "x<br> ", "pre-wrap: Don't delete uncollapsed space");
+ ok(getSelection().isCollapsed, "pre-wrap: Selection must be collapsed");
+ is(getSelection().focusNode, div.lastChild,
+ "pre-wrap: Focus must be in final text node");
+ is(getSelection().focusOffset, 1, "pre-wrap: Focus must be at end of node");
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug1268736.html b/editor/libeditor/tests/test_bug1268736.html
new file mode 100644
index 0000000000..697152b2fd
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1268736.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1268736
+-->
+<head>
+ <title>Test for Bug 1268736</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268736">Mozilla Bug 1268736</a>
+<table id="table" border="1" width="100%">
+ <tbody>
+ <tr>
+ <td>a</td>
+ <td>b</td>
+ <td>c</td>
+ </tr>
+ <tr>
+ <td>d</td>
+ <td id="cell_readonly">e</td>
+ <td contenteditable="true" id="cell_writable">f</td>
+ </tr>
+ </tbody>
+</table>
+
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1268736
+ *
+ * Tests for editing a table cell's contents when the table cell is or isn't a child of a contenteditable node.
+ *
+ */
+
+function getEditor() {
+ const Ci = SpecialPowers.Ci;
+ const editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+const table = document.getElementById("table");
+const tableHTML = table.innerHTML;
+const editor = getEditor();
+
+const readOnlyCell = document.getElementById("cell_readonly");
+readOnlyCell.focus();
+try {
+ editor.deleteTableCellContents();
+} catch (e) {}
+is(table.innerHTML == tableHTML, true, "editor should not modify non-editable table cell" );
+
+const editableCell = document.getElementById("cell_writable");
+editableCell.focus();
+editor.deleteTableCellContents();
+is(editableCell.innerHTML == "<br>", true, "editor can modify editable table cells" );
+
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1270235.html b/editor/libeditor/tests/test_bug1270235.html
new file mode 100644
index 0000000000..1499239ae1
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1270235.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1270235
+-->
+<head>
+ <title>Test for Bug 1270235</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1270235">Mozilla Bug 1270235</a>
+<p id="display"></p>
+<div id="content" style="display: none;"></div>
+
+<div id="edit1" contenteditable="true"><p>AB</p></div>
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let element = document.getElementById("edit1");
+ element.focus();
+ let textNode = element.firstChild.firstChild;
+ let node = textNode.splitText(0);
+ node.remove();
+
+ ok(!node.parentNode, "parent must be null");
+
+ let newRange = document.createRange();
+ newRange.setStart(node, 0);
+ newRange.setEnd(node, 0);
+ let selection = document.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(newRange);
+
+ ok(selection.isCollapsed, "isCollapsed must be true");
+
+ // Don't crash by user input
+ sendString("X");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1306532.html b/editor/libeditor/tests/test_bug1306532.html
new file mode 100644
index 0000000000..57c4ad6927
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1306532.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 1306532</title>
+<style src="/tests/SimpleTest/test.css" type="text/css"></style>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+function runTest() {
+ const headingone = document.getElementById("headingone");
+ const celltwo = document.getElementById("celltwo");
+ const pasteframe = document.getElementById("pasteframe");
+
+ // Copy content from table.
+ var selection = getSelection();
+ var startRange = document.createRange();
+ startRange.setStart(headingone, 0);
+ startRange.setEnd(celltwo, 0);
+ selection.removeAllRanges();
+ selection.addRange(startRange);
+ SpecialPowers.wrap(document).execCommand("copy", false, null);
+
+ // Paste content into "pasteframe"
+ var pasteContainer = pasteframe.contentDocument.body;
+ var pasteRange = pasteframe.contentDocument.createRange();
+ pasteRange.selectNodeContents(pasteContainer);
+ pasteRange.collapse(false);
+ selection.removeAllRanges();
+ selection.addRange(pasteRange);
+ SpecialPowers.wrap(pasteframe.contentDocument).execCommand("paste", false, null);
+
+ is(pasteContainer.querySelector("#headingone").textContent, "Month", "First heading should be 'Month'.");
+ is(pasteContainer.querySelector("#headingtwo").textContent, "Savings", "Second heading should be 'Savings'.");
+ is(pasteContainer.querySelector("#cellone").textContent, "January", "First cell should be 'January'.");
+ is(pasteContainer.querySelector("#celltwo").textContent, "$100", "Second cell should be '$100'.");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1306532">Mozilla Bug 1306532</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<div id="container">
+<table border="1">
+ <tr>
+ <th id="headingone">Month</th>
+ <th id="headingtwo">Savings</th>
+ </tr>
+ <tr>
+ <td id="cellone">January</td>
+ <td id="celltwo">$100</td>
+ </tr>
+</table>
+</div>
+
+<iframe onload="runTest();" id="pasteframe" srcdoc="<html><body contenteditable='true'>"></iframe>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1310912.html b/editor/libeditor/tests/test_bug1310912.html
new file mode 100644
index 0000000000..6c2036e4c0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1310912.html
@@ -0,0 +1,242 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1310912
+-->
+<html>
+<head>
+ <title>Test for Bug 1310912</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1310912">Mozilla Bug 1310912</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div contenteditable>ABC</div>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ const editor = document.querySelector("div[contenteditable]");
+
+ editor.focus();
+ const sel = window.getSelection();
+ sel.collapse(editor.childNodes[0], editor.textContent.length);
+
+ (function testInsertEmptyTextNodeWhenCaretIsAtEndOfComposition() {
+ const description =
+ "testInsertEmptyTextNodeWhenCaretIsAtEndOfComposition: ";
+ synthesizeCompositionChange({
+ composition: {
+ string: "DEF",
+ clauses: [
+ { length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ caret: { start: 3, length: 0 },
+ });
+ is(
+ editor.textContent,
+ "ABCDEF",
+ `${description} Composing text "DEF" should be inserted at end of the text node`
+ );
+
+ window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
+ is(
+ editor.childNodes[0].data,
+ "ABCDEF",
+ `${
+ description
+ } First text node should have both preceding text and the composing text`
+ );
+ is(
+ editor.childNodes[1].data,
+ "",
+ `${description} Second text node should be empty`
+ );
+ })();
+
+ (function testInsertEmptyTextNodeWhenCaretIsAtStartOfComposition() {
+ const description =
+ "testInsertEmptyTextNodeWhenCaretIsAtStartOfComposition: ";
+ synthesizeCompositionChange({
+ composition: {
+ string: "GHI",
+ clauses: [
+ { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ ],
+ },
+ caret: { start: 0, length: 0 },
+ });
+ is(
+ editor.textContent,
+ "ABCGHI",
+ `${description} Composing text should be replaced with new one`
+ );
+
+ window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
+ is(
+ editor.childNodes[0].data,
+ "ABC",
+ `${
+ description
+ } First text node should have only the preceding text of the composition`
+ );
+ is(
+ editor.childNodes[1].data,
+ "",
+ `${description} Second text node should have be empty`
+ );
+ is(
+ editor.childNodes[2].data,
+ "GHI",
+ `${description} Third text node should have only composing text`
+ );
+ })();
+
+ (function testInsertEmptyTextNodeWhenCaretIsAtStartOfCompositionAgain() {
+ const description =
+ "testInsertEmptyTextNodeWhenCaretIsAtStartOfCompositionAgain: ";
+ synthesizeCompositionChange({
+ composition: {
+ string: "JKL",
+ clauses: [
+ { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ ],
+ },
+ caret: { start: 0, length: 0 },
+ });
+ is(
+ editor.textContent,
+ "ABCJKL",
+ `${description} Composing text should be replaced`
+ );
+
+ window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
+ is(
+ editor.childNodes[0].data,
+ "ABC",
+ `${
+ description
+ } First text node should have only the preceding text of the composition`
+ );
+ is(
+ editor.childNodes[1].data,
+ "",
+ `${description} Second text node should have be empty`
+ );
+ is(
+ editor.childNodes[2].data,
+ "JKL",
+ `${description} Third text node should have only composing text`
+ );
+ })();
+
+ (function testInsertEmptyTextNodeWhenCaretIsAtMiddleOfComposition() {
+ const description =
+ "testInsertEmptyTextNodeWhenCaretIsAtMiddleOfComposition: ";
+ synthesizeCompositionChange({
+ composition: {
+ string: "MNO",
+ clauses: [
+ { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ ],
+ },
+ caret: { start: 1, length: 0 },
+ });
+ is(
+ editor.textContent,
+ "ABCMNO",
+ `${description} Composing text should be replaced`
+ );
+
+ // Normal selection is the caret, therefore, inserting empty text node
+ // creates the following DOM tree:
+ // <div contenteditable>
+ // |- #text ("ABCM")
+ // |- #text ("")
+ // +- #text ("NO")
+ window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
+ is(
+ editor.childNodes[0].data,
+ "ABCM",
+ `${
+ description
+ } First text node should have the preceding text and composing string before the split point`
+ );
+ is(
+ editor.childNodes[1].data,
+ "",
+ `${description} Second text node should be empty`
+ );
+ is(
+ editor.childNodes[2].data,
+ "NO",
+ `${
+ description
+ } Third text node should have the remaining composing string`
+ );
+ todo_is(editor.childNodes[3].nodeName, "BR",
+ "Forth node is empty text node, but I don't where this comes from");
+ })();
+
+ // Then, committing composition makes the commit string into the first
+ // text node and makes the following text nodes empty.
+ // XXX I don't know whether the empty text nodes should be removed or not
+ // at this moment.
+ (function testCommitComposition() {
+ const description = "testCommitComposition: ";
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ editor.textContent,
+ "ABCMNO",
+ `${description} Composing text should be committed as-is`
+ );
+ is(
+ editor.childNodes[0].data,
+ "ABCMNO",
+ `${description} First text node should have the committed string`
+ );
+ })();
+
+ (function testUndoComposition() {
+ const description = "testUndoComposition: ";
+ synthesizeKey("Z", { accelKey: true });
+ is(
+ editor.textContent,
+ "ABC",
+ `${description} Text should be undone (commit string should've gone)`
+ );
+ is(
+ editor.childNodes[0].data,
+ "ABC",
+ `${description} First text node should have all text`
+ );
+ })();
+
+ (function testUndoAgain() {
+ const description = "testUndoAgain: ";
+ synthesizeKey("Z", { accelKey: true, shiftKey: true });
+ is(
+ editor.textContent,
+ "ABCMNO",
+ `${description} Text should be redone (commit string should've be back)`
+ );
+ is(
+ editor.childNodes[0].data,
+ "ABCMNO",
+ `${description} First text node should have all text`
+ );
+ })();
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1314790.html b/editor/libeditor/tests/test_bug1314790.html
new file mode 100644
index 0000000000..29c9e471a2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1314790.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1314790
+-->
+<html>
+<head>
+ <title>Test for Bug 1314790</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1314790">Mozilla Bug 1314790</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div contenteditable="true" id="contenteditable1"><p>pen pineapple</p></div>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let elm = document.getElementById("contenteditable1");
+ elm.focus();
+ window.getSelection().collapse(elm.childNodes[0], 0);
+
+ SpecialPowers.doCommand(window, "cmd_wordNext");
+ SpecialPowers.doCommand(window, "cmd_wordNext");
+
+ synthesizeKey("KEY_Enter");
+ sendString("apple pen");
+
+ is(elm.childNodes[0].textContent, "pen pineapple",
+ "'pen pineapple' is first elment");
+ is(elm.childNodes[1].textContent, "apple pen",
+ "'apple pen' is second elment");
+
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ is(elm.childNodes[0].textContent, "pen pineapple",
+ "'pen pineapple' is first elment");
+
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ is(elm.childNodes[0].textContent, "pen pineapple",
+ "'pen pineapple' is first elment");
+
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ is(elm.childNodes[0].textContent, "pen ", "'pen ' is first elment");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1315065.html b/editor/libeditor/tests/test_bug1315065.html
new file mode 100644
index 0000000000..c8e861c3a8
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1315065.html
@@ -0,0 +1,145 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1315065
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1315065</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315065">Mozilla Bug 1315065</a>
+<div contenteditable><p>abc<br></p></div>
+<script type="application/javascript">
+/** Test for Bug 1315065 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ var editor = document.getElementsByTagName("div")[0];
+ function initForBackspace(aSelectionCollapsedTo /* = 0 ~ 3 */) {
+ editor.innerHTML = "<p id='p'>abc<br></p>";
+ var p = document.getElementById("p");
+ // FYI: We cannot inserting empty text nodes as expected with
+ // Node.appendChild() nor Node.insertBefore(). Therefore, let's use
+ // Range.insertNode() like actual web apps.
+ var selection = window.getSelection();
+ selection.collapse(p, 1);
+ var range = selection.getRangeAt(0);
+ var emptyTextNode3 = document.createTextNode("");
+ range.insertNode(emptyTextNode3);
+ var emptyTextNode2 = document.createTextNode("");
+ range.insertNode(emptyTextNode2);
+ var emptyTextNode1 = document.createTextNode("");
+ range.insertNode(emptyTextNode1);
+ is(p.childNodes.length, 5, "Failed to initialize the editor");
+ is(p.childNodes.item(1), emptyTextNode1, "1st text node should be emptyTextNode1");
+ is(p.childNodes.item(2), emptyTextNode2, "2nd text node should be emptyTextNode2");
+ is(p.childNodes.item(3), emptyTextNode3, "3rd text node should be emptyTextNode3");
+ switch (aSelectionCollapsedTo) {
+ case 0:
+ selection.collapse(p.firstChild, 3); // next to 'c'
+ break;
+ case 1:
+ selection.collapse(emptyTextNode1, 0);
+ break;
+ case 2:
+ selection.collapse(emptyTextNode2, 0);
+ break;
+ case 3:
+ selection.collapse(emptyTextNode3, 0);
+ break;
+ default:
+ ok(false, "aSelectionCollapsedTo is illegal value");
+ }
+ }
+
+ for (let i = 0; i < 4; i++) {
+ const kDescription = i == 0 ? "Backspace from immediately after the last character" :
+ "Backspace from " + i + "th empty text node";
+ editor.focus();
+ initForBackspace(i);
+ synthesizeKey("KEY_Backspace");
+ let p = document.getElementById("p");
+ ok(p, kDescription + ": <p> element shouldn't be removed by Backspace key press");
+ is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Backspace key press");
+ // When Backspace key is pressed even in empty text nodes, Gecko should not remove empty text nodes for now
+ // because we should keep our traditional behavior (same as Edge) for backward compatibility as far as possible.
+ // In this case, Chromium removes all empty text nodes, but Edge doesn't remove any empty text nodes.
+ is(p.childNodes.length, 5, kDescription + ": <p> should have 5 children after pressing Backspace key");
+ is(p.childNodes.item(0).textContent, "ab", kDescription + ": 'c' should be removed by pressing Backspace key");
+ is(p.childNodes.item(1).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Backspace key");
+ is(p.childNodes.item(2).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Backspace key");
+ is(p.childNodes.item(3).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Backspace key");
+ editor.blur();
+ }
+
+ function initForDelete(aSelectionCollapsedTo /* = 0 ~ 3 */) {
+ editor.innerHTML = "<p id='p'>abc<br></p>";
+ var p = document.getElementById("p");
+ // FYI: We cannot inserting empty text nodes as expected with
+ // Node.appendChild() nor Node.insertBefore(). Therefore, let's use
+ // Range.insertNode() like actual web apps.
+ var selection = window.getSelection();
+ selection.collapse(p, 0);
+ var range = selection.getRangeAt(0);
+ var emptyTextNode1 = document.createTextNode("");
+ range.insertNode(emptyTextNode1);
+ var emptyTextNode2 = document.createTextNode("");
+ range.insertNode(emptyTextNode2);
+ var emptyTextNode3 = document.createTextNode("");
+ range.insertNode(emptyTextNode3);
+ is(p.childNodes.length, 5, "Failed to initialize the editor");
+ is(p.childNodes.item(0), emptyTextNode3, "1st text node should be emptyTextNode3");
+ is(p.childNodes.item(1), emptyTextNode2, "2nd text node should be emptyTextNode2");
+ is(p.childNodes.item(2), emptyTextNode1, "3rd text node should be emptyTextNode1");
+ switch (aSelectionCollapsedTo) {
+ case 0:
+ selection.collapse(p.childNodes.item(3), 0); // next to 'a'
+ break;
+ case 1:
+ selection.collapse(emptyTextNode1, 0);
+ break;
+ case 2:
+ selection.collapse(emptyTextNode2, 0);
+ break;
+ case 3:
+ selection.collapse(emptyTextNode3, 0);
+ break;
+ default:
+ ok(false, "aSelectionCollapsedTo is illegal value");
+ }
+ }
+
+ for (let i = 0; i < 4; i++) {
+ const kDescription = i == 0 ? "Delete from immediately before the first character" :
+ "Delete from " + i + "th empty text node";
+ editor.focus();
+ initForDelete(i);
+ synthesizeKey("KEY_Delete");
+ var p = document.getElementById("p");
+ ok(p, kDescription + ": <p> element shouldn't be removed by Delete key press");
+ is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Delete key press");
+ if (i == 0) {
+ // If Delete key is pressed in non-empty text node, only the text node should be modified.
+ // This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case.
+ is(p.childNodes.length, 5, kDescription + ": <p> should have only 2 children after pressing Delete key (empty text nodes should be removed");
+ is(p.childNodes.item(0).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Delete key");
+ is(p.childNodes.item(1).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Delete key");
+ is(p.childNodes.item(2).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Delete key");
+ is(p.childNodes.item(3).textContent, "bc", kDescription + ": 'a' should be removed by pressing Delete key");
+ } else {
+ // If Delete key is pressed in an empty text node, it and following empty text nodes should be removed and the non-empty text node should be modified.
+ // This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case.
+ var expectedEmptyTextNodes = 3 - i;
+ is(p.childNodes.length, expectedEmptyTextNodes + 2, kDescription + ": <p> should have only " + i + " children after pressing Delete key (" + i + " empty text nodes should be removed");
+ is(p.childNodes.item(expectedEmptyTextNodes).textContent, "bc", kDescription + ": empty text nodes and 'a' should be removed by pressing Delete key");
+ }
+ editor.blur();
+ }
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1316302.html b/editor/libeditor/tests/test_bug1316302.html
new file mode 100644
index 0000000000..f8f7299330
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1316302.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1316302
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1316302</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1316302">Mozilla Bug 1316302</a>
+<div contenteditable>
+<blockquote><p>abc</p></blockquote>
+</div>
+<script type="application/javascript">
+/** Test for Bug 1316302 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ var editor = document.getElementsByTagName("div")[0];
+ var blockquote = document.getElementsByTagName("blockquote")[0];
+ var selection = window.getSelection();
+
+ editor.focus();
+
+ // Try to remove the last character from the end of the <blockquote>
+ selection.collapse(blockquote, blockquote.childNodes.length);
+ var range = selection.getRangeAt(0);
+ ok(range.collapsed, "range should be collapsed at the end of <blockquote>");
+ is(range.startContainer, blockquote, "range should be collapsed in the <blockquote>");
+ is(range.startOffset, blockquote.childNodes.length, "range should be collapsed at the end");
+ synthesizeKey("KEY_Backspace");
+ is(blockquote.innerHTML, "<p>ab</p>", "Pressing Backspace key at the end of <blockquote> should remove the last character in the <p>");
+
+ // Try to remove the first character from the start of the <blockquote>
+ selection.collapse(blockquote, 0);
+ range = selection.getRangeAt(0);
+ ok(range.collapsed, "range should be collapsed at the start of <blockquote>");
+ is(range.startContainer, blockquote, "range should be collapsed in the <blockquote>");
+ is(range.startOffset, 0, "range should be collapsed at the start");
+ synthesizeKey("KEY_Delete");
+ is(blockquote.innerHTML, "<p>b</p>", "Pressing Delete key at the start of <blockquote> should remove the first character in the <p>");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1328023.html b/editor/libeditor/tests/test_bug1328023.html
new file mode 100644
index 0000000000..b3c7cadee0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1328023.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1328023
+-->
+<html>
+<head>
+ <title>Test for Bug 1328023</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1328023">Mozilla Bug 1328023</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<input type="text" id="input1"/>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let elm = document.getElementById("input1");
+
+ elm.focus();
+ sendString("AB");
+ is(elm.value, "AB", "AB is input.value now");
+
+ synthesizeKey("KEY_Backspace");
+ is(elm.value, "A", "A is input.value now");
+
+ synthesizeKey("Z", { accelKey: true });
+ is(elm.value, "AB", "AB is input.value now");
+
+ sendString("C");
+ is(elm.value, "ABC", "ABC is input.value now");
+
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("KEY_Backspace");
+
+ sendString("ABC");
+ is(elm.value, "ABC", "ABC is input.value now");
+
+ synthesizeKey("Z", { accelKey: true });
+ is(elm.value, "", "'' is input.value now");
+
+ synthesizeKey("Z", { accelKey: true, shiftKey: true });
+ is(elm.value, "ABC", "ABC is input.value now");
+
+ sendString("D");
+ is(elm.value, "ABCD", "ABCD is input.value now");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1330796.html b/editor/libeditor/tests/test_bug1330796.html
new file mode 100644
index 0000000000..1fe1430b78
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1330796.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1330796
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 772796</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style> .pre { white-space: pre } </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 1330796</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="editable" contenteditable></div>
+
+<pre id="test">
+
+<script type="application/javascript">
+// We want to test what happens when the user splits a mail cite by clicking
+// at the start, the middle and the end of the cite and hitting the enter key.
+// Mail cites are spans, and since bug 1288911 they are displayed as blocks.
+// The _moz_quote attribute is used to give the cite a blue color via CSS.
+// As an internal attribute, it's not returned from the innerHTML.
+// To the user the tests look like:
+// > mailcite
+// This text is 10 characters long, so we position at 0, 5 and 10.
+// Althought since bug 1288911 those cites are displayed as block,
+// the tests are repeated also for inline display.
+// Each entry of the 'tests' array has the original HTML, the offset to click
+// at and the expected result HTML.
+var tests = [
+ // With style="display: block;".
+ [ "<span _moz_quote=true style=\"display: block;\">&gt; mailcite<br></span>", 0,
+ "x<br><span style=\"display: block;\">&gt; mailcite<br></span>" ],
+ [ "<span _moz_quote=true style=\"display: block;\">&gt; mailcite<br></span>", 5,
+ "<span style=\"display: block;\">&gt; mai<br></span>x<br><span style=\"display: block;\">lcite<br></span>"],
+ [ "<span _moz_quote=true style=\"display: block;\">&gt; mailcite<br></span>", 10,
+ "<span style=\"display: block;\">&gt; mailcite<br></span>x<br>" ],
+ // No <br> at the end to simulate prior deletion to the end of the quote.
+ [ "<span _moz_quote=true style=\"display: block;\">&gt; mailcite</span>", 10,
+ "<span style=\"display: block;\">&gt; mailcite<br></span>x<br>" ],
+
+ // Without style="display: block;".
+ [ "<span _moz_quote=true>&gt; mailcite<br></span>", 0,
+ "x<br><span>&gt; mailcite<br></span>" ],
+ [ "<span _moz_quote=true>&gt; mailcite<br></span>", 5,
+ "<span>&gt; mai</span><br>x<br><span>lcite<br></span>" ],
+ [ "<span _moz_quote=true>&gt; mailcite<br></span>", 10,
+ "<span>&gt; mailcite<br></span>x<br>" ],
+ // No <br> at the end to simulate prior deletion to the end of the quote.
+ [ "<span _moz_quote=true>&gt; mailcite</span>", 10,
+ "<span>&gt; mailcite</span><br>x<br>" ],
+];
+
+/** Test for Bug 1330796 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ var sel = window.getSelection();
+ var theEdit = document.getElementById("editable");
+ makeMailEditor();
+
+ for (let i = 0; i < tests.length; i++) {
+ theEdit.innerHTML = tests[i][0];
+ theEdit.focus();
+ var theText = theEdit.firstChild.firstChild;
+ // Position set at the beginning , middle and end of the text.
+ sel.collapse(theText, tests[i][1]);
+
+ synthesizeKey("KEY_Enter");
+ sendString("x");
+ is(theEdit.innerHTML, tests[i][2], "unexpected HTML for test " + i.toString());
+ }
+
+ SimpleTest.finish();
+});
+
+function makeMailEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ var editor = editingSession.getEditorForWindow(window);
+ editor.flags |= Ci.nsIEditor.eEditorMailMask;
+}
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1332876.html b/editor/libeditor/tests/test_bug1332876.html
new file mode 100644
index 0000000000..1234b53f62
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1332876.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1332876
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1332876</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1332876">Mozilla Bug 1332876</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<iframe srcdoc="<html><body><span>Edit me!</span>"></iframe>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 1332876 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let iframe = document.querySelector("iframe");
+ iframe.contentDocument.designMode = "on";
+
+ iframe.contentWindow.addEventListener("keypress", function() {
+ info("Hiding the iframe...");
+ iframe.style.display = "none";
+ document.body.offsetHeight;
+ ok(true, "did not crash");
+ SimpleTest.finish();
+ }, {once: true});
+
+ iframe.contentWindow.addEventListener("click", function() {
+ info("Waiting keypress event...");
+ // Use another macro task for avoiding impossible event nesting.
+ SimpleTest.executeSoon(() => {
+ synthesizeKey("a", {}, iframe.contentWindow);
+ });
+ }, {once: true});
+
+ let span = iframe.contentDocument.querySelector("span");
+ ok(span != null, "The span element should've been loaded in the iframe");
+ info("Waiting click event to focus the iframe...");
+ synthesizeMouseAtCenter(span, {}, iframe.contentWindow);
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1352799.html b/editor/libeditor/tests/test_bug1352799.html
new file mode 100644
index 0000000000..a3f53fc43b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1352799.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1352799
+-->
+<head>
+ <title>Test for Bug 1352799</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1352799">Mozilla Bug 1352799</a>
+<p id="display"></p>
+<div id="content">
+<div id="input-container" style="display: none;">
+<input id="input" maxlength="1">
+</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1352799 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ var input = document.getElementById("input");
+
+ var inputcontainer = document.getElementById("input-container");
+ input.setAttribute("maxlength", 2);
+ inputcontainer.style.display = "block";
+
+ input.focus();
+
+ sendString("123");
+
+ is(input.value, "12", "value should be 12 with maxlength = 2");
+
+ input.value = "";
+ inputcontainer.style.display = "none";
+
+ window.setTimeout(() => {
+ input.setAttribute("maxlength", 4);
+ inputcontainer.style.display = "block";
+
+ input.focus();
+
+ sendString("45678");
+
+ is(input.value, "4567", "value should be 4567 with maxlength = 4");
+
+ inputcontainer.style.display = "none";
+
+ window.setTimeout(() => {
+ input.setAttribute("maxlength", 2);
+ inputcontainer.style.display = "block";
+
+ input.focus();
+
+ sendString("12");
+
+ todo_is(input.value, "45", "value should be 45 with maxlength = 2");
+
+ input.value = "";
+ inputcontainer.style.display = "none";
+
+ window.setTimeout(() => {
+ input.removeAttribute("maxlength");
+ inputcontainer.style.display = "block";
+
+ input.focus();
+
+ sendString("12345678");
+
+ is(input.value, "12345678", "value should be 12345678 without maxlength");
+
+ SimpleTest.finish();
+ }, 0);
+ }, 0);
+ }, 0);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1355792.html b/editor/libeditor/tests/test_bug1355792.html
new file mode 100644
index 0000000000..c8231ebec4
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1355792.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Test for Bug 1355792</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<div contenteditable><div><font><table><td>a</table><br><br><table><td>b</table></font></div></div>
+<script>
+var font = document.querySelector("font");
+getSelection().collapse(font, 1);
+document.execCommand("forwarddelete");
+is(document.body.firstChild.innerHTML,
+ "<div><font><table><tbody><tr><td>a</td></tr></tbody></table><br>"
+ + "<table><tbody><tr><td>b</td></tr></tbody></table></font></div>",
+ "No creating an extra <br>");
+is(getSelection().focusNode, font, "Selection node should not change");
+is(getSelection().focusOffset, 1, "Selection offset should not move");
+ok(getSelection().isCollapsed, "Selection should be collapsed");
+</script>
diff --git a/editor/libeditor/tests/test_bug1358025.html b/editor/libeditor/tests/test_bug1358025.html
new file mode 100644
index 0000000000..331bde5d8a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1358025.html
@@ -0,0 +1,98 @@
+<html>
+<head>
+ <title>Test for bug 1358025</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1358025">Mozilla Bug 1358025</a>
+<div id="display">
+ <input type="text" id="edit">
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let input = document.getElementById("edit");
+ input.focus();
+
+ let controller =
+ SpecialPowers.wrap(input).controllers.getControllerForCommand("cmd_undo");
+
+ // Create undo transaction
+ SpecialPowers.wrap(input).setUserInput("XXX");
+ SpecialPowers.wrap(input).setUserInput("");
+
+ // Enable undo because this is user input
+ SpecialPowers.wrap(input).setUserInput("");
+ is(input.value, "", "value is empty");
+ ok(controller.isCommandEnabled("cmd_undo"),
+ "Undo is enabled by same empty string");
+
+ SpecialPowers.wrap(input).setUserInput("ABC");
+ is(input.value, "ABC", "value is ABC");
+ ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled by a string");
+
+ SpecialPowers.wrap(input).setUserInput("ABC");
+ is(input.value, "ABC", "value is ABC");
+ ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled by same string");
+
+ SpecialPowers.wrap(input).setUserInput("DEF");
+ is(input.value, "DEF", "value is DEF");
+ ok(controller.isCommandEnabled("cmd_undo"),
+ "Undo is enabled by different string");
+
+ SpecialPowers.wrap(input).setUserInput("");
+ is(input.value, "", "value is empty");
+ ok(controller.isCommandEnabled("cmd_undo"),
+ "Undo is enabled by empty string");
+
+ // disable undo because this is by script
+
+ // But Edge and Chrome still turn on undo when setting empty value from
+ // empty value. So we are same behaviour for this case.
+ input.value = "";
+ is(input.value, "", "value is empty");
+ ok(controller.isCommandEnabled("cmd_undo"),
+ "Undo is still enabled by same empty string");
+
+ input.value = "ABC";
+ is(input.value, "ABC", "value is ABC");
+ ok(!controller.isCommandEnabled("cmd_undo"), "Undo is disabled by a string");
+
+ // When setting same value by script, all browsers (Edge, Safari and Chrome)
+ // keep undo state.
+ input.value = "";
+ SpecialPowers.wrap(input).setUserInput("ABC");
+ ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled");
+ input.value = "ABC";
+ is(input.value, "ABC", "value is ABC");
+ ok(controller.isCommandEnabled("cmd_undo"),
+ "Undo is still enabled by same string");
+
+ input.value = "";
+ SpecialPowers.wrap(input).setUserInput("ABC");
+ ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled");
+ input.value = "DEF";
+ is(input.value, "DEF", "value is DEF");
+ ok(!controller.isCommandEnabled("cmd_undo"),
+ "Undo is disabled by different string");
+
+ input.value = "";
+ SpecialPowers.wrap(input).setUserInput("DEF");
+ ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled");
+ input.value = "";
+ is(input.value, "", "value is empty");
+ ok(!controller.isCommandEnabled("cmd_undo"),
+ "Undo is disabled by empty string");
+
+ SimpleTest.finish();
+}, window);
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1361008.html b/editor/libeditor/tests/test_bug1361008.html
new file mode 100644
index 0000000000..177ed6085e
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1361008.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi=id=1361008
+-->
+<html>
+ <head>
+ <title>Bug 1361008</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1361008">Mozilla Bug 1361008</a>
+<p id="display"></p>
+<div contenteditable="true" id="edit1">
+ <br>
+</div>
+<div contenteditable="true" id="edit2">
+ <br>
+</div>
+<pre id="test">
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ document.execCommand("defaultparagraphseparator", false, "div");
+ let edit1 = document.getElementById("edit1");
+ edit1.focus();
+ let selection = window.getSelection();
+ // Insert text before BR element
+ selection.collapse(edit1, 0);
+ sendString("AB");
+ synthesizeKey("KEY_Enter");
+ // count <div> element into contenteidable
+ is(edit1.getElementsByTagName("div").length, 2,
+ "DIV element should be 2 children");
+ is(edit1.getElementsByTagName("div")[0].innerText,
+ "AB", "AB should be in 1st DIV element");
+ is(edit1.getElementsByTagName("div")[1].innerHTML,
+ "<br>", "BR element should be in 2nd DIV element");
+
+ document.execCommand("defaultparagraphseparator", false, "p");
+ let edit2 = document.getElementById("edit2");
+ edit2.focus();
+ selection.collapse(edit2, 0);
+ // Insert text before BR element
+ sendString("AB");
+ synthesizeKey("KEY_Enter");
+
+ is(edit2.getElementsByTagName("p").length, 2,
+ "P element should be 2 children");
+ is(edit2.getElementsByTagName("p")[0].innerText,
+ "AB", "AB should be in 1st P element");
+ is(edit2.getElementsByTagName("p")[1].innerHTML,
+ "<br>", "BR element should be into 2nd P element");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1361052.html b/editor/libeditor/tests/test_bug1361052.html
new file mode 100644
index 0000000000..5e9e4df065
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1361052.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for Bug 1361052</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1361052">Mozilla Bug 1361052</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(() => {
+ var strike = document.createElement("strike");
+ strike.contentEditable = true;
+ document.documentElement.appendChild(strike);
+
+ var textarea = document.createElement("textarea");
+ document.documentElement.appendChild(textarea);
+
+ var h5 = document.createElement("h5");
+ strike.appendChild(h5);
+
+ textarea.setCustomValidity("A");
+ document.documentElement.dir = "rtl";
+ document.designMode = "on";
+ document.execCommand("styleWithCSS", false, true);
+ document.designMode = "off";
+ textarea.reportValidity();
+ document.documentElement.dir = "ltr";
+
+ var range = document.createRange();
+ range.selectNode(h5);
+ window.getSelection().addRange(range);
+
+ document.execCommand("inserthorizontalrule", false, null);
+ ok(true, "No crash");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1385905.html b/editor/libeditor/tests/test_bug1385905.html
new file mode 100644
index 0000000000..fda99ae741
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1385905.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi=id=1385905
+-->
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1385905">Mozilla Bug 1385905</a>
+<div id="display"></div>
+<div id="editor" contenteditable style="padding: 5px;"><div>contents</div></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ function ensureNoPaddingBR() {
+ for (let br of document.querySelectorAll("#editor > div > br")) {
+ ok(!SpecialPowers.wrap(br).isPaddingForEmptyLastLine,
+ "padding <br> element shouldn't be used with this test");
+ }
+ }
+ document.execCommand("defaultparagraphseparator", false, "div");
+ var editor = document.getElementById("editor");
+ // Click the left blank area of the first line to set cursor to the start of "contents".
+ synthesizeMouse(editor, 3, 10, {});
+ synthesizeKey("KEY_Enter");
+ is(editor.innerHTML, "<div><br></div><div>contents</div>",
+ "Typing Enter at start of the <div> element should split the <div> element");
+ synthesizeKey("KEY_ArrowUp");
+ sendString("x");
+ is(editor.innerHTML, "<div>x<br></div><div>contents</div>",
+ "Typing 'x' at the empty <div> element should just insert 'x' into the <div> element");
+ ensureNoPaddingBR();
+ synthesizeKey("KEY_Enter");
+ is(editor.innerHTML, "<div>x</div><div><br></div><div>contents</div>",
+ "Typing Enter next to 'x' in the first <div> element should split the <div> element and inserts <br> element to a new <div> element");
+ ensureNoPaddingBR();
+ synthesizeKey("KEY_Enter");
+ is(editor.innerHTML, "<div>x</div><div><br></div><div><br></div><div>contents</div>",
+ "Typing Enter in the empty <div> should split the <div> element and inserts <br> element to a new <div> element");
+ ensureNoPaddingBR();
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1390562.html b/editor/libeditor/tests/test_bug1390562.html
new file mode 100644
index 0000000000..924062352d
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1390562.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1390562
+-->
+<html>
+<head>
+ <title>Test for Bug 1390562</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1390562">Mozilla Bug 1390562</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="editor" contenteditable></div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("editor");
+
+ editor.focus();
+
+ // Make the HTML editor's default break is <br>
+ document.execCommand("defaultParagraphSeparator", false, "br");
+
+ editor.innerHTML = "<div>abc<br><br></div>def";
+
+ // Collapse selection at the end of the first text node.
+ window.getSelection().collapse(editor.firstChild.firstChild, 3);
+
+ // Then, typing Enter should insert <br> for <div> container.
+ // This is necessary for backward compatibility. When we change default
+ // value of "defaultParagraphSeparator" to "div" or "p", it may be possible
+ // to remove this hack.
+ synthesizeKey("KEY_Enter");
+
+ is(editor.innerHTML,
+ "<div>abc<br><br><br></div>def",
+ "Enter key press at end of a text node followed by a visible <br> shouldn't split <div> container when defaultParagraphSeparator is 'br'");
+
+ // Check also the case of <p> as container.
+ editor.innerHTML = "<p>abc<br><br></p>def";
+
+ // Collapse selection at the end of the first text node.
+ window.getSelection().collapse(editor.firstChild.firstChild, 3);
+
+ // Then, typing Enter should splitting <p> container and remove the visible
+ // <br> element next to the caret position.
+ // This is not consistent with <div> container, but this is better behavior
+ // and keep using this behavior.
+ synthesizeKey("KEY_Enter");
+
+ is(editor.innerHTML,
+ "<p>abc</p><p><br></p>def",
+ "Enter key press at end of a text node followed by a visible <br> should split <p> container and remove the visible <br> when defaultParagraphSeparator is 'br'");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1394758.html b/editor/libeditor/tests/test_bug1394758.html
new file mode 100644
index 0000000000..d1560e5092
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1394758.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1394758
+-->
+<head>
+ <title>Test for Bug1394758</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1394758">Mozilla Bug 1394758</a>
+<p id="display"></p>
+<div id="content">
+<div id="editable" contenteditable="true">
+ <span id="span" contenteditable="false">
+ Hello
+ </span>
+ World
+</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 611182 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var editable = document.getElementById("editable");
+ var span = document.getElementById("span");
+ var beforeSpan = span.textContent;
+
+ editable.focus();
+ window.getSelection().collapse(span.nextSibling, 0);
+
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("KEY_Backspace");
+
+ is(span.textContent, beforeSpan,
+ "VK_BACK_SPACE should not modify non-editable area");
+ is(span.nextSibling.textContent.trim(), "rld",
+ "VK_BACK_SPACE should delete first 2 characters");
+
+ synthesizeKey("KEY_Delete");
+
+ is(span.textContent, beforeSpan,
+ "VK_DELETE should not modify non-editable area");
+ is(span.nextSibling.textContent.trim(), "ld",
+ "VK_DELETE should delete first character");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1397412.xhtml b/editor/libeditor/tests/test_bug1397412.xhtml
new file mode 100644
index 0000000000..a975967696
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1397412.xhtml
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1397412
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 1397412" onload="runTest();">
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1397412"
+ target="_blank">Mozilla Bug 1397412</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ type="content"
+ editortype="textmail"
+ style="width: 400px; height: 200px;"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+function runTest() {
+ var initialHTML1 = "xx<br><br>";
+ var expectedHTML1 = "xx<br>t<br>";
+ var initialHTML2 = "xx<br><br>yy<br>";
+ var expectedHTML2 = "xx<br>t<br>yy<br>";
+ window.docShell
+ .rootTreeItem
+ .QueryInterface(Ci.nsIDocShell)
+ .appType = Ci.nsIDocShell.APP_TYPE_EDITOR;
+ var e = document.getElementById("editor");
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ var body = doc.body;
+
+ // Test 1.
+ body.innerHTML = initialHTML1;
+ selection.collapse(body, 2);
+ sendString("t");
+ var actualHTML = body.innerHTML;
+ is(actualHTML, expectedHTML1, "'t' should be inserted between <br>s");
+
+ // Test 2.
+ body.innerHTML = initialHTML2;
+ selection.collapse(body, 2);
+ sendString("t");
+ actualHTML = body.innerHTML;
+ is(actualHTML, expectedHTML2, "'t' should be inserted between <br>s");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_bug1399722.html b/editor/libeditor/tests/test_bug1399722.html
new file mode 100644
index 0000000000..4b6e2c7c72
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1399722.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1399722
+-->
+<html>
+<head>
+ <title>Test for Bug 1399722</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1399722">Mozilla Bug 1399722</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<input spellcheck="true" onkeypress="if (event.key=='Enter')this.value='';">
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let elm = document.querySelector("input");
+
+ elm.focus();
+ sendString("AB");
+ synthesizeKey("KEY_Enter");
+ sendString("CD");
+ is(elm.value, "CD", "Can type into the textbox successfully after the onkeypress handler deleting the value");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1406726.html b/editor/libeditor/tests/test_bug1406726.html
new file mode 100644
index 0000000000..7f7cbca963
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1406726.html
@@ -0,0 +1,126 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1406726
+-->
+<head>
+ <title>Test for Bug 1406726</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406726">Mozilla Bug 1406726</a>
+<p id="display"></p>
+<div id="editor" contenteditable></div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1406726 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("editor");
+ let selection = window.getSelection();
+
+ editor.focus();
+ for (let paragraphSeparator of ["div", "p"]) {
+ document.execCommand("defaultParagraphSeparator", false, paragraphSeparator);
+
+ // The result of editor.innerHTML may be wrong in this tests.
+ // Currently, editor wraps following elements of <br> element with default
+ // paragraph separator only when there is only non-editable elements.
+ // This behavior should be standardized by execCommand spec.
+
+ editor.innerHTML = "foo<br>bar<br><span contenteditable=\"false\">baz</span>";
+ selection.collapse(editor.childNodes.item(2), "bar".length);
+ document.execCommand("insertParagraph", false);
+ is(editor.innerHTML, "foo<br>" +
+ "<" + paragraphSeparator + ">bar</" + paragraphSeparator + ">" +
+ "<" + paragraphSeparator + "><br></" + paragraphSeparator + ">" +
+ "<" + paragraphSeparator + "><span contenteditable=\"false\">baz</span></" + paragraphSeparator + ">",
+ "All inline nodes including non-editable <span> element should be wrapped with default paragraph separator, <" + paragraphSeparator + ">");
+ ok(selection.isCollapsed, "Selection should be collapsed");
+ is(selection.anchorNode, editor.childNodes.item(3),
+ "Caret should be in the third line");
+ is(selection.anchorOffset, 0,
+ "Caret should be at start of the third line");
+
+ editor.innerHTML = "foo<br>bar<br><span>baz</span>";
+ selection.collapse(editor.childNodes.item(2), "bar".length);
+ document.execCommand("insertParagraph", false);
+ is(editor.innerHTML, "foo<br>" +
+ "<" + paragraphSeparator + ">bar</" + paragraphSeparator + ">" +
+ "<" + paragraphSeparator + "><br></" + paragraphSeparator + ">" +
+ "<span>baz</span>",
+ "All inline nodes in the second line should be wrapped with default paragraph separator, <" + paragraphSeparator + ">");
+ ok(selection.isCollapsed, "Selection should be collapsed");
+ is(selection.anchorNode, editor.childNodes.item(3),
+ "Caret should be in the third line");
+ is(selection.anchorOffset, 0,
+ "Caret should be at start of the third line");
+
+ editor.innerHTML = "foo<br>bar<br><span contenteditable=\"false\">baz</span>qux";
+ selection.collapse(editor.childNodes.item(2), "bar".length);
+ document.execCommand("insertParagraph", false);
+ is(editor.innerHTML, "foo<br>" +
+ "<" + paragraphSeparator + ">bar</" + paragraphSeparator + ">" +
+ "<" + paragraphSeparator + "><br></" + paragraphSeparator + ">" +
+ "<span contenteditable=\"false\">baz</span>qux",
+ "All inline nodes in the second line should be wrapped with default paragraph separator, <" + paragraphSeparator + ">");
+ ok(selection.isCollapsed, "Selection should be collapsed");
+ is(selection.anchorNode, editor.childNodes.item(3),
+ "Caret should be in the third line");
+ is(selection.anchorOffset, 0,
+ "Caret should be at start of the third line");
+
+ editor.innerHTML = "foo<br>bar<br><span contenteditable=\"false\">baz</span>";
+ selection.collapse(editor.childNodes.item(2), "ba".length);
+ document.execCommand("insertParagraph", false);
+ is(editor.innerHTML, "foo<br>" +
+ "<" + paragraphSeparator + ">ba</" + paragraphSeparator + ">" +
+ "<" + paragraphSeparator + ">r</" + paragraphSeparator + ">" +
+ "<" + paragraphSeparator + "><span contenteditable=\"false\">baz</span></" + paragraphSeparator + ">",
+ "All inline nodes including non-editable <span> element should be wrapped with default paragraph separator, <" + paragraphSeparator + ">");
+ ok(selection.isCollapsed, "Selection should be collapsed");
+ is(selection.anchorNode, editor.childNodes.item(3).firstChild,
+ "Caret should be in the text node in the third line");
+ is(selection.anchorOffset, 0,
+ "Caret should be at start of the text node in the third line");
+
+ editor.innerHTML = "foo<br>bar<br><span>baz</span>";
+ selection.collapse(editor.childNodes.item(2), "ba".length);
+ document.execCommand("insertParagraph", false);
+ is(editor.innerHTML, "foo<br>" +
+ "<" + paragraphSeparator + ">ba</" + paragraphSeparator + ">" +
+ "<" + paragraphSeparator + ">r</" + paragraphSeparator + ">" +
+ "<span>baz</span>",
+ "All inline nodes in the second line should be wrapped with default paragraph separator, <" + paragraphSeparator + ">");
+ ok(selection.isCollapsed, "Selection should be collapsed");
+ is(selection.anchorNode, editor.childNodes.item(3).firstChild,
+ "Caret should be in the text node in the third line");
+ is(selection.anchorOffset, 0,
+ "Caret should be at start of the text node in the third line");
+
+ editor.innerHTML = "foo<br>bar<br><span contenteditable=\"false\">baz</span>qux";
+ selection.collapse(editor.childNodes.item(2), "ba".length);
+ document.execCommand("insertParagraph", false);
+ is(editor.innerHTML, "foo<br>" +
+ "<" + paragraphSeparator + ">ba</" + paragraphSeparator + ">" +
+ "<" + paragraphSeparator + ">r</" + paragraphSeparator + ">" +
+ "<span contenteditable=\"false\">baz</span>qux",
+ "All inline nodes in the second line should be wrapped with default paragraph separator, <" + paragraphSeparator + ">");
+ ok(selection.isCollapsed, "Selection should be collapsed");
+ is(selection.anchorNode, editor.childNodes.item(3).firstChild,
+ "Caret should be in the text node in the third line");
+ is(selection.anchorOffset, 0,
+ "Caret should be at start of the text node in the third line");
+ }
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1409520.html b/editor/libeditor/tests/test_bug1409520.html
new file mode 100644
index 0000000000..f610d25a93
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1409520.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1409520
+-->
+<html>
+<head>
+ <title>Test for Bug 1409520</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1409520">Mozilla Bug 1409520</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<li contenteditable id="editor">
+ <select><option>option1</option></select>
+</li>
+
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var selection = window.getSelection();
+ var editor = document.getElementById("editor");
+ editor.focus();
+ selection.collapse(editor, 0);
+ document.execCommand("insertText", false, "A");
+ is(editor.firstChild.textContent, "A",
+ "'A' should be inserted at start of the editor");
+ is(editor.firstChild.nextSibling.tagName, "SELECT",
+ "<select> element shouldn't be removed by inserting 'A'");
+ is(selection.getRangeAt(0).startContainer, editor.firstChild,
+ "Caret should be moved after 'A'");
+ is(selection.getRangeAt(0).startOffset, 1,
+ "Caret should be moved after 'A'");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1425997.html b/editor/libeditor/tests/test_bug1425997.html
new file mode 100644
index 0000000000..637d77a0ca
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1425997.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1425997
+-->
+<html>
+<head>
+ <title>Test for Bug 1425997</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1425997">Mozilla Bug 1425997</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="editor" contenteditable>
+<!-- -->
+<span id="inline">foo</span>
+</div>
+
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+// 2 assertions are recorded due to nested execCommand() but not a problem.
+// They are necessary to detect invalid method call without mutation event listers.
+SimpleTest.expectAssertions(2, 2);
+SimpleTest.waitForFocus(async function() {
+ await SpecialPowers.pushPrefEnv({set: [["dom.document.exec_command.nested_calls_allowed", true]]});
+ let selection = window.getSelection();
+ let editor = document.getElementById("editor");
+ function onCharacterDataModified() {
+ // Until removing all NBSPs which were inserted by the editor,
+ // emulates Backspace key with "delete" command.
+ // When this test is created, the behavior was:
+ // after 1st delete: "\n<!-- -->&nbsp;\n"
+ // after 2nd delete: "\n<!-- -->&nbsp;"
+ // Then, selection is moved into the comment node and deletion won't
+ // work after that.
+ while (editor.innerHTML.includes("&nbsp;")) {
+ let preInnerHTML = editor.innerHTML;
+ if (!document.execCommand("delete", false) || preInnerHTML === editor.innerHTML) {
+ break;
+ }
+ info(`editor.innerHTML: "${editor.innerHTML.replace(/\n/g, "\\n")}"`);
+ }
+ }
+ editor.addEventListener("DOMCharacterDataModified", onCharacterDataModified, { once: true });
+ editor.focus();
+ selection.selectAllChildren(document.getElementById("inline"));
+ document.execCommand("insertHTML", false, "text");
+ // This expected result is just same as the result of Chrome.
+ // If the spec says this is wrong, feel free to change this result.
+ todo_is(editor.innerHTML, "\n<!-- --><span id=\"inline\">text</span>",
+ "The 'foo' should be replaced with 'text' and whitespaces before the span element should be removed");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1543312.html b/editor/libeditor/tests/test_bug1543312.html
new file mode 100644
index 0000000000..c0463d6d2f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1543312.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1543312
+-->
+<head>
+ <title>Test for Bug 1543312</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script>
+SimpleTest.expectAssertions(0, 1);
+
+add_task(async function() {
+ let iframe = document.getElementById("iframe2");
+ iframe.style.display = "none";
+ iframe.offsetHeight; // reflow
+ iframe.style.display = "";
+ iframe.offsetHeight; // reflow
+
+ iframe.focus();
+ let edit = iframe.contentDocument.getElementById("edit");
+ edit.focus();
+ synthesizeCompositionChange({
+ "composition": {
+ "string": "foo",
+ "clauses": [{
+ "length": 3,
+ "attr": COMPOSITION_ATTR_RAW_CLAUSE
+ }],
+ "caret": {
+ "start": 3,
+ "length": 0
+ }
+ }
+ });
+ synthesizeComposition({type: "compositioncommitasis"});
+ synthesizeCompositionChange({
+ "composition": {
+ "string": "bar",
+ "clauses": [{
+ "length": 3,
+ "attr": COMPOSITION_ATTR_RAW_CLAUSE
+ }],
+ "caret": {
+ "start": 3,
+ "length": 0
+ }
+ }
+ });
+ synthesizeComposition({type: "compositioncommitasis"});
+
+ is(edit.textContent, "foobar", "caret is updated correctly");
+
+ synthesizeKey("1");
+ is(edit.textContent, "foobar1", "caret is updated correctly");
+});
+</script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1543312">Mozilla Bug 1543312</a>
+ <p id="display"></p>
+
+ <div>
+ <iframe id="iframe1" srcdoc="<div contenteditable></div>"></iframe>
+ <iframe id="iframe2" srcdoc="<div id=edit contenteditable></div>"></iframe>
+ </div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1568996.html b/editor/libeditor/tests/test_bug1568996.html
new file mode 100644
index 0000000000..e0d48dad76
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1568996.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1568996
+-->
+<html>
+<head>
+<title>Test for Bug 1568996</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1568996">Bug 1568996</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<input id="input1">
+<script>
+add_task(async () => {
+ await new Promise((resolve) => {
+ SimpleTest.waitForFocus(() => {
+ SimpleTest.executeSoon(resolve);
+ }, window);
+ });
+
+ let input1 = document.getElementById("input1");
+ input1.value = "hello"
+ input1.focus();
+ input1.setSelectionRange(0, 0);
+
+ input1.addEventListener('keydown', () => {
+ let s = input1.selectionStart;
+ let e = input1.selectionEnd;
+ input1.value = input1.value.toUpperCase();
+ input1.setSelectionRange(s, e);
+ });
+
+ input1.addEventListener('input', () => {
+ let s = input1.selectionStart;
+ let e = input1.selectionEnd;
+ input1.value = input1.value.toLowerCase();
+ input1.setSelectionRange(s, e);
+ });
+
+ synthesizeKey('1');
+ synthesizeKey('KEY_Delete');
+ is(input1.value, "1ello", "Delete key should be worked");
+
+ synthesizeKey('B');
+ synthesizeKey('KEY_Delete');
+ synthesizeKey('KEY_Delete');
+ is(input1.value, "1blo", "Multiple delete key should be worked");
+
+ synthesizeKey('KEY_ArrowRight');
+ synthesizeKey('2');
+ synthesizeKey('KEY_Delete');
+ is(input1.value, "1bl2", "Delete key should be worked");
+
+ synthesizeKey('3');
+ is(input1.value, "1bl23", "charcter should be inserted");
+
+ synthesizeKey('KEY_Backspace');
+ is(input1.value, "1bl2", "Backspace key should be worked");
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1574596.html b/editor/libeditor/tests/test_bug1574596.html
new file mode 100644
index 0000000000..fe0a090d26
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1574596.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1574596
+-->
+<head>
+ <title>Test for Bug 1574596</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script>
+add_task(async function() {
+ let iframe = document.getElementById("iframe1");
+ iframe.focus();
+ let edit = iframe.contentDocument.getElementById("edit");
+ edit.focus();
+
+ iframe.contentDocument.execCommand("enableObjectResizing", false, true);
+
+ async function waitForSelectionChange() {
+ return new Promise(resolve => {
+ iframe.contentDocument.addEventListener("selectionchange", () => {
+ resolve();
+ }, {once: true});
+ });
+ }
+
+ let target = iframe.contentDocument.getElementById("target");
+ let promiseSelectionChangeEvent = waitForSelectionChange();
+ synthesizeMouseAtCenter(target, {}, iframe.contentWindow);
+ await promiseSelectionChangeEvent;
+
+ ok(target.hasAttribute("_moz_resizing"),
+ "resizers of the <img> should be visible");
+
+ iframe.style.display = "none";
+ iframe.offsetHeight; // reflow
+
+ await new Promise(SimpleTest.executeSoon);
+ ok(!target.hasAttribute("_moz_resizing"),
+ "resizers of the <img> should be hidden");
+
+ iframe.style.display = "";
+ iframe.offsetHeight; // reflow
+
+ promiseSelectionChangeEvent = waitForSelectionChange();
+ synthesizeMouseAtCenter(target, {}, iframe.contentWindow);
+ await promiseSelectionChangeEvent;
+
+ ok(target.hasAttribute("_moz_resizing"),
+ "resizers of the <img> should be visible again");
+});
+</script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1574596">Mozilla Bug 1574596</a>
+ <p id="display"></p>
+
+ <div>
+ <iframe id="iframe1" srcdoc="<div id=edit contenteditable><img id='target' src='green.png'></div>"></iframe>
+ </div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1581337.html b/editor/libeditor/tests/test_bug1581337.html
new file mode 100644
index 0000000000..c6163f6637
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1581337.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1581337
+-->
+<head>
+ <title>Test for Bug 1581337</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1581337">Mozilla Bug 1581337</a>
+ <p id="display"></p>
+
+ <div id="editor" contenteditable><span _moz_quote="true">foo bar</span></div>
+</body>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("editor");
+ editor.focus();
+ let selection = document.getSelection();
+ selection.collapse(editor.firstChild.firstChild, 4);
+ synthesizeKey("KEY_Backspace");
+ // FYI: `_moz_quote` attribute is ignored at serializing HTML content.
+ is(editor.innerHTML, "<span>foobar</span>", "Backspace should delete the previous whitespace");
+ synthesizeKey("KEY_Delete");
+ is(editor.innerHTML, "<span>fooar</span>", "Delete should delete the next character, \"b\"");
+ SimpleTest.finish();
+});
+</script>
+</html>
diff --git a/editor/libeditor/tests/test_bug1619852.html b/editor/libeditor/tests/test_bug1619852.html
new file mode 100644
index 0000000000..4564f36526
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1619852.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1619852
+-->
+<html>
+<head>
+<title>Test for Bug 1619852</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1619852">Bug 1619852</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<div contenteditable>abcd</div>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.querySelector("div[contenteditable]");
+ // Do nothing, but `HTMLEditor` may use different path to detect unexpected DOM tree or selection change.
+ editor.addEventListener("DOMNodeRemoved", () => {});
+ getSelection().collapse(editor.firstChild, 4);
+ synthesizeKey("KEY_Backspace");
+ is(editor.textContent, "abc", "The last character should've been removed by the Backspace");
+ getSelection().collapse(editor.firstChild, 1);
+ synthesizeKey("KEY_Backspace");
+ is(editor.textContent, "bc", "The first character should've been removed by the Backspace");
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1620778.html b/editor/libeditor/tests/test_bug1620778.html
new file mode 100644
index 0000000000..5a1c34fb1a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1620778.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Test for Bug 1620778</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<input id=a value=abcd autocomplete=off>
+<input id=a value=abcd>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let expectedPosition = null;
+ for (let input of document.querySelectorAll("input")) {
+ input.focus();
+ input.selectionStart = 0;
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_ArrowDown");
+ if (expectedPosition === null)
+ expectedPosition = input.selectionStart;
+ isnot(input.selectionStart, 0);
+ is(input.selectionStart, expectedPosition, "autocomplete shouldn't make a difference on inputs that have no completion results of any kind");
+ }
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1649005.html b/editor/libeditor/tests/test_bug1649005.html
new file mode 100644
index 0000000000..fbd8e16ef8
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1649005.html
@@ -0,0 +1,49 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1649005
+-->
+<head>
+ <meta charset="UTF-8" />
+ <title>Test for bug 1649005</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script>
+ /** Test for bug 1649005, bug 1779343 **/
+ window.addEventListener("DOMContentLoaded", (event) => {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ document.body.textContent = ""; // It would be \n\n otherwise...
+ synthesizeMouseAtCenter(document.body, {});
+
+ var editor = getEditor();
+ is(document.body.textContent, "", "Initial body check");
+ editor.rewrap(false);
+ is(document.body.textContent, "", "Initial body check after rewrap");
+
+ document.body.innerHTML = "&gt;abc<br/>&gt;def<br/>&gt;ghi";
+ editor.rewrap(true);
+ is(document.body.textContent, "> abc def ghi", "Rewrapped");
+
+ document.body.innerHTML = "&gt; ";
+ editor.rewrap(true);
+ is(document.body.textContent, "> ", "Rewrapped half-empty string");
+
+ SimpleTest.finish();
+ });
+ });
+
+ function getEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ var editor = editingSession.getEditorForWindow(window);
+ editor.QueryInterface(Ci.nsIHTMLEditor);
+ editor.QueryInterface(Ci.nsIEditorMailSupport);
+ editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+ return editor;
+ }
+ </script>
+</head>
+<body contenteditable></body>
+</html>
diff --git a/editor/libeditor/tests/test_bug1659276.html b/editor/libeditor/tests/test_bug1659276.html
new file mode 100644
index 0000000000..6789db2e77
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1659276.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>Test for Bug 1659276</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+</body>
+
+<script>
+document.designMode="on";
+while (window.find("Lorem")) {
+ document.execCommand("BackColor", 0, 'pink');
+}
+document.designMode="off";
+scroll(0,0);
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ function waitForTickOfRefeshDriver() {
+ function awaitOneRefresh() {
+ return new Promise(function(aResolve, aReject) {
+ requestAnimationFrame(aResolve);
+ });
+ }
+ return awaitOneRefresh().then(awaitOneRefresh);
+ }
+ await waitForTickOfRefeshDriver();
+ is(document.documentElement.scrollTop, 0, "scrollTop should be 0");
+ is(document.documentElement.scrollLeft, 0, "scrollLeft should be 0");
+ SimpleTest.finish();
+});
+</script>
+</html>
diff --git a/editor/libeditor/tests/test_bug1704381.html b/editor/libeditor/tests/test_bug1704381.html
new file mode 100644
index 0000000000..54751d922b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1704381.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Test for Bug 1704381</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<textarea>abc</textarea>
+</body>
+<script>
+"use strict";
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(() => {
+ const textarea = document.querySelector("textarea");
+ (function test_EditorBase_ToggleTextDirectionAsAction() {
+ textarea.removeAttribute("dir");
+ textarea.focus();
+ let newValue;
+ textarea.oninput = () => {
+ textarea.scrollHeight; // flush pending layout and run re-initializing editor synchronously
+ newValue = textarea.value;
+ };
+ SpecialPowers.doCommand(window, "cmd_switchTextDirection");
+ is(newValue, "abc",
+ "EditorBase::ToggleTextDirectionAsAction: Getting value should be succeeded immediately after reinitializing the editor");
+ textarea.removeAttribute("dir");
+ })();
+ (function test_EditorBase_NotifyEditorObservers() {
+ textarea.focus();
+ let newValue;
+ textarea.oninput = () => {
+ textarea.scrollHeight; // flush pending layout and run re-initializing editor synchronously
+ newValue = textarea.value;
+ };
+ document.execCommand("insertLineBreak");
+ is(newValue, "\nabc",
+ "EditorBase::NotifyEditorObservers: Getting value should be succeeded immediately after reinitializing the editor");
+ textarea.value = "abc";
+ })();
+ // TODO: Cannot test EditorBase::SwitchTextDirectionTo() since it requires bidi keyboard layout activated.
+ SimpleTest.finish();
+});
+</script>
+</html>
diff --git a/editor/libeditor/tests/test_bug200416.html b/editor/libeditor/tests/test_bug200416.html
new file mode 100644
index 0000000000..9fb6564250
--- /dev/null
+++ b/editor/libeditor/tests/test_bug200416.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=200416
+-->
+<title>Test for Bug 200416</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=200416">Mozilla Bug 200416</a>
+<div contenteditable><span>foo<p>bar</p></span></div>
+<script>
+getSelection().collapse(document.querySelector("p").firstChild, 0);
+document.execCommand("delete");
+var innerHTML = document.querySelector("div").innerHTML;
+ok(/foo.*bar/.test(innerHTML), "foo needs to still come before bar");
+</script>
diff --git a/editor/libeditor/tests/test_bug289384.html b/editor/libeditor/tests/test_bug289384.html
new file mode 100644
index 0000000000..2352dd3bc8
--- /dev/null
+++ b/editor/libeditor/tests/test_bug289384.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=289384
+-->
+<head>
+ <title>Test for Bug 289384</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=289384">Mozilla Bug 289384</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ var win = window.open("file_bug289384-1.html", "", "test-289384");
+ win.addEventListener("load", function() {
+ win.document.querySelector("a").click();
+ }, {once: true});
+});
+
+function continueTest(win) {
+ SimpleTest.waitForFocus(function() {
+ var doc = win.document;
+ var sel = win.getSelection();
+ doc.body.focus();
+ sel.collapse(doc.body.firstChild, 3);
+ SimpleTest.executeSoon(function() {
+ synthesizeKey("KEY_ArrowLeft", {accelKey: true}, win);
+ ok(sel.isCollapsed, "The selection must be collapsed");
+ is(sel.anchorNode, doc.body.firstChild, "The anchor node should be the body element's text node");
+ is(sel.anchorOffset, 0, "The anchor offset should be 0");
+ win.close();
+ SimpleTest.finish();
+ });
+ }, win);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug290026.html b/editor/libeditor/tests/test_bug290026.html
new file mode 100644
index 0000000000..786a12018c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug290026.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=290026
+-->
+<head>
+ <title>Test for Bug 290026</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=290026">Mozilla Bug 290026</a>
+<p id="display"></p>
+<div id="editor" contenteditable></div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 290026 **/
+SimpleTest.waitForExplicitFinish();
+
+var editor = document.getElementById("editor");
+editor.innerHTML = "<p></p><ul><li>Item 1</li><li>Item 2</li></ul><p></p>";
+editor.focus();
+
+addLoadEvent(function() {
+ document.execCommand("stylewithcss", false, "true");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ var lis = document.getElementsByTagName("li");
+ var range = document.createRange();
+ range.setStart(lis[0], 0);
+ range.setEnd(lis[1], lis[1].childNodes.length);
+ sel.addRange(range);
+ document.execCommand("indent", false, false);
+ var oneindent = '<p></p><ul style="margin-left: 40px;"><li>Item 1</li><li>Item 2</li></ul><p></p>';
+ is(editor.innerHTML, oneindent, "a once indented bulleted list");
+ document.execCommand("indent", false, false);
+ var twoindent = '<p></p><ul style="margin-left: 80px;"><li>Item 1</li><li>Item 2</li></ul><p></p>';
+ is(editor.innerHTML, twoindent, "a twice indented bulleted list");
+ document.execCommand("outdent", false, false);
+ is(editor.innerHTML, oneindent, "outdenting a twice indented bulleted list");
+
+ // done
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug291780.html b/editor/libeditor/tests/test_bug291780.html
new file mode 100644
index 0000000000..3a97073fad
--- /dev/null
+++ b/editor/libeditor/tests/test_bug291780.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=291780
+-->
+<head>
+ <title>Test for Bug 291780</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=291780">Mozilla Bug 291780</a>
+<p id="display"></p>
+<div id="editor" contenteditable></div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 291780 **/
+SimpleTest.waitForExplicitFinish();
+
+var original = '<ul style="margin-left: 40px;"><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li></ul>';
+var editor = document.getElementById("editor");
+editor.innerHTML = original;
+editor.focus();
+
+addLoadEvent(function() {
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ var lis = document.getElementsByTagName("li");
+ var range = document.createRange();
+ range.setStart(lis[1], 0);
+ range.setEnd(lis[2], lis[2].childNodes.length);
+ sel.addRange(range);
+ document.execCommand("indent", false, false);
+ var expected = '<ul style="margin-left: 40px;"><li>Item 1</li><ul><li>Item 2</li><li>Item 3</li></ul><li>Item 4</li></ul>';
+ is(editor.innerHTML, expected, "indenting part of an already indented bulleted list");
+ document.execCommand("outdent", false, false);
+ is(editor.innerHTML, original, "outdenting the partially indented part of an already indented bulleted list");
+
+ // done
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug309731.html b/editor/libeditor/tests/test_bug309731.html
new file mode 100644
index 0000000000..6e2a2e6001
--- /dev/null
+++ b/editor/libeditor/tests/test_bug309731.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=309731
+-->
+<head>
+ <title>Test for Bug 309731</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=309731">Mozilla Bug 309731</a>
+<p id="display"></p>
+
+<div id="content">
+ <div id="input" contentEditable="true"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 309731 **/
+
+function selectNode(node) {
+ getSelection().selectAllChildren(node);
+}
+
+function selectInNode(node) {
+ getSelection().collapse(node, 0);
+}
+
+function doTest() {
+ var input = document.getElementById("input");
+
+ is(input.textContent, "", "Input node starts empty");
+
+ selectInNode(input);
+ ok(document.execCommand("inserthtml", false, ""), "execCommand should return true");
+ is(input.textContent, "", "empty inserthtml with empty selection shouldn't change contents");
+
+ selectInNode(input);
+ ok(document.execCommand("inserthtml", false, "foo"), "execCommand should return true");
+ is(input.textContent, "foo", "'foo'inserthtml with empty selection should add foo to contents");
+
+ selectNode(input);
+ ok(document.execCommand("inserthtml", false, "bar"), "execCommand should return true");
+ is(input.textContent, "bar", "'bar' inserthtml with complete selection should replace contents with bar");
+
+ selectNode(input);
+ ok(document.execCommand("inserthtml", false, ""), "execCommand should return true");
+ is(input.textContent, "", "empty inserthtml with complete selection should delete everything");
+}
+
+doTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug316447.html b/editor/libeditor/tests/test_bug316447.html
new file mode 100644
index 0000000000..76d123815b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug316447.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=316447
+-->
+<title>Test for Bug 316447</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=316447">Mozilla Bug 316447</a>
+<div contenteditable><br></div>
+<script>
+/** Test for Bug 316447 **/
+
+getSelection().selectAllChildren(document.querySelector("div"));
+document.execCommand("inserthorizontalrule");
+is(document.querySelector("div").innerHTML, "<hr>", "Wrong innerHTML");
+</script>
diff --git a/editor/libeditor/tests/test_bug318065.html b/editor/libeditor/tests/test_bug318065.html
new file mode 100644
index 0000000000..be989fcdef
--- /dev/null
+++ b/editor/libeditor/tests/test_bug318065.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=318065
+-->
+
+<head>
+ <title>Test for Bug 318065</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=318065">Mozilla Bug 318065</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 318065 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ var expectedValues = ["A", "", "A", "", "A", "", "A"];
+ var messages = ["Initial text inserted",
+ "Initial text deleted",
+ "Undo of deletion",
+ "Redo of deletion",
+ "Initial text typed",
+ "Undo of typing",
+ "Redo of typing"];
+ var step = 0;
+
+ function onInput() {
+ is(this.value, expectedValues[step], messages[step]);
+ step++;
+ if (step == expectedValues.length) {
+ this.removeEventListener("input", onInput);
+ SimpleTest.finish();
+ }
+ }
+
+ var input = document.getElementById("t1");
+ input.addEventListener("input", onInput);
+ var input2 = document.getElementById("t2");
+ input2.addEventListener("input", onInput);
+
+ input.focus();
+
+ // Tests 0 + 1: Input letter and delete it again
+ sendString("A");
+ synthesizeKey("KEY_Backspace");
+
+ // Test 2: Undo deletion. Value of input should be "A"
+ synthesizeKey("Z", {accelKey: true});
+
+ // Test 3: Redo deletion. Value of input should be ""
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ input2.focus();
+
+ // Test 4: Input letter
+ sendString("A");
+
+ // Test 5: Undo typing. Value of input should be ""
+ synthesizeKey("Z", {accelKey: true});
+
+ // Test 6: Redo typing. Value of input should be "A"
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+ });
+ </script>
+ </pre>
+
+ <input type="text" value="" id="t1" />
+ <input type="text" value="" id="t2" />
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug332636.html b/editor/libeditor/tests/test_bug332636.html
new file mode 100644
index 0000000000..6fc255f7d6
--- /dev/null
+++ b/editor/libeditor/tests/test_bug332636.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=332636
+-->
+<head>
+ <title>Test for Bug 332636</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=332636">Mozilla Bug 332636</a>
+<p id="display"></p>
+<div id="content">
+ <div id="edit0" contenteditable="true">axb</div><!-- reference: plane 0 base character -->
+ <div id="edit1" contenteditable="true">a&#x0308;b</div><!-- reference: plane 0 diacritic -->
+ <div id="edit2" contenteditable="true">a&#x10400;b</div><!-- plane 1 base character -->
+ <div id="edit3" contenteditable="true">a&#x10a0f;b</div><!-- plane 1 diacritic -->
+
+ <div id="edit0b" contenteditable="true">axb</div><!-- reference: plane 0 base character -->
+ <div id="edit1b" contenteditable="true">a&#x0308;b</div><!-- reference: plane 0 diacritic -->
+ <div id="edit2b" contenteditable="true">a&#x10400;b</div><!-- plane 1 base character -->
+ <div id="edit3b" contenteditable="true">a&#x10a0f;b</div><!-- plane 1 diacritic -->
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 332636 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+function test(edit) {
+ edit.focus();
+ var sel = window.getSelection();
+ sel.collapse(edit.childNodes[0], edit.textContent.length - 1);
+ synthesizeKey("KEY_Backspace");
+ is(edit.textContent, "ab", "The backspace key should delete the UTF-16 surrogate pair correctly");
+}
+
+function testWithMove(edit, offset) {
+ edit.focus();
+ var sel = window.getSelection();
+ sel.collapse(edit.childNodes[0], 0);
+ var i;
+ for (i = 0; i < offset; ++i) {
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_ArrowRight");
+ }
+ synthesizeKey("KEY_Backspace");
+ is(edit.textContent, "ab", "The backspace key should delete the UTF-16 surrogate pair correctly");
+}
+
+function runTest() {
+ /* test backspace-deletion of the middle character */
+ test(document.getElementById("edit0"));
+ test(document.getElementById("edit1"));
+ test(document.getElementById("edit2"));
+ test(document.getElementById("edit3"));
+
+ /* extra tests with the use of RIGHT and LEFT to get to the right place */
+ testWithMove(document.getElementById("edit0b"), 2);
+ testWithMove(document.getElementById("edit1b"), 1);
+ testWithMove(document.getElementById("edit2b"), 2);
+ testWithMove(document.getElementById("edit3b"), 1);
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug332636.html^headers^ b/editor/libeditor/tests/test_bug332636.html^headers^
new file mode 100644
index 0000000000..e853d6cee5
--- /dev/null
+++ b/editor/libeditor/tests/test_bug332636.html^headers^
@@ -0,0 +1 @@
+Content-Type: text/html; charset=UTF-8
diff --git a/editor/libeditor/tests/test_bug358033.html b/editor/libeditor/tests/test_bug358033.html
new file mode 100644
index 0000000000..db2b00400e
--- /dev/null
+++ b/editor/libeditor/tests/test_bug358033.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=358033
+-->
+<head>
+ <title>Test for Bug 358033</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=358033">Mozilla Bug 358033</a>
+<p id="display"></p>
+<div id="content">
+<input type="text" id="input1">
+</div>
+<pre id="test">
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let input = document.getElementById("input1");
+
+ input.value = "ABC DEF";
+ input.focus();
+ input.setSelectionRange(4, 7, "backward");
+ synthesizeKey("KEY_Backspace");
+ is(input.value, "ABC ", "KEY_Backspace should remove selected string");
+ synthesizeKey("Z", { accelKey: true });
+ is(input.value, "ABC DEF", "Undo should restore string");
+ synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ synthesizeKey("KEY_Backspace");
+
+ is(input.value, "AB", "anchor node and focus node should be kept order after undo");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug372345.html b/editor/libeditor/tests/test_bug372345.html
new file mode 100644
index 0000000000..bd2de542c4
--- /dev/null
+++ b/editor/libeditor/tests/test_bug372345.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=372345
+-->
+<head>
+ <title>Test for Bug 372345</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=372345">Mozilla Bug 372345</a>
+<p id="display"></p>
+<div id="content">
+ <iframe srcdoc="<body>"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 372345 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var iframe = document.querySelector("iframe");
+ var doc = iframe.contentDocument;
+ var content = doc.body;
+ function testCursor(post) {
+ setTimeout(function() {
+ var link = document.createElement("a");
+ link.href = "http://mozilla.org/";
+ link.textContent = "link";
+ link.style.cursor = "pointer";
+ content.appendChild(link);
+ is(iframe.contentWindow.getComputedStyle(link).cursor, "pointer", "Make sure that the cursor is set to pointer");
+ setTimeout(post, 0);
+ }, 0);
+ }
+ testCursor(function() {
+ doc.designMode = "on";
+ testCursor(function() {
+ doc.designMode = "off";
+ testCursor(function() {
+ content.setAttribute("contenteditable", "true");
+ testCursor(function() {
+ content.removeAttribute("contenteditable");
+ testCursor(function() {
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug404320.html b/editor/libeditor/tests/test_bug404320.html
new file mode 100644
index 0000000000..fe27dfc875
--- /dev/null
+++ b/editor/libeditor/tests/test_bug404320.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=404320
+-->
+<head>
+ <title>Test for Bug 404320</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=404320">Mozilla Bug 404320</a>
+<p id="display"></p>
+<div id="content">
+ <iframe id="testIframe"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 404320 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function runTests() {
+ var win = document.getElementById("testIframe").contentWindow;
+ var doc = document.getElementById("testIframe").contentDocument;
+
+ function testFormatBlock(tag, withAngleBrackets, shouldSucceed) {
+ win.getSelection().selectAllChildren(doc.body.firstChild);
+ doc.execCommand("FormatBlock", false,
+ withAngleBrackets ? tag : "<" + tag + ">");
+ var resultNode;
+ if (shouldSucceed && (tag == "dd" || tag == "dt")) {
+ is(doc.body.firstChild.tagName, "DL", "tag was changed");
+ resultNode = doc.body.firstChild.firstChild;
+ } else {
+ resultNode = doc.body.firstChild;
+ }
+
+ is(resultNode.tagName, shouldSucceed ? tag.toUpperCase() : "P", "tag was changed");
+ }
+
+ function formatBlockTests(tags, shouldSucceed) {
+ var html = "<p>Content</p>";
+ for (var i = 0; i < tags.length; ++i) {
+ var tag = tags[i];
+
+ doc.body.innerHTML = html;
+ testFormatBlock(tag, false, shouldSucceed);
+
+ doc.body.innerHTML = html;
+ testFormatBlock(tag, true, shouldSucceed);
+ }
+ }
+
+ doc.designMode = "on";
+
+ var goodTags = [ "address",
+ "blockquote",
+ "dd",
+ "div",
+ "dl",
+ "dt",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "p",
+ "pre" ];
+ var badTags = [ "b",
+ "i",
+ "span",
+ "foo" ];
+
+ formatBlockTests(goodTags, true);
+ formatBlockTests(badTags, false);
+ SimpleTest.finish();
+}
+
+addLoadEvent(runTests);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug408231.html b/editor/libeditor/tests/test_bug408231.html
new file mode 100644
index 0000000000..517b20619a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug408231.html
@@ -0,0 +1,233 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=408231
+-->
+<head>
+ <title>Test for Bug 408231</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body style="font-family: serif">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408231">Mozilla Bug 408231</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 408231 **/
+
+ var commandEnabledResults = [
+ ["copy", "false"],
+ ["createlink", "true"],
+ ["cut", "false"],
+ ["delete", "true"],
+ ["fontname", "true"],
+ ["fontsize", "true"],
+ ["formatblock", "true"],
+ ["hilitecolor", "true"],
+ ["indent", "true"],
+ ["inserthorizontalrule", "true"],
+ ["inserthtml", "true"],
+ ["insertimage", "true"],
+ ["insertorderedlist", "true"],
+ ["insertunorderedlist", "true"],
+ ["insertparagraph", "true"],
+ ["italic", "true"],
+ ["justifycenter", "true"],
+ ["justifyfull", "true"],
+ ["justifyleft", "true"],
+ ["justifyright", "true"],
+ ["outdent", "true"],
+ ["paste", "false"],
+ ["redo", "false"],
+ ["removeformat", "true"],
+ ["selectall", "true"],
+ ["strikethrough", "true"],
+ ["styleWithCSS", "true"],
+ ["subscript", "true"],
+ ["superscript", "true"],
+ ["underline", "true"],
+ ["undo", "false"],
+ ["unlink", "true"],
+ ["not-a-command", "false"],
+ ];
+
+ var commandIndetermResults = [
+ ["copy", "false"],
+ ["createlink", "false"],
+ ["cut", "false"],
+ ["delete", "false"],
+ ["fontname", "false"],
+ ["fontsize", "false"],
+ ["formatblock", "false"],
+ ["hilitecolor", "false"],
+ ["indent", "false"],
+ ["inserthorizontalrule", "false"],
+ ["inserthtml", "false"],
+ ["insertimage", "false"],
+ ["insertorderedlist", "false"],
+ ["insertunorderedlist", "false"],
+ ["insertparagraph", "false"],
+ ["italic", "false"],
+ ["justifycenter", "false"],
+ ["justifyfull", "false"],
+ ["justifyleft", "false"],
+ ["justifyright", "false"],
+ ["outdent", "false"],
+ // ["paste", "false"],
+ ["redo", "false"],
+ ["removeformat", "false"],
+ ["selectall", "false"],
+ ["strikethrough", "false"],
+ ["styleWithCSS", "false"],
+ ["subscript", "false"],
+ ["superscript", "false"],
+ ["underline", "false"],
+ ["undo", "false"],
+ ["unlink", "false"],
+ ["not-a-command", "false"],
+ ];
+
+ var commandStateResults = [
+ ["copy", "false"],
+ ["createlink", "false"],
+ ["cut", "false"],
+ ["delete", "false"],
+ ["fontname", "false"],
+ ["fontsize", "false"],
+ ["formatblock", "false"],
+ ["hilitecolor", "false"],
+ ["indent", "false"],
+ ["inserthorizontalrule", "false"],
+ ["inserthtml", "false"],
+ ["insertimage", "false"],
+ ["insertorderedlist", "false"],
+ ["insertunorderedlist", "false"],
+ ["insertparagraph", "false"],
+ ["italic", "false"],
+ ["justifycenter", "false"],
+ ["justifyfull", "false"],
+ ["justifyleft", "true"],
+ ["justifyright", "false"],
+ ["outdent", "false"],
+ // ["paste", "false"],
+ ["redo", "false"],
+ ["removeformat", "false"],
+ ["selectall", "false"],
+ ["strikethrough", "false"],
+ ["styleWithCSS", "false"],
+ ["subscript", "false"],
+ ["superscript", "false"],
+ ["underline", "false"],
+ ["undo", "false"],
+ ["unlink", "false"],
+ ["not-a-command", "false"],
+ ];
+
+ var commandValueResults = [
+ ["copy", ""],
+ ["createlink", ""],
+ ["cut", ""],
+ ["delete", ""],
+ ["fontname", "serif"],
+ ["fontsize", ""],
+ ["formatblock", ""],
+ ["hilitecolor", "transparent"],
+ ["indent", ""],
+ ["inserthorizontalrule", ""],
+ ["inserthtml", ""],
+ ["insertimage", ""],
+ ["insertorderedlist", ""],
+ ["insertunorderedlist", ""],
+ ["insertparagraph", ""],
+ ["italic", ""],
+ ["justifycenter", "left"],
+ ["justifyfull", "left"],
+ ["justifyleft", "left"],
+ ["justifyright", "left"],
+ ["outdent", ""],
+ // ["paste", ""],
+ ["redo", ""],
+ ["removeformat", ""],
+ ["selectall", ""],
+ ["strikethrough", ""],
+ ["styleWithCSS", ""],
+ ["subscript", ""],
+ ["superscript", ""],
+ ["underline", ""],
+ ["undo", ""],
+ ["unlink", ""],
+ ["not-a-command", ""],
+ ];
+
+
+ function callQueryCommandEnabled(cmdName) {
+ var result;
+ try {
+ result = "" + document.queryCommandEnabled(cmdName);
+ } catch (error) {
+ result = "name" in error ? error.name : "exception";
+ }
+ return result;
+ }
+
+ function callQueryCommandIndeterm(cmdName) {
+ var result;
+ try {
+ result = "" + document.queryCommandIndeterm(cmdName);
+ } catch (error) {
+ result = "name" in error ? error.name : "exception";
+ }
+ return result;
+ }
+
+ function callQueryCommandState(cmdName) {
+ var result;
+ try {
+ result = "" + document.queryCommandState(cmdName);
+ } catch (error) {
+ result = "name" in error ? error.name : "exception";
+ }
+ return result;
+ }
+
+ function callQueryCommandValue(cmdName) {
+ var result;
+ try {
+ result = "" + document.queryCommandValue(cmdName);
+ } catch (error) {
+ result = "name" in error ? error.name : "exception";
+ }
+ return result;
+ }
+
+ function testQueryCommand(expectedResults, fun, funName) {
+ for (let i = 0; i < expectedResults.length; i++) {
+ var commandName = expectedResults[i][0];
+ var expectedResult = expectedResults[i][1];
+ var result = fun(commandName);
+ ok(result == expectedResult, funName + "(" + commandName + ") result=" + result + " expected=" + expectedResult);
+ }
+ }
+
+ function runTests() {
+ document.designMode = "on";
+ window.getSelection().collapse(document.body, 0);
+ testQueryCommand(commandEnabledResults, callQueryCommandEnabled, "queryCommandEnabled");
+ testQueryCommand(commandIndetermResults, callQueryCommandIndeterm, "queryCommandIndeterm");
+ testQueryCommand(commandStateResults, callQueryCommandState, "queryCommandState");
+ testQueryCommand(commandValueResults, callQueryCommandValue, "queryCommandValue");
+ document.designMode = "off";
+ SimpleTest.finish();
+ }
+
+ window.onload = runTests;
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug410986.html b/editor/libeditor/tests/test_bug410986.html
new file mode 100644
index 0000000000..fdc5d27631
--- /dev/null
+++ b/editor/libeditor/tests/test_bug410986.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=410986
+-->
+<head>
+ <title>Test for Bug 410986</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=410986">Mozilla Bug 410986</a>
+<p id="display"></p>
+<div id="content">
+ <div id="contents"><span style="color: green;">green text</span></div>
+ <div id="editor" contenteditable="true"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 410986 **/
+
+var gPasteEvents = 0;
+document.getElementById("editor").addEventListener("paste", function() {
+ ++gPasteEvents;
+});
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ getSelection().selectAllChildren(document.getElementById("contents"));
+ SimpleTest.waitForClipboard("green text",
+ function() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function() {
+ var ed = document.getElementById("editor");
+ ed.focus();
+ if (navigator.platform.includes("Mac")) {
+ synthesizeKey("V", {accelKey: true, shiftKey: true, altKey: true});
+ } else {
+ synthesizeKey("V", {accelKey: true, shiftKey: true});
+ }
+ is(ed.innerHTML, "green text", "Content should be pasted in plaintext format");
+ is(gPasteEvents, 1, "One paste event must be fired");
+
+ ed.innerHTML = "";
+ ed.blur();
+ getSelection().selectAllChildren(document.getElementById("contents"));
+ SimpleTest.waitForClipboard("green text",
+ function() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function() {
+ var ed1 = document.getElementById("editor");
+ ed1.focus();
+ synthesizeKey("V", {accelKey: true});
+ isnot(ed1.innerHTML.indexOf("<span style=\"color: green;\">green text</span>"), -1,
+ "Content should be pasted in HTML format");
+ is(gPasteEvents, 2, "Two paste events must be fired");
+
+ SimpleTest.finish();
+ },
+ function() {
+ ok(false, "Failed to copy the second item to the clipboard");
+ SimpleTest.finish();
+ }
+ );
+ },
+ function() {
+ ok(false, "Failed to copy the first item to the clipboard");
+ SimpleTest.finish();
+ }
+ );
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug414526.html b/editor/libeditor/tests/test_bug414526.html
new file mode 100644
index 0000000000..03aa72867e
--- /dev/null
+++ b/editor/libeditor/tests/test_bug414526.html
@@ -0,0 +1,234 @@
+<html>
+<head>
+ <title>Test for backspace key and delete key shouldn't remove another editing host's text</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="display"></div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+ var container = document.getElementById("display");
+
+ function reset() {
+ document.execCommand("Undo", false, null);
+ }
+
+ var selection = window.getSelection();
+ function moveCaretToStartOf(aEditor) {
+ selection.selectAllChildren(aEditor);
+ selection.collapseToStart();
+ }
+
+ function moveCaretToEndOf(aEditor) {
+ selection.selectAllChildren(aEditor);
+ selection.collapseToEnd();
+ }
+
+ /* TestCase #1
+ */
+ const kTestCase1 =
+ "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" +
+ "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" +
+ "<div id=\"editor3\" contenteditable=\"true\"><div>editor3</div></div>" +
+ "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" +
+ "non-editable text" +
+ "<p id=\"editor5\" contenteditable=\"true\">editor5</p>";
+
+ const kTestCase1_editor3_deleteAtStart =
+ "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" +
+ "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" +
+ "<div id=\"editor3\" contenteditable=\"true\"><div>ditor3</div></div>" +
+ "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" +
+ "non-editable text" +
+ "<p id=\"editor5\" contenteditable=\"true\">editor5</p>";
+
+ const kTestCase1_editor3_backspaceAtEnd =
+ "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" +
+ "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" +
+ "<div id=\"editor3\" contenteditable=\"true\"><div>editor</div></div>" +
+ "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" +
+ "non-editable text" +
+ "<p id=\"editor5\" contenteditable=\"true\">editor5</p>";
+
+ container.innerHTML = kTestCase1;
+
+ var editor1 = document.getElementById("editor1");
+ var editor2 = document.getElementById("editor2");
+ var editor3 = document.getElementById("editor3");
+ var editor4 = document.getElementById("editor4");
+ var editor5 = document.getElementById("editor5");
+
+ /* TestCase #1:
+ * pressing backspace key at start should not change the content.
+ */
+ editor2.focus();
+ moveCaretToStartOf(editor2);
+ synthesizeKey("KEY_Backspace");
+ is(container.innerHTML, kTestCase1,
+ "Pressing backspace key at start of editor2 changes the content");
+ reset();
+
+ editor3.focus();
+ moveCaretToStartOf(editor3);
+ synthesizeKey("KEY_Backspace");
+ is(container.innerHTML, kTestCase1,
+ "Pressing backspace key at start of editor3 changes the content");
+ reset();
+
+ editor4.focus();
+ moveCaretToStartOf(editor4);
+ synthesizeKey("KEY_Backspace");
+ is(container.innerHTML, kTestCase1,
+ "Pressing backspace key at start of editor4 changes the content");
+ reset();
+
+ editor5.focus();
+ moveCaretToStartOf(editor5);
+ synthesizeKey("KEY_Backspace");
+ is(container.innerHTML, kTestCase1,
+ "Pressing backspace key at start of editor5 changes the content");
+ reset();
+
+ /* TestCase #1:
+ * pressing delete key at end should not change the content.
+ */
+ editor1.focus();
+ moveCaretToEndOf(editor1);
+ synthesizeKey("KEY_Delete");
+ is(container.innerHTML, kTestCase1,
+ "Pressing delete key at end of editor1 changes the content");
+ reset();
+
+ editor2.focus();
+ moveCaretToEndOf(editor2);
+ synthesizeKey("KEY_Delete");
+ is(container.innerHTML, kTestCase1,
+ "Pressing delete key at end of editor2 changes the content");
+ reset();
+
+ editor3.focus();
+ moveCaretToEndOf(editor3);
+ synthesizeKey("KEY_Delete");
+ is(container.innerHTML, kTestCase1,
+ "Pressing delete key at end of editor3 changes the content");
+ reset();
+
+ editor4.focus();
+ moveCaretToEndOf(editor4);
+ synthesizeKey("KEY_Delete");
+ is(container.innerHTML, kTestCase1,
+ "Pressing delete key at end of editor4 changes the content");
+ reset();
+
+ /* TestCase #1: cases when the caret is not on text node.
+ * - pressing delete key at start should remove the first character
+ * - pressing backspace key at end should remove the first character
+ * and the adjacent blocks should not be changed.
+ */
+ editor3.focus();
+ moveCaretToStartOf(editor3);
+ synthesizeKey("KEY_Delete");
+ is(container.innerHTML, kTestCase1_editor3_deleteAtStart,
+ "Pressing delete key at start of editor3 changes adjacent elements"
+ + " and/or does not remove the first character.");
+ reset();
+
+ editor3.focus();
+ moveCaretToEndOf(editor3);
+ synthesizeKey("KEY_Backspace");
+ is(container.innerHTML, kTestCase1_editor3_backspaceAtEnd,
+ "Pressing backspace key at end of editor3 changes adjacent elements"
+ + " and/or does not remove the last character.");
+ reset();
+
+ /* TestCase #2:
+ * two adjacent editable <span> in a table cell.
+ */
+ const kTestCase2 = "<table><tbody><tr><td><span id=\"editor1\" contenteditable=\"true\">test</span>" +
+ "<span id=\"editor2\" contenteditable=\"true\">test</span></td></tr></tbody></table>";
+
+ container.innerHTML = kTestCase2;
+ editor1 = document.getElementById("editor1");
+ editor2 = document.getElementById("editor2");
+
+ editor2.focus();
+ moveCaretToStartOf(editor2);
+ synthesizeKey("KEY_Backspace");
+ is(container.innerHTML, kTestCase2,
+ "Pressing backspace key at the start of editor2 changes the content for kTestCase2");
+ reset();
+
+ editor1.focus();
+ moveCaretToEndOf(editor1);
+ synthesizeKey("KEY_Delete");
+ is(container.innerHTML, kTestCase2,
+ "Pressing delete key at the end of editor1 changes the content for kTestCase2");
+ reset();
+
+ /* TestCase #3:
+ * editable <span> in two adjacent table cells.
+ */
+ const kTestCase3 = "<table><tbody><tr><td><span id=\"editor1\" contenteditable=\"true\">test</span></td>" +
+ "<td><span id=\"editor2\" contenteditable=\"true\">test</span></td></tr></tbody></table>";
+
+ container.innerHTML = kTestCase3;
+ editor1 = document.getElementById("editor1");
+ editor2 = document.getElementById("editor2");
+
+ editor2.focus();
+ moveCaretToStartOf(editor2);
+ synthesizeKey("KEY_Backspace");
+ is(container.innerHTML, kTestCase3,
+ "Pressing backspace key at the start of editor2 changes the content for kTestCase3");
+ reset();
+
+ editor1.focus();
+ moveCaretToEndOf(editor1);
+ synthesizeKey("KEY_Delete");
+ is(container.innerHTML, kTestCase3,
+ "Pressing delete key at the end of editor1 changes the content for kTestCase3");
+ reset();
+
+ /* TestCase #4:
+ * editable <div> in two adjacent table cells.
+ */
+ const kTestCase4 = "<table><tbody><tr><td><div id=\"editor1\" contenteditable=\"true\">test</div></td>" +
+ "<td><div id=\"editor2\" contenteditable=\"true\">test</div></td></tr></tbody></table>";
+
+ container.innerHTML = kTestCase4;
+ editor1 = document.getElementById("editor1");
+ editor2 = document.getElementById("editor2");
+
+ editor2.focus();
+ moveCaretToStartOf(editor2);
+ synthesizeKey("KEY_Backspace");
+ is(container.innerHTML, kTestCase4,
+ "Pressing backspace key at the start of editor2 changes the content for kTestCase4");
+ reset();
+
+ editor1.focus();
+ moveCaretToEndOf(editor1);
+ synthesizeKey("KEY_Delete");
+ is(container.innerHTML, kTestCase4,
+ "Pressing delete key at the end of editor1 changes the content for kTestCase4");
+ reset();
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug417418.html b/editor/libeditor/tests/test_bug417418.html
new file mode 100644
index 0000000000..d319e4e984
--- /dev/null
+++ b/editor/libeditor/tests/test_bug417418.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=417418
+-->
+<head>
+ <title>Test for Bug 417418</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=417418">Mozilla Bug 417418</a>
+<div id="display" contenteditable="true">
+<p id="coin">first paragraph</p>
+<p>second paragraph. <img id="img" src="green.png"></p>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 417418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+function resetSelection() {
+ window.getSelection().collapse(document.getElementById("coin"), 0);
+}
+
+function runTest() {
+ var rightClickDown = {type: "mousedown", button: 2},
+ rightClickUp = {type: "mouseup", button: 2},
+ singleClickDown = {type: "mousedown", button: 0},
+ singleClickUp = {type: "mouseup", button: 0};
+ var selection = window.getSelection();
+
+ var div = document.getElementById("display");
+ var img = document.getElementById("img");
+ var divRect = div.getBoundingClientRect();
+ var imgselected;
+
+ resetSelection();
+ synthesizeMouse(div, divRect.width - 1, divRect.height - 1, rightClickDown);
+ synthesizeMouse(div, divRect.width - 1, divRect.height - 1, rightClickUp);
+ ok(selection.isCollapsed, "selection is not collapsed");
+
+ resetSelection();
+ synthesizeMouse(div, divRect.width - 1, divRect.height - 1, singleClickDown);
+ synthesizeMouse(div, divRect.width - 1, divRect.height - 1, singleClickUp);
+ ok(selection.isCollapsed, "selection is not collapsed");
+
+ resetSelection();
+ synthesizeMouseAtCenter(img, rightClickDown);
+ synthesizeMouseAtCenter(img, rightClickUp);
+ imgselected = selection.anchorNode == img.parentNode &&
+ selection.anchorOffset === 1 &&
+ selection.rangeCount === 1;
+ ok(imgselected, "image is not selected");
+
+ resetSelection();
+ synthesizeMouseAtCenter(img, singleClickDown);
+ synthesizeMouseAtCenter(img, singleClickUp);
+ imgselected = selection.anchorNode == img.parentNode &&
+ selection.anchorOffset === 1 &&
+ selection.rangeCount === 1;
+ ok(imgselected, "image is not selected");
+
+ SimpleTest.finish();
+}
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug426246.html b/editor/libeditor/tests/test_bug426246.html
new file mode 100644
index 0000000000..50e5df2cb4
--- /dev/null
+++ b/editor/libeditor/tests/test_bug426246.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=426246
+-->
+<html>
+<head>
+ <title>Test for Bug 426246</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=426246">Mozilla Bug 426246</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div contenteditable="true" id="contenteditable1">
+ <p>first line</p>
+ <p>this is the second line</p>
+</div>
+
+<div contenteditable="true" id="contenteditable2">first line<br>this is the second line</div>
+<div contenteditable="true" id="contenteditable3"><ul><li>first line</li><li>this is the second line</li></ul></div>
+<pre contenteditable="true" id="contenteditable4">first line
+this is the second line</pre>
+
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let elm1 = document.getElementById("contenteditable1");
+ elm1.focus();
+ window.getSelection().collapse(elm1.lastElementChild.firstChild, "this is the ".length);
+ SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
+ is(elm1.firstElementChild.textContent, "first line", "two paragraphs: the first line should stay untouched");
+ is(elm1.lastElementChild.textContent, "second line", "two paragraphs: the characters after the caret should remain");
+
+ let elm2 = document.getElementById("contenteditable2");
+ elm2.focus();
+ window.getSelection().collapse(elm2.lastChild, "this is the ".length);
+ is(elm2.lastChild.textContent, "this is the second line", "br: correct initial content");
+ SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
+ is(elm2.firstChild.textContent, "first line", "br: the first line should stay untouched");
+ is(elm2.lastChild.textContent, "second line", "br: the characters after the caret should remain");
+
+ let elm3 = document.getElementById("contenteditable3");
+ elm3.focus();
+ let firstLineLI = elm3.querySelector("li:first-child");
+ let secondLineLI = elm3.querySelector("li:last-child");
+ window.getSelection().collapse(secondLineLI.firstChild, "this is the ".length);
+ is(secondLineLI.textContent, "this is the second line", "li: correct initial content");
+ SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
+ is(firstLineLI.textContent, "first line", "li: the first line should stay untouched");
+ is(secondLineLI.textContent, "second line", "li: the characters after the caret should remain");
+
+ let elm4 = document.getElementById("contenteditable4");
+ elm4.focus();
+ window.getSelection().collapse(elm4.firstChild, "first line\nthis is the ".length);
+ is(elm4.textContent, "first line\nthis is the second line", "pre: correct initial content");
+ SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
+ is(elm4.textContent, "first line\nsecond line", "pre: the first line should stay untouched and the characters after the caret in the second line should remain");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug430392.html b/editor/libeditor/tests/test_bug430392.html
new file mode 100644
index 0000000000..7ae6b0f7b0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug430392.html
@@ -0,0 +1,171 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=430392
+-->
+<head>
+ <title>Test for Bug 430392</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=430392">Mozilla Bug 430392</a>
+<p id="display"></p>
+<div id="content">
+ <div contenteditable="true" id="edit"> <span contenteditable="false">A</span> ; <span contenteditable="false">B</span> ; <span contenteditable="false">C</span> </div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 430392 **/
+
+function test() {
+ var edit = document.getElementById("edit");
+ var html = edit.innerHTML;
+ var expectedText = edit.textContent;
+ document.getElementById("edit").focus();
+
+ // Each test is [desc, callback, inputType of `beforeinput`, inputType of `input`].
+ // callback() is called and we check that the textContent didn't change.
+ // For expected failures, the format is
+ // [desc, callback, undefined, inputType of `beforeinput`, inputType of `input`, expectedValue],
+ // and the test will be marked as an expected fail if the textContent changes
+ // to expectedValue, and an unexpected fail if it's neither the original value
+ // nor expectedValue.
+ let tests = [["adding returns", () => {
+ getSelection().collapse(edit.firstChild, 0);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_Enter");
+ synthesizeKey("KEY_Enter");
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("KEY_Backspace");
+ }, [
+ "insertParagraph",
+ "insertParagraph",
+ "deleteContentBackward",
+ "deleteContentBackward",
+ ],
+ // Blink does nothing in this case, but WebKit does insert paragraph.
+ [/* no input events for NOOP */]],
+ ["adding shift-returns", () => {
+ getSelection().collapse(edit.firstChild, 0);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_Enter", {shiftKey: true});
+ synthesizeKey("KEY_Enter", {shiftKey: true});
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("KEY_Backspace");
+ }, [
+ "insertLineBreak",
+ "insertLineBreak",
+ "deleteContentBackward",
+ "deleteContentBackward",
+ ],
+ // Blink does nothing in this case, but WebKit inserts `<br>` element.
+ [/* no input events for NOOP */]],
+ ];
+ [
+ ["insertorderedlist", "insertOrderedList"],
+ ["insertunorderedlist", "insertUnorderedList"],
+ ["formatblock", "", "p"],
+ ]
+ .forEach(item => {
+ let cmd = item[0];
+ let param = item[2];
+ let inputType = item[1];
+ tests.push([cmd, () => { document.execCommand(cmd, false, param); },
+ [/* execCommand shouldn't cause beforeinput event */],
+ [inputType]]);
+ });
+ // These are all TODO -- they don't move the non-editable elements
+ [
+ ["bold", "formatBold"],
+ ["italic", "formatItalic"],
+ ["underline", "formatUnderline"],
+ ["strikethrough", "formatStrikeThrough"],
+ ["subscript", "formatSubscript"],
+ ["superscript", "formatSuperscript"],
+ ["forecolor", "formatFontColor", "blue"],
+ ["backcolor", "formatBackColor", "blue"],
+ ["hilitecolor", "formatBackColor", "blue"],
+ ["fontname", "formatFontName", "monospace"],
+ ["fontsize", "", "1"],
+ ["justifyright", "formatJustifyRight"],
+ ["justifycenter", "formatJustifyCenter"],
+ ["justifyfull", "formatJustifyFull"],
+ ]
+ .forEach(item => {
+ let cmd = item[0];
+ let param = item[2];
+ let inputType = item[1];
+ tests.push([cmd, () => { document.execCommand(cmd, false, param); },
+ [/* execCommand shouldn't cause beforeinput event */],
+ [inputType],
+ " A ; ; BC "]);
+ });
+ tests.push(["indent", () => { document.execCommand("indent"); },
+ [/* execCommand shouldn't cause beforeinput event */],
+ ["formatIndent"],
+ " ; ; ABC"]);
+
+ let beforeinputTypes = [];
+ let inputTypes = [];
+ edit.addEventListener("beforeinput", event => { beforeinputTypes.push(event.inputType); });
+ edit.addEventListener("input", event => { inputTypes.push(event.inputType); });
+ tests.forEach(arr => {
+ ["div", "br", "p"].forEach(sep => {
+ document.execCommand("defaultParagraphSeparator", false, sep);
+
+ let expectedFailText = typeof arr[4] == "function" ? arr[4]() : arr[4];
+
+ edit.innerHTML = html;
+ edit.focus();
+ getSelection().selectAllChildren(edit);
+ beforeinputTypes = [];
+ inputTypes = [];
+ arr[1]();
+ if (typeof expectedFailText != "undefined") {
+ todo_is(edit.textContent, expectedText,
+ arr[0] + " should not change text (" + sep + ")");
+ if (edit.textContent !== expectedText &&
+ edit.textContent !== expectedFailText) {
+ is(edit.textContent, expectedFailText,
+ arr[0] + " changed to different failure (" + sep + ")");
+ }
+ } else {
+ is(edit.textContent, expectedText,
+ arr[0] + " should not change text (" + sep + ")");
+ }
+ is(beforeinputTypes.length, arr[2].length, `${arr[0]}: number of beforeinput events should be ${arr[2].length} (${sep})`);
+ for (let i = 0; i < Math.max(beforeinputTypes.length, arr[2].length); i++) {
+ if (i < beforeinputTypes.length && i < arr[2].length) {
+ is(beforeinputTypes[i], arr[2][i], `${arr[0]}: ${i + 1}th inputType of beforeinput event should be "${arr[2][i]}" (${sep})`);
+ } else if (i < beforeinputTypes.length) {
+ ok(false, `${arr[0]}: Redundant beforeinput event shouldn't be fired, its inputType was "${beforeinputTypes[i]}" (${sep})`);
+ } else {
+ ok(false, `${arr[0]}: beforeinput event whose inputType is "${arr[2][i]}" should be fired, but not fired (${sep})`);
+ }
+ }
+ is(inputTypes.length, arr[3].length, `${arr[0]}: number of input events is unexpected (${sep})`);
+ for (let i = 0; i < Math.max(inputTypes.length, arr[3].length); i++) {
+ if (i < inputTypes.length && i < arr[3].length) {
+ is(inputTypes[i], arr[3][i], `${arr[0]}: ${i + 1}th inputType of input event should be "${arr[3][i]}" (${sep})`);
+ } else if (i < inputTypes.length) {
+ ok(false, `${arr[0]}: Redundant input event shouldn't be fired, its inputType was "${inputTypes[i]}" (${sep})`);
+ } else {
+ ok(false, `${arr[0]}: input event whose inputType is "${arr[3][i]}" should be fired, but not fired (${sep})`);
+ }
+ }
+ });
+ });
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(test);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug439808.html b/editor/libeditor/tests/test_bug439808.html
new file mode 100644
index 0000000000..169ba5bd70
--- /dev/null
+++ b/editor/libeditor/tests/test_bug439808.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=439808
+-->
+<head>
+ <title>Test for Bug 439808</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=439808">Mozilla Bug 439808</a>
+<p id="display"></p>
+<div id="content">
+<span><span contenteditable id="e">twest</span></span>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 439808 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var e = document.getElementById("e");
+ e.focus();
+ getSelection().collapse(e.firstChild, 1);
+ synthesizeKey("KEY_Delete");
+ is(e.textContent, "test", "Delete key worked");
+ synthesizeKey("KEY_Backspace");
+ is(e.textContent, "est", "Backspace key worked");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug442186.html b/editor/libeditor/tests/test_bug442186.html
new file mode 100644
index 0000000000..deecbed5f1
--- /dev/null
+++ b/editor/libeditor/tests/test_bug442186.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=442186
+-->
+<head>
+ <title>Test for Bug 442186</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=442186">Mozilla Bug 442186</a>
+<p id="display"></p>
+<div id="content">
+ <h2> two &lt;div&gt; containers </h2>
+ <section contenteditable id="test1">
+ <div> First paragraph with some text. </div>
+ <div> Second paragraph with some text. </div>
+ </section>
+
+ <h2> two paragraphs </h2>
+ <section contenteditable id="test2">
+ <p> First paragraph with some text. </p>
+ <p> Second paragraph with some text. </p>
+ </section>
+
+ <h2> one text node, one paragraph </h2>
+ <section contenteditable id="test3">
+ First paragraph with some text.
+ <p> Second paragraph with some text. </p>
+ </section>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 442186 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function justify(textNode, pos) {
+ if (!pos) pos = 10;
+
+ // put the caret on the requested character
+ var range = document.createRange();
+ var sel = window.getSelection();
+ range.setStart(textNode, pos);
+ range.setEnd(textNode, pos);
+ sel.addRange(range);
+
+ // align
+ document.execCommand("justifyright", false, null);
+}
+
+function runTests() {
+ document.execCommand("stylewithcss", false, "true");
+
+ const test1 = document.getElementById("test1");
+ const test2 = document.getElementById("test2");
+ const test3 = document.getElementById("test3");
+
+ // #test1: two <div> containers
+ const line1 = test1.querySelector("div").firstChild;
+ test1.focus();
+ justify(line1);
+ is(test1.querySelectorAll("*").length, 2,
+ "Aligning the first child should not create nor remove any element.");
+ is(line1.parentNode.nodeName.toLowerCase(), "div",
+ "Aligning the first <div> should not modify its node type.");
+ is(line1.parentNode.style.textAlign, "right",
+ "Aligning the first <div> should set a 'text-align: right' style rule.");
+
+ // #test2: two paragraphs
+ const line2 = test2.querySelector("p").firstChild;
+ test2.focus();
+ justify(line2);
+ is(test2.querySelectorAll("*").length, 2,
+ "Aligning the first child should not create nor remove any element.");
+ is(line2.parentNode.nodeName.toLowerCase(), "p",
+ "Aligning the first paragraph should not modify its node type.");
+ is(line2.parentNode.style.textAlign, "right",
+ "Aligning the first paragraph should set a 'text-align: right' style rule.");
+
+ // #test3: one text node, two paragraphs
+ const line3 = test3.firstChild;
+ test3.focus();
+ justify(line3);
+ is(test3.querySelectorAll("*").length, 2,
+ "Aligning the first child should create a block element.");
+ is(line3.parentNode.nodeName.toLowerCase(), "div",
+ "Aligning the first child should create a block element.");
+ is(line3.parentNode.style.textAlign, "right",
+ "Aligning the first line should set a 'text-align: right' style rule.");
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug455992.html b/editor/libeditor/tests/test_bug455992.html
new file mode 100644
index 0000000000..57f6716336
--- /dev/null
+++ b/editor/libeditor/tests/test_bug455992.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 455992</title>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+function runTest() {
+ function select(id) {
+ var e = document.getElementById(id);
+ e.focus();
+ return e;
+ }
+
+ function setupIframe(id) {
+ var e = document.getElementById(id);
+ var doc = e.contentDocument;
+ doc.body.innerHTML = String.fromCharCode(10) + '<span id="' + id + '_span" style="border:1px solid blue" contenteditable="true">X</span>' + String.fromCharCode(10);
+ e = doc.getElementById(id + "_span");
+ e.focus();
+ return e;
+ }
+
+ function test_begin_bs(e) {
+ const msg = "BACKSPACE at beginning of contenteditable inline element";
+ var before = e.parentNode.childNodes[0].nodeValue;
+ sendKey("back_space");
+ is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id);
+ is(e.innerHTML, "X", msg + " with id=" + e.id);
+ }
+
+ function test_begin_space(e) {
+ const msg = "SPACE at beginning of contenteditable inline element";
+ var before = e.parentNode.childNodes[0].nodeValue;
+ sendChar(" ");
+ is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id);
+ is(e.innerHTML, "&nbsp;X", msg + " with id=" + e.id);
+ }
+
+ function test_end_delete(e) {
+ const msg = "DEL at end of contenteditable inline element";
+ var before = e.parentNode.childNodes[2].nodeValue;
+ sendKey("right");
+ sendKey("delete");
+ is(e.parentNode.childNodes[2].nodeValue, before, msg + " with id=" + e.id);
+ is(e.innerHTML, "X", msg + " with id=" + e.id);
+ }
+
+ function test_end_space(e) {
+ const msg = "SPACE at end of contenteditable inline element";
+ var before = e.parentNode.childNodes[2].nodeValue;
+ sendKey("right");
+ sendChar(" ");
+ is(e.parentNode.childNodes[2].nodeValue, before, msg + " with id=" + e.id);
+ is(
+ e.innerHTML,
+ SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible") || e.tagName == "SPAN"
+ ? "X&nbsp;"
+ : "X <br>",
+ msg + " with id=" + e.id
+ );
+ }
+
+ test_begin_bs(select("t1"));
+ test_begin_space(select("t2"));
+ test_end_delete(select("t3"));
+ test_end_space(select("t4"));
+ test_end_space(select("t5"));
+
+ test_begin_bs(setupIframe("i1"));
+ test_begin_space(setupIframe("i2"));
+ test_end_delete(setupIframe("i3"));
+ test_end_space(setupIframe("i4"));
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=455992">Mozilla Bug 455992</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<div> <span id="t1" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+<div> <span id="t2" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+<div> <span id="t3" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+<div> <span id="t4" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+<div> <div id="t5" style="border:1px solid blue" contenteditable="true">X</div> Y</div>
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i3" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i4" width="200" height="100" src="about:blank"></iframe><br>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug456244.html b/editor/libeditor/tests/test_bug456244.html
new file mode 100644
index 0000000000..11ae5ad987
--- /dev/null
+++ b/editor/libeditor/tests/test_bug456244.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 456244</title>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+function runTest() {
+ function select(id) {
+ var e = document.getElementById(id);
+ e.focus();
+ return e;
+ }
+
+ function setupIframe(id) {
+ var e = document.getElementById(id);
+ var doc = e.contentDocument;
+ doc.body.innerHTML = String.fromCharCode(10) + '<span id="' + id + '_span" style="border:1px solid blue" contenteditable="true">X</span>' + String.fromCharCode(10);
+ e = doc.getElementById(id + "_span");
+ e.focus();
+ return e;
+ }
+
+ function test_end_bs(e) {
+ const msg = "Deleting all text in contenteditable inline element";
+ var before = e.parentNode.childNodes[0].nodeValue;
+ sendKey("right");
+ sendKey("back_space");
+ sendKey("back_space");
+ is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id);
+ is(e.innerHTML, "", msg + " with id=" + e.id);
+ }
+
+ test_end_bs(select("t1"));
+ test_end_bs(setupIframe("i1", 0));
+
+ {
+ const msg = "Deleting all text in contenteditable body element";
+ var e = document.getElementById("i2");
+ var doc = e.contentDocument;
+ doc.body.setAttribute("contenteditable", "true");
+ doc.body.focus();
+ sendKey("right");
+ sendKey("back_space");
+ is(doc.body.innerHTML, "<br>", msg + " with id=" + e.id);
+ }
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=456244">Mozilla Bug 456244</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<div> <span id="t1" style="border:1px solid blue" contenteditable="true">X</span> Y</div>
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank">X</iframe><br>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug460740.html b/editor/libeditor/tests/test_bug460740.html
new file mode 100644
index 0000000000..509ad6d6ae
--- /dev/null
+++ b/editor/libeditor/tests/test_bug460740.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=460740
+-->
+<head>
+ <title>Test for Bug 460740</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=460740">Mozilla Bug 460740</a>
+<p id="display"></p>
+<div id="content">
+ <ul>
+ <li contenteditable>
+ Editable LI
+ </li>
+ <li>
+ <div contenteditable>
+ Editable DIV inside LI
+ </div>
+ </li>
+ <li>
+ <div>
+ <div contenteditable>
+ Editable DIV inside DIV inside LI
+ </div>
+ </div>
+ </li>
+ <li>
+ <h3>
+ <div contenteditable>
+ Editable DIV inside H3 inside LI
+ </div>
+ </h3>
+ </li>
+ </ul>
+ <div contenteditable>
+ Editable DIV
+ </div>
+ <h3 contenteditable>
+ Editable H3
+ </h3>
+ <p contenteditable>
+ Editable P
+ </p>
+ <div>
+ <p contenteditable>
+ Editable P in a DIV
+ </p>
+ </div>
+ <p><span contenteditable>Editable SPAN in a P</span></p>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 460740 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+const CARET_BEGIN = 0;
+const CARET_MIDDLE = 1;
+const CARET_END = 2;
+
+function split(element, caretPos) {
+ // compute the requested position
+ var len = element.textContent.length;
+ var pos = -1;
+ switch (caretPos) {
+ case CARET_BEGIN:
+ pos = 0;
+ break;
+ case CARET_MIDDLE:
+ pos = Math.floor(len / 2);
+ break;
+ case CARET_END:
+ pos = len;
+ break;
+ }
+
+ // put the caret on the requested position
+ var range = document.createRange();
+ var sel = window.getSelection();
+ range.setStart(element.firstChild, pos);
+ range.setEnd(element.firstChild, pos);
+ sel.addRange(range);
+
+ // simulates a [Return] keypress
+ synthesizeKey("VK_RETURN", {shiftKey: true});
+}
+
+// count the number of non-BR elements in #content
+function getBlockCount() {
+ return document.querySelectorAll("#content *:not(br)").length;
+}
+
+// count the number of BRs in element
+function checkBR(element) {
+ return element.querySelectorAll("br").length;
+}
+
+function runTests() {
+ var count = getBlockCount();
+ var nodes = document.querySelectorAll("#content [contenteditable]");
+ for (var i = 0; i < nodes.length; i++) {
+ var node = nodes[i];
+ node.focus();
+ is(checkBR(node), 0, node.textContent.trim() + ": This node should not have any <br> element yet.");
+ for (var j = 0; j < 3; j++) { // CARET_BEGIN|MIDDLE|END
+ split(node, j);
+ ok(checkBR(node) > 0, node.textContent.trim() + " " + j + ": Pressing [Return] should add (at least) one <br> element.");
+ is(getBlockCount(), count, node.textContent.trim() + " " + j + ": Pressing [Return] should not change the number of non-<br> elements.");
+ document.execCommand("Undo", false, null);
+ }
+ }
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug46555.html b/editor/libeditor/tests/test_bug46555.html
new file mode 100644
index 0000000000..3838bdb3b2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug46555.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=46555
+-->
+
+<head>
+ <title>Test for Bug 46555</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=46555">Mozilla Bug 46555</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <input type="text" value="" id="t1" />
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 46555 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ const kCmd = "cmd_selectAll";
+
+ var input = document.getElementById("t1");
+ input.focus();
+ var controller =
+ SpecialPowers.wrap(input).controllers.getControllerForCommand(kCmd);
+
+ // Test 1: Select All should be disabled if editor is empty
+ is(controller.isCommandEnabled(kCmd), false,
+ "Select All command disabled when editor is empty");
+
+ SimpleTest.finish();
+ });
+ </script>
+ </pre>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug471319.html b/editor/libeditor/tests/test_bug471319.html
new file mode 100644
index 0000000000..5a180cb310
--- /dev/null
+++ b/editor/libeditor/tests/test_bug471319.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=471319
+-->
+
+<head>
+ <title>Test for Bug 471319</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=471319">Mozilla Bug 471319</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 471319 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function doTest() {
+ let t1 = SpecialPowers.wrap($("t1"));
+
+ // Test 1: Undo on an empty editor - the editor should not forget about
+ // the padding <br> element for empty editor.
+ let t1Editor = t1.editor;
+
+ // Did the editor recognize the new padding <br> element?
+ t1Editor.undo();
+ ok(!t1.value, "<br> still recognized as padding on undo");
+
+
+ // Test 2: Redo on an empty editor - the editor should not forget about
+ // the padding <br> element for empty editor.
+ let t2 = SpecialPowers.wrap($("t2"));
+ let t2Editor = t2.editor;
+
+ // Did the editor recognize the new padding <br> element?
+ t2Editor.redo();
+ ok(!t2.value, "<br> still recognized as padding on redo");
+
+
+ // Test 3: Undoing a batched transaction where both end points of the
+ // transaction are the padding <br> element for empty editor - the
+ // <br> element should still be recognized as padding.
+ t1Editor.beginTransaction();
+ t1.value = "mozilla";
+ t1.value = "";
+ t1Editor.endTransaction();
+ t1Editor.undo();
+ ok(!t1.value,
+ "recreated <br> from undo transaction recognized as padding");
+
+
+ // Test 4: Redoing a batched transaction where both end points of the
+ // transaction are the padding <br> element for empty editor - the
+ // <br> element should still be recognized padding.
+ t1Editor.redo();
+ ok(!t1.value,
+ "recreated <br> from redo transaction recognized as padding");
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input type="text" id="t1" />
+ <input type="text" id="t2" />
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug471722.html b/editor/libeditor/tests/test_bug471722.html
new file mode 100644
index 0000000000..188c30d245
--- /dev/null
+++ b/editor/libeditor/tests/test_bug471722.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=471722
+-->
+
+<head>
+ <title>Test for Bug 471722</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=471722">Mozilla Bug 471722</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 471722 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function doTest() {
+ var t1 = $("t1");
+ var editor = SpecialPowers.wrap(t1).editor;
+
+ ok(editor, "able to get editor for the element");
+ t1.focus();
+ t1.select();
+
+ try {
+ // Cut the initial text in the textbox
+ ok(editor.canCut(), "can cut text");
+ editor.cut();
+ is(t1.value, "", "initial text was removed");
+
+ // So now we will have emptied the textfield and the editor will have
+ // created a padding <br> element for empty editor.
+ // Check the transaction is in the undo stack...
+ ok(editor.canUndo, "undo should be available");
+
+ // Undo the cut
+ editor.undo();
+ is(t1.value, "minefield", "text reinserted");
+
+ // So now, the cut should be in the redo stack, so executing the redo
+ // will clear the text once again and reinsert the padding <br>
+ // element for empty editor that was removed after undo.
+ // This will require the editor to figure out that we have a padding
+ // <br> element again...
+ ok(editor.canRedo, "redo should be available");
+ editor.redo();
+
+ // Did the editor notice a padding <br> element for empty editor
+ // reappeared?
+ is(t1.value, "", "editor found padding <br> element");
+ } catch (e) {
+ ok(false, "test failed with error " + e);
+ }
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input type="text" value="minefield" id="t1" />
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug478725.html b/editor/libeditor/tests/test_bug478725.html
new file mode 100644
index 0000000000..45e6aeed13
--- /dev/null
+++ b/editor/libeditor/tests/test_bug478725.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 478725</title>
+<style src="/tests/SimpleTest/test.css" type="text/css"></style>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+function runTest() {
+ function verifyContent(s) {
+ var e = document.getElementById("i1");
+ var doc = e.contentDocument;
+ is(doc.body.innerHTML, s, "");
+ }
+
+ function pasteInto(html, target_id) {
+ var e = document.getElementById("i1");
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.body.innerHTML = html;
+ e = doc.getElementById(target_id);
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(e);
+ selection.collapseToEnd();
+ SpecialPowers.wrap(doc).execCommand("paste", false, null);
+ return e;
+ }
+
+ function copyToClipBoard(s, asHTML, target_id) {
+ var e = document.getElementById("i2");
+ var doc = e.contentDocument;
+ if (asHTML) {
+ doc.body.innerHTML = s;
+ } else {
+ var text = doc.createTextNode(s);
+ doc.body.appendChild(text);
+ }
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ if (!target_id) {
+ selection.selectAllChildren(doc.body);
+ } else {
+ var range = document.createRange();
+ range.selectNode(doc.getElementById(target_id));
+ selection.addRange(range);
+ }
+ SpecialPowers.wrap(doc).execCommand("copy", false, null);
+ return e;
+ }
+
+ copyToClipBoard("<dl><dd>Hello Kitty</dd></dl>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here");
+ verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl></li></ol>');
+
+ copyToClipBoard("<li>Hello Kitty</li>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here");
+ verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>');
+
+ copyToClipBoard("<ol><li>Hello Kitty</li></ol>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here");
+ verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>');
+
+ copyToClipBoard("<ul><li>Hello Kitty</li></ul>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here");
+ verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>');
+
+ copyToClipBoard("<ul><li>Hello</li><ul><li>Kitty</li></ul></ul>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here");
+ verifyContent('<ol><li id="paste_here">X</li><li>Hello</li><ul><li>Kitty</li></ul></ol>');
+
+ copyToClipBoard("<dl><dd>Hello</dd><dd>Kitty</dd></dl>", true);
+ pasteInto('<dl><dd id="paste_here">X</dd></dl>', "paste_here");
+ verifyContent('<dl><dd id="paste_here">X</dd><dd>Hello</dd><dd>Kitty</dd></dl>');
+
+ copyToClipBoard("<dl><dd>Hello</dd><dd>Kitty</dd></dl>", true);
+ pasteInto('<dl><dt id="paste_here">X</dt></dl>', "paste_here");
+ verifyContent('<dl><dt id="paste_here">X</dt><dd>Hello</dd><dd>Kitty</dd></dl>');
+
+ copyToClipBoard("<dl><dt>Hello</dt><dd>Kitty</dd></dl>", true);
+ pasteInto('<dl><dd id="paste_here">X</dd></dl>', "paste_here");
+ verifyContent('<dl><dd id="paste_here">X</dd><dt>Hello</dt><dd>Kitty</dd></dl>');
+
+ copyToClipBoard("<pre>Kitty</pre>", true);
+ pasteInto('<pre id="paste_here">Hello </pre>', "paste_here");
+ verifyContent('<pre id="paste_here">Hello Kitty</pre>');
+
+// I was expecting these to trigger the special TABLE/TR rules in nsHTMLEditor::InsertHTMLWithContext
+// but they don't for some reason...
+// copyToClipBoard('<table><tr id="copy_here"><td>Kitty</td></tr></table>', true, "copy_here");
+// pasteInto('<table><tr id="paste_here"><td>Hello</td></tr></table>',"paste_here");
+// verifyContent('');
+//
+// copyToClipBoard('<table id="copy_here"><tr><td>Kitty</td></tr></table>', true, "copy_here");
+// pasteInto('<table><tr id="paste_here"><td>Hello</td></tr></table>',"paste_here");
+// verifyContent('');
+//
+// copyToClipBoard('<table id="copy_here"><tr><td>Kitty</td></tr></table>', true, "copy_here");
+// pasteInto('<table id="paste_here"><tr><td>Hello</td></tr></table>',"paste_here");
+// verifyContent('');
+//
+// copyToClipBoard('<table><tr id="copy_here"><td>Kitty</td></tr></table>', true, "copy_here");
+// pasteInto('<table id="paste_here"><tr><td>Hello</td></tr></table>',"paste_here");
+// verifyContent('');
+
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=478725">Mozilla Bug 478725</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug480647.html b/editor/libeditor/tests/test_bug480647.html
new file mode 100644
index 0000000000..33f088a1b1
--- /dev/null
+++ b/editor/libeditor/tests/test_bug480647.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=480647
+-->
+<title>Test for Bug 480647</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=480647">Mozilla Bug 480647</a>
+<div contenteditable></div>
+<script>
+/** Test for Bug 480647 **/
+
+var div = document.querySelector("div");
+
+function parseFontSize(input, expected) {
+ parseFontSizeInner(input, expected, is);
+}
+
+function parseFontSizeTodo(input, expected) {
+ parseFontSizeInner(input, expected, todo_is);
+}
+
+function parseFontSizeInner(input, expected, fn) {
+ div.innerHTML = "foo";
+ getSelection().selectAllChildren(div);
+ document.execCommand("fontSize", false, input);
+ if (expected === null) {
+ fn(div.innerHTML, "foo",
+ 'execCommand("fontSize", false, "' + input + '") should be no-op');
+ } else {
+ fn(div.innerHTML, '<font size="' + expected + '">foo</font>',
+ 'execCommand("fontSize", false, "' + input + '") should parse to ' +
+ expected);
+ }
+}
+
+// Parse errors
+parseFontSize("", null);
+parseFontSize("abc", null);
+parseFontSize("larger", null);
+parseFontSize("smaller", null);
+parseFontSize("xx-small", null);
+parseFontSize("x-small", null);
+parseFontSize("small", null);
+parseFontSize("medium", null);
+parseFontSize("large", null);
+parseFontSize("x-large", null);
+parseFontSize("xx-large", null);
+parseFontSize("xxx-large", null);
+// Bug 747879
+parseFontSizeTodo("1.2em", null);
+parseFontSizeTodo("8px", null);
+parseFontSizeTodo("-1.2em", null);
+parseFontSizeTodo("-8px", null);
+parseFontSizeTodo("+1.2em", null);
+parseFontSizeTodo("+8px", null);
+
+// Numbers
+parseFontSize("0", 1);
+parseFontSize("1", 1);
+parseFontSize("2", 2);
+parseFontSize("3", 3);
+parseFontSize("4", 4);
+parseFontSize("5", 5);
+parseFontSize("6", 6);
+parseFontSize("7", 7);
+parseFontSize("8", 7);
+parseFontSize("9", 7);
+parseFontSize("10", 7);
+parseFontSize("1000000000000000000000", 7);
+parseFontSize("2.72", 2);
+parseFontSize("2.72e9", 2);
+
+// Minus sign
+parseFontSize("-0", 3);
+parseFontSize("-1", 2);
+parseFontSize("-2", 1);
+parseFontSize("-3", 1);
+parseFontSize("-4", 1);
+parseFontSize("-5", 1);
+parseFontSize("-6", 1);
+parseFontSize("-7", 1);
+parseFontSize("-8", 1);
+parseFontSize("-9", 1);
+parseFontSize("-10", 1);
+parseFontSize("-1000000000000000000000", 1);
+parseFontSize("-1.72", 2);
+parseFontSize("-1.72e9", 2);
+
+// Plus sign
+parseFontSize("+0", 3);
+parseFontSize("+1", 4);
+parseFontSize("+2", 5);
+parseFontSize("+3", 6);
+parseFontSize("+4", 7);
+parseFontSize("+5", 7);
+parseFontSize("+6", 7);
+parseFontSize("+7", 7);
+parseFontSize("+8", 7);
+parseFontSize("+9", 7);
+parseFontSize("+10", 7);
+parseFontSize("+1000000000000000000000", 7);
+parseFontSize("+1.72", 4);
+parseFontSize("+1.72e9", 4);
+
+// Whitespace
+parseFontSize(" \t\n\r\f5 \t\n\r\f", 5);
+parseFontSize("\u00a05", null);
+parseFontSize("\b5", null);
+</script>
diff --git a/editor/libeditor/tests/test_bug480972.html b/editor/libeditor/tests/test_bug480972.html
new file mode 100644
index 0000000000..37037b756a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug480972.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 480972</title>
+<style src="/tests/SimpleTest/test.css" type="text/css"></style>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+function runTest() {
+ function verifyContent(s) {
+ var e = document.getElementById("i1");
+ var doc = e.contentDocument;
+ is(doc.body.innerHTML, s, "");
+ }
+
+ function pasteInto(html, target_id) {
+ var e = document.getElementById("i1");
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.body.innerHTML = html;
+ doc.defaultView.focus();
+ if (target_id)
+ e = doc.getElementById(target_id);
+ else
+ e = doc.body;
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(e);
+ selection.collapseToEnd();
+ SpecialPowers.wrap(doc).execCommand("paste", false, null);
+ return e;
+ }
+
+ function copyToClipBoard(s, asHTML, target_id) {
+ var e = document.getElementById("i2");
+ var doc = e.contentDocument;
+ if (asHTML) {
+ doc.body.innerHTML = s;
+ } else {
+ var text = doc.createTextNode(s);
+ doc.body.appendChild(text);
+ }
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ if (!target_id) {
+ selection.selectAllChildren(doc.body);
+ } else {
+ var range = document.createRange();
+ range.selectNode(doc.getElementById(target_id));
+ selection.addRange(range);
+ }
+ SpecialPowers.wrap(doc).execCommand("copy", false, null);
+ return e;
+ }
+
+ copyToClipBoard("<span>Hello</span><span>Kitty</span>", true);
+ pasteInto("");
+ verifyContent("<span>Hello</span><span>Kitty</span>");
+
+ copyToClipBoard("<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span>", true);
+ pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here");
+ verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span></li></ol>');
+
+// The following test doesn't do what I expected, because the special handling
+// of IsList nodes in nsHTMLEditor::InsertHTMLWithContext simply removes
+// non-list/item children. See bug 481177.
+// copyToClipBoard("<ol><li>Hello Kitty</li><span>Hello</span></ol>", true);
+// pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+// verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li><span>Hello</span></ol>');
+
+ copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true);
+ pasteInto('<pre id="paste_here">Hello </pre>', "paste_here");
+ verifyContent('<pre id="paste_here">Hello Kitty<span>Hello</span></pre>');
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=480972">Mozilla Bug 480972</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug483651.html b/editor/libeditor/tests/test_bug483651.html
new file mode 100644
index 0000000000..4ad570aa8b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug483651.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=483651
+-->
+
+<head>
+ <title>Test for Bug 483651</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=483651">Mozilla Bug 483651</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 483651 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function doTest() {
+ var t1 = $("t1");
+ var editor = SpecialPowers.wrap(t1).editor;
+
+ ok(editor, "able to get editor for the element");
+ t1.focus();
+ sendString("A");
+ synthesizeKey("KEY_Backspace");
+
+ try {
+ // Was the trailing br removed?
+ is(editor.documentIsEmpty, true, "trailing <br> correctly removed");
+ } catch (e) {
+ ok(false, "test failed with error " + e);
+ }
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <textarea id="t1" rows="2" columns="80"></textarea>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug489202.xhtml b/editor/libeditor/tests/test_bug489202.xhtml
new file mode 100644
index 0000000000..6b26573f69
--- /dev/null
+++ b/editor/libeditor/tests/test_bug489202.xhtml
@@ -0,0 +1,73 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=489202
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 489202" onload="runTest();">
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=489202"
+ target="_blank">Mozilla Bug 489202</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="i1"
+ type="content"
+ editortype="htmlmail"
+ style="width: 400px; height: 100px;"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function runTest() {
+ var trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ trans.addDataFlavor("text/html");
+ var test_data = '<meta/><a href="http://mozilla.org/">mozilla.org</a>';
+ var cstr = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ cstr.data = test_data;
+ trans.setTransferData("text/html", cstr);
+
+ window.docShell
+ .rootTreeItem
+ .QueryInterface(Ci.nsIDocShell)
+ .appType = Ci.nsIDocShell.APP_TYPE_EDITOR;
+ var e = document.getElementById('i1');
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.body.innerHTML = "";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(doc.body);
+ selection.collapseToEnd();
+
+ var point = doc.defaultView.getSelection().getRangeAt(0).startOffset;
+ ok(point==0, "Cursor should be at editor start before paste");
+
+ utils.sendContentCommandEvent("pasteTransferable", trans);
+
+ point = doc.defaultView.getSelection().getRangeAt(0).startOffset;
+ ok(point>0, "Cursor should not be at editor start after paste");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_bug490879.html b/editor/libeditor/tests/test_bug490879.html
new file mode 100644
index 0000000000..68458862db
--- /dev/null
+++ b/editor/libeditor/tests/test_bug490879.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<title>Mozilla Bug 490879</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=490879"
+ target="_blank">Mozilla Bug 490879</a>
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe>
+<img id="i" src="green.png">
+<script>
+async function runTest() {
+ function verifyContent() {
+ const kExpectedImgSpec = "data:image/png;base64,";
+ var e = document.getElementById("i1");
+ var doc = e.contentDocument;
+ is(doc.getElementsByTagName("img")[0].src.substring(0, kExpectedImgSpec.length),
+ kExpectedImgSpec, "The pasted image is a base64-encoded data: URI");
+ }
+
+ async function pasteInto() {
+ var e = document.getElementById("i1");
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(doc.body);
+ selection.collapseToEnd();
+
+ let input = new Promise(resolve => {
+ doc.body.addEventListener("input", resolve, {once: true})
+ });
+
+ SpecialPowers.doCommand(window, "cmd_paste");
+
+ info("Waiting for input event");
+ await input;
+ }
+
+ function copyToClipBoard() {
+ SpecialPowers.setCommandNode(window, document.getElementById("i"));
+ SpecialPowers.doCommand(window, "cmd_copyImageContents");
+ }
+
+ copyToClipBoard();
+
+ await pasteInto();
+
+ verifyContent();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
diff --git a/editor/libeditor/tests/test_bug502673.html b/editor/libeditor/tests/test_bug502673.html
new file mode 100644
index 0000000000..0850cf3de0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug502673.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=502673
+-->
+
+<head>
+ <title>Test for Bug 502673</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502673">Mozilla Bug 502673</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 502673 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function listener() {
+ }
+
+ listener.prototype =
+ {
+ NotifyDocumentWillBeDestroyed() {
+ var editor = SpecialPowers.wrap(this.input).editor;
+ editor.removeDocumentStateListener(this);
+ },
+
+ NotifyDocumentStateChanged(aNowDirty) {
+ var editor = SpecialPowers.wrap(this.input).editor;
+ editor.removeDocumentStateListener(this);
+ },
+
+ QueryInterface: SpecialPowers.wrapCallback(function(iid) {
+ if (iid.equals(SpecialPowers.Ci.nsIDocumentStateListener) ||
+ iid.equals(SpecialPowers.Ci.nsISupports))
+ return this;
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ }),
+ };
+
+ function doTest() {
+ var input = document.getElementById("ip");
+
+ // Add multiple listeners to the same editor
+ var editor = SpecialPowers.wrap(input).editor;
+ var listener1 = new listener();
+ listener1.input = input;
+ var listener2 = new listener();
+ listener2.input = input;
+ var listener3 = new listener();
+ listener3.input = input;
+ editor.addDocumentStateListener(listener1);
+ editor.addDocumentStateListener(listener2);
+ editor.addDocumentStateListener(listener3);
+
+ // Test 1. Fire NotifyDocumentStateChanged notifications where the
+ // listeners remove themselves
+ input.value = "mozilla";
+ editor.undo();
+
+ // Report success if we get here - clearly we didn't crash
+ ok(true, "Multiple listeners removed themselves after " +
+ "NotifyDocumentStateChanged notifications - didn't crash");
+
+ // Add the listeners again for the next test
+ editor.addDocumentStateListener(listener1);
+ editor.addDocumentStateListener(listener2);
+ editor.addDocumentStateListener(listener3);
+
+ // Test 2. Fire NotifyDocumentWillBeDestroyed notifications where the
+ // listeners remove themselves (though in the real world, listeners
+ // shouldn't do this as nsEditor::PreDestroy removes them as
+ // listeners anyway)
+ document.body.removeChild(input);
+ ok(true, "Multiple listeners removed themselves after " +
+ "NotifyDocumentWillBeDestroyed notifications - didn't crash");
+
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input type="text" id="ip" />
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug514156.html b/editor/libeditor/tests/test_bug514156.html
new file mode 100644
index 0000000000..1331d62b12
--- /dev/null
+++ b/editor/libeditor/tests/test_bug514156.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=514156
+-->
+<head>
+ <title>Test for Bug 514156</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="test()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=514156">Mozilla Bug 514156</a>
+<p id="display"></p>
+<div id="content">
+<input type="text" id="input1">
+<input type="text" id="input2">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 514156 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ var input1 = $("input1");
+ input1.focus();
+ sendString("\u200e\u05d0\u05d1");
+ is(escape(input1.value), escape("\u200e\u05d0\u05d1"), "non-spacing character and direction change shouldn't change content");
+
+ var input2 = $("input2");
+ input2.focus();
+ sendString("\u05b6");
+ sendString("abc");
+ is(escape(input2.value), escape("\u05b6abc"), "non-spacing character and direction change shouldn't change content");
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/editor/libeditor/tests/test_bug520189.html b/editor/libeditor/tests/test_bug520189.html
new file mode 100644
index 0000000000..0e8d286abc
--- /dev/null
+++ b/editor/libeditor/tests/test_bug520189.html
@@ -0,0 +1,618 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=520182
+-->
+<head>
+ <title>Test for Bug 520182</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=520182">Mozilla Bug 520182</a>
+<p id="display"></p>
+<div id="content">
+ <iframe id="a" src="about:blank"></iframe>
+ <iframe id="b" src="about:blank"></iframe>
+ <iframe id="c" src="about:blank"></iframe>
+ <div id="d" contenteditable="true"></div>
+ <div id="e" contenteditable="true"></div>
+ <div id="f" contenteditable="true"></div>
+ <iframe id="g" src="about:blank"></iframe>
+ <iframe id="h" src="about:blank"></iframe>
+ <div id="i" contenteditable="true"></div>
+ <div id="j" contenteditable="true"></div>
+ <iframe id="k" src="about:blank"></iframe>
+ <div id="l" contenteditable="true"></div>
+ <iframe id="m" src="about:blank"></iframe>
+ <div id="n" contenteditable="true"></div>
+ <iframe id="o" src="about:blank"></iframe>
+ <div id="p" contenteditable="true"></div>
+ <iframe id="q" src="about:blank"></iframe>
+ <div id="r" contenteditable="true"></div>
+ <iframe id="s" src="about:blank"></iframe>
+ <div id="t" contenteditable="true"></div>
+ <iframe id="u" src="about:blank"></iframe>
+ <div id="v" contenteditable="true"></div>
+ <iframe id="w" src="about:blank"></iframe>
+ <div id="x" contenteditable="true"></div>
+ <iframe id="y" src="about:blank"></iframe>
+ <div id="z" contenteditable="true"></div>
+ <iframe id="aa" src="about:blank"></iframe>
+ <div id="bb" contenteditable="true"></div>
+ <iframe id="cc" src="about:blank"></iframe>
+ <div id="dd" contenteditable="true"></div>
+ <iframe id="ee" src="about:blank"></iframe>
+ <div id="ff" contenteditable="true"></div>
+ <iframe id="gg" src="about:blank"></iframe>
+ <div id="hh" contenteditable="true"></div>
+ <iframe id="ii" src="about:blank"></iframe>
+ <div id="jj" contenteditable="true"></div>
+ <iframe id="kk" src="about:blank"></iframe>
+ <div id="ll" contenteditable="true"></div>
+ <iframe id="mm" src="about:blank"></iframe>
+ <div id="nn" contenteditable="true"></div>
+ <iframe id="oo" src="about:blank"></iframe>
+ <div id="pp" contenteditable="true"></div>
+ <iframe id="qq" src="about:blank"></iframe>
+ <div id="rr" contenteditable="true"></div>
+ <iframe id="ss" src="about:blank"></iframe>
+ <div id="tt" contenteditable="true"></div>
+ <iframe id="uu" src="about:blank"></iframe>
+ <div id="vv" contenteditable="true"></div>
+ <div id="sss" contenteditable="true"></div>
+ <iframe id="ssss" src="about:blank"></iframe>
+ <div id="ttt" contenteditable="true"></div>
+ <iframe id="tttt" src="about:blank"></iframe>
+ <div id="uuu" contenteditable="true"></div>
+ <iframe id="uuuu" src="about:blank"></iframe>
+ <div id="vvv" contenteditable="true"></div>
+ <iframe id="vvvv" src="about:blank"></iframe>
+ <div id="www" contenteditable="true"></div>
+ <iframe id="wwww" src="about:blank"></iframe>
+ <div id="xxx" contenteditable="true"></div>
+ <iframe id="xxxx" src="about:blank"></iframe>
+ <div id="yyy" contenteditable="true"></div>
+ <iframe id="yyyy" src="about:blank"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/* eslint-disable no-useless-concat */
+
+/** Test for Bug 520182 **/
+
+const dataPayload = "foo<iframe src=\"data:text/html,bar\"></iframe>baz";
+const jsPayload = "foo<iframe src=\"javascript:void('bar');\"></iframe>baz";
+const httpPayload = "foo<iframe src=\"http://mochi.test:8888/\"></iframe>baz";
+const scriptPayload = "foo<script>document.write(\"<iframe></iframe>\");</sc" + "ript>baz";
+const scriptExternalPayload = "foo<script src=\"data:text/javascript,document.write('<iframe></iframe>');\"></sc" + "ript>baz";
+const validStyle1Payload = "foo<style>#bar{color:red;}</style>baz";
+const validStyle2Payload = "foo<span style=\"color:red\">bar</span>baz";
+const validStyle3Payload = "foo<style>@font-face{font-family:xxx;src:'xxx.ttf';}</style>baz";
+const validStyle4Payload = "foo<style>@namespace xxx url(http://example.com/);</style>baz";
+const invalidStyle1Payload = "foo<style>#bar{-moz-binding:url('data:text/xml,<?xml version=\"1.0\"><binding xmlns=\"http://www.mozilla.org/xbl\"/>');}</style>baz";
+const invalidStyle2Payload = "foo<span style=\"-moz-binding:url('data:text/xml,<?xml version=&quot;1.0&quot;><binding xmlns=&quot;http://www.mozilla.org/xbl&quot;/>');\">bar</span>baz";
+const invalidStyle3Payload = "foo<style>@import 'xxx.css';</style>baz";
+const invalidStyle4Payload = "foo<span style=\"@import 'xxx.css';\">bar</span>baz";
+const invalidStyle5Payload = "foo<span style=\"@font-face{font-family:xxx;src:'xxx.ttf';}\">bar</span>baz";
+const invalidStyle6Payload = "foo<span style=\"@namespace xxx url(http://example.com/);\">bar</span>baz";
+const invalidStyle7Payload = "<html><head><title>xxx</title></head><body>foo</body></html>";
+const invalidStyle8Payload = "foo<style>@-moz-document url-prefix() {};</style>baz";
+const invalidStyle9Payload = "foo<style>@keyframes bar {};</style>baz";
+const nestedStylePayload = "foo<style>#bar1{-moz-binding:url('data:text/xml,<?xml version=&quot;1.0&quot;><binding xmlns=&quot;http://www.mozilla.org/xbl&quot; id=&quot;binding-1&quot;/>');<style></style>#bar2{-moz-binding:url('data:text/xml,<?xml version=&quot;1.0&quot;><binding xmlns=&quot;http://www.mozilla.org/xbl&quot; id=&quot;binding-2&quot;/>');</style>baz";
+const validImgSrc1Payload = "foo<img src=\"data:image/png,bar\">baz";
+const validImgSrc2Payload = "foo<img src=\"javascript:void('bar');\">baz";
+const validImgSrc3Payload = "foo<img src=\"file:///bar.png\">baz";
+const validDataFooPayload = "foo<span data-bar=\"value\">baz</span>";
+const validDataFoo2Payload = "foo<span _bar=\"value\">baz</span>";
+const svgPayload = "foo<svg><title>svgtitle</title></svg>bar";
+const svg2Payload = "foo<svg><bogussvg/></svg>bar";
+const mathPayload = "foo<math><bogusmath/></math>bar";
+const math2Payload = "foo<math><style>@import \"yyy.css\";</style</math>bar";
+const math3Payload = "foo<math><mi></mi></math>bar";
+const videoPayload = "foo<video></video>bar";
+const microdataPayload = "<head><meta name=foo content=bar><link rel=stylesheet href=url></head><body><meta itemprop=foo content=bar><link itemprop=bar href=url></body>";
+
+var tests = [
+ {
+ id: "a",
+ isIFrame: true,
+ payload: dataPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("a").contentDocument.documentElement; },
+ },
+ {
+ id: "b",
+ isIFrame: true,
+ payload: jsPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("b").contentDocument.documentElement; },
+ },
+ {
+ id: "c",
+ isIFrame: true,
+ payload: httpPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("c").contentDocument.documentElement; },
+ },
+ {
+ id: "g",
+ isIFrame: true,
+ payload: scriptPayload,
+ rootElement() { return document.getElementById("g").contentDocument.documentElement; },
+ iframeCount: 0,
+ },
+ {
+ id: "h",
+ isIFrame: true,
+ payload: scriptExternalPayload,
+ rootElement() { return document.getElementById("h").contentDocument.documentElement; },
+ iframeCount: 0,
+ },
+ {
+ id: "d",
+ payload: dataPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("d"); },
+ },
+ {
+ id: "e",
+ payload: jsPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("e"); },
+ },
+ {
+ id: "f",
+ payload: httpPayload,
+ iframeCount: 0,
+ rootElement() { return document.getElementById("f"); },
+ },
+ {
+ id: "i",
+ payload: scriptPayload,
+ rootElement() { return document.getElementById("i"); },
+ iframeCount: 0,
+ },
+ {
+ id: "j",
+ payload: scriptExternalPayload,
+ rootElement() { return document.getElementById("j"); },
+ iframeCount: 0,
+ },
+ {
+ id: "k",
+ isIFrame: true,
+ payload: validStyle1Payload,
+ rootElement() { return document.getElementById("k").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); },
+ },
+ {
+ id: "l",
+ payload: validStyle1Payload,
+ rootElement() { return document.getElementById("l"); },
+ checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); },
+ },
+ {
+ id: "m",
+ isIFrame: true,
+ payload: validStyle2Payload,
+ rootElement() { return document.getElementById("m").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); },
+ },
+ {
+ id: "n",
+ payload: validStyle2Payload,
+ rootElement() { return document.getElementById("n"); },
+ checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); },
+ },
+ {
+ id: "s",
+ isIFrame: true,
+ payload: invalidStyle1Payload,
+ rootElement() { return document.getElementById("s").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); },
+ },
+ {
+ id: "t",
+ payload: invalidStyle1Payload,
+ rootElement() { return document.getElementById("t"); },
+ checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); },
+ },
+ {
+ id: "u",
+ isIFrame: true,
+ payload: invalidStyle2Payload,
+ rootElement() { return document.getElementById("u").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); },
+ },
+ {
+ id: "v",
+ payload: invalidStyle2Payload,
+ rootElement() { return document.getElementById("v"); },
+ checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); },
+ },
+ {
+ id: "w",
+ isIFrame: true,
+ payload: validStyle3Payload,
+ rootElement() { return document.getElementById("w").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the font-face style"); },
+ },
+ {
+ id: "x",
+ payload: validStyle3Payload,
+ rootElement() { return document.getElementById("x"); },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the font-face style"); },
+ },
+ {
+ id: "y",
+ isIFrame: true,
+ payload: invalidStyle5Payload,
+ rootElement() { return document.getElementById("y").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the font-face style"); },
+ },
+ {
+ id: "z",
+ payload: invalidStyle5Payload,
+ rootElement() { return document.getElementById("z"); },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the font-face style"); },
+ },
+ {
+ id: "cc",
+ isIFrame: true,
+ payload: validStyle4Payload,
+ rootElement() { return document.getElementById("cc").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the namespace style"); },
+ },
+ {
+ id: "dd",
+ payload: validStyle4Payload,
+ rootElement() { return document.getElementById("dd"); },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the namespace style"); },
+ },
+ {
+ id: "ee",
+ isIFrame: true,
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("ee").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the namespace style"); },
+ },
+ {
+ id: "ff",
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("ff"); },
+ checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the namespace style"); },
+ },
+ {
+ id: "gg",
+ isIFrame: true,
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("gg").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "hh",
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("hh"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "ii",
+ isIFrame: true,
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("ii").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "jj",
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("jj"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "kk",
+ isIFrame: true,
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("kk").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "ll",
+ payload: invalidStyle6Payload,
+ rootElement() { return document.getElementById("ll"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); },
+ },
+ {
+ id: "mm",
+ isIFrame: true,
+ indirectPaste: true,
+ payload: invalidStyle7Payload,
+ rootElement() { return document.getElementById("mm").contentDocument.documentElement; },
+ checkResult(html) {
+ is(html.indexOf("xxx"), -1, "Should not have retained the title text");
+ isnot(html.indexOf("foo"), -1, "Should have retained the body text");
+ },
+ },
+ {
+ id: "nn",
+ indirectPaste: true,
+ payload: invalidStyle7Payload,
+ rootElement() { return document.getElementById("nn"); },
+ checkResult(html) {
+ is(html.indexOf("xxx"), -1, "Should not have retained the title text");
+ isnot(html.indexOf("foo"), -1, "Should have retained the body text");
+ },
+ },
+ {
+ id: "oo",
+ isIFrame: true,
+ payload: validDataFooPayload,
+ rootElement() { return document.getElementById("oo").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the data-bar attribute"); },
+ },
+ {
+ id: "pp",
+ payload: validDataFooPayload,
+ rootElement() { return document.getElementById("pp"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the data-bar attribute"); },
+ },
+ {
+ id: "qq",
+ isIFrame: true,
+ payload: validDataFoo2Payload,
+ rootElement() { return document.getElementById("qq").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the _bar attribute"); },
+ },
+ {
+ id: "rr",
+ payload: validDataFoo2Payload,
+ rootElement() { return document.getElementById("rr"); },
+ checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the _bar attribute"); },
+ },
+ {
+ id: "ss",
+ isIFrame: true,
+ payload: invalidStyle8Payload,
+ rootElement() { return document.getElementById("ss").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("@-moz-document"), -1, "Should not have retained the @-moz-document rule"); },
+ },
+ {
+ id: "tt",
+ payload: invalidStyle8Payload,
+ rootElement() { return document.getElementById("tt"); },
+ checkResult(html) { is(html.indexOf("@-moz-document"), -1, "Should not have retained the @-moz-document rule"); },
+ },
+ {
+ id: "uu",
+ isIFrame: true,
+ payload: invalidStyle9Payload,
+ rootElement() { return document.getElementById("uu").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("@keyframes"), -1, "Should not have retained the @keyframes rule"); },
+ },
+ {
+ id: "vv",
+ payload: invalidStyle9Payload,
+ rootElement() { return document.getElementById("vv"); },
+ checkResult(html) { is(html.indexOf("@keyframes"), -1, "Should not have retained the @keyframes rule"); },
+ },
+ {
+ id: "sss",
+ payload: svgPayload,
+ rootElement() { return document.getElementById("sss"); },
+ checkResult(html) { isnot(html.indexOf("svgtitle"), -1, "Should have retained SVG title"); },
+ },
+ {
+ id: "ssss",
+ isIFrame: true,
+ payload: svgPayload,
+ rootElement() { return document.getElementById("ssss").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("svgtitle"), -1, "Should have retained SVG title"); },
+ },
+ {
+ id: "ttt",
+ payload: svg2Payload,
+ rootElement() { return document.getElementById("ttt"); },
+ checkResult(html) { is(html.indexOf("bogussvg"), -1, "Should have dropped bogussvg element"); },
+ },
+ {
+ id: "tttt",
+ isIFrame: true,
+ payload: svg2Payload,
+ rootElement() { return document.getElementById("tttt").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("bogussvg"), -1, "Should have dropped bogussvg element"); },
+ },
+ {
+ id: "uuu",
+ payload: mathPayload,
+ rootElement() { return document.getElementById("uuu"); },
+ checkResult(html) { is(html.indexOf("bogusmath"), -1, "Should have dropped bogusmath element"); },
+ },
+ {
+ id: "uuuu",
+ isIFrame: true,
+ payload: mathPayload,
+ rootElement() { return document.getElementById("uuuu").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("bogusmath"), -1, "Should have dropped bogusmath element"); },
+ },
+ {
+ id: "vvv",
+ payload: math2Payload,
+ rootElement() { return document.getElementById("vvv"); },
+ checkResult(html) { is(html.indexOf("yyy.css"), -1, "Should have dropped MathML style element"); },
+ },
+ {
+ id: "vvvv",
+ isIFrame: true,
+ payload: math2Payload,
+ rootElement() { return document.getElementById("vvvv").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("yyy.css"), -1, "Should have dropped MathML style element"); },
+ },
+ {
+ id: "www",
+ payload: math3Payload,
+ rootElement() { return document.getElementById("www"); },
+ checkResult(html) { isnot(html.indexOf("<mi"), -1, "Should not have dropped MathML mi element"); },
+ },
+ {
+ id: "wwww",
+ isIFrame: true,
+ payload: math3Payload,
+ rootElement() { return document.getElementById("wwww").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("<mi"), -1, "Should not have dropped MathML mi element"); },
+ },
+ {
+ id: "xxx",
+ payload: videoPayload,
+ rootElement() { return document.getElementById("xxx"); },
+ checkResult(html) { isnot(html.indexOf("controls="), -1, "Should have added the controls attribute"); },
+ },
+ {
+ id: "xxxx",
+ isIFrame: true,
+ payload: videoPayload,
+ rootElement() { return document.getElementById("xxxx").contentDocument.documentElement; },
+ checkResult(html) { isnot(html.indexOf("controls="), -1, "Should have added the controls attribute"); },
+ },
+ {
+ id: "yyy",
+ payload: microdataPayload,
+ rootElement() { return document.getElementById("yyy"); },
+ checkResult(html) { is(html.indexOf("name"), -1, "Should have dropped name."); is(html.indexOf("rel"), -1, "Should have dropped rel."); isnot(html.indexOf("itemprop"), -1, "Should not have dropped itemprop."); },
+ },
+ {
+ id: "yyyy",
+ isIFrame: true,
+ payload: microdataPayload,
+ rootElement() { return document.getElementById("yyyy").contentDocument.documentElement; },
+ checkResult(html) { is(html.indexOf("name"), -1, "Should have dropped name."); is(html.indexOf("rel"), -1, "Should have dropped rel."); isnot(html.indexOf("itemprop"), -1, "Should not have dropped itemprop."); },
+ },
+];
+
+function doNextTest() {
+ /* global testCounter:true */
+ if (typeof testCounter == "undefined") {
+ testCounter = 0;
+ } else if (++testCounter == tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ runTest(tests[testCounter]);
+
+ doNextTest();
+}
+
+function getLoadContext() {
+ const Ci = SpecialPowers.Ci;
+ return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function runTest(test) {
+ var elem = document.getElementById(test.id);
+ if ("isIFrame" in test) {
+ elem.contentDocument.designMode = "on";
+ elem.contentWindow.focus();
+ } else {
+ elem.focus();
+ }
+
+ var trans = SpecialPowers.Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(SpecialPowers.Ci.nsITransferable);
+ trans.init(getLoadContext());
+ var data = SpecialPowers.Cc["@mozilla.org/supports-string;1"]
+ .createInstance(SpecialPowers.Ci.nsISupportsString);
+ data.data = test.payload;
+ trans.addDataFlavor("text/html");
+ trans.setTransferData("text/html", data);
+
+ if ("indirectPaste" in test) {
+ var editor, win;
+ if ("isIFrame" in test) {
+ win = elem.contentDocument.defaultView;
+ } else {
+ getSelection().collapse(elem, 0);
+ win = window;
+ }
+ editor = SpecialPowers.wrap(win).docShell.editor;
+ let beforeInputEvent = null;
+ let inputEvent = null;
+ let selectionRanges = [];
+ win.addEventListener("beforeinput", aEvent => {
+ beforeInputEvent = aEvent;
+ selectionRanges = [];
+ let selection = win.getSelection();
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push(
+ new win.StaticRange({startContainer: range.startContainer,
+ startOffset: range.startOffset,
+ endContainer: range.endContainer,
+ endOffset: range.endOffset}));
+ }
+ }, {once: true});
+ win.addEventListener("input", aEvent => { inputEvent = aEvent; }, {once: true});
+ editor.pasteTransferable(trans);
+ isnot(beforeInputEvent, null, '"beforeinput" event should be fired');
+ if (beforeInputEvent) {
+ is(beforeInputEvent.cancelable, true, '"beforeinput" event for "insertFromPaste" should be cancelable');
+ is(beforeInputEvent.inputType, "insertFromPaste", `inputType of "beforeinput" event should be "insertFromPaste"`);
+ is(beforeInputEvent.data, null, 'data of "beforeinput" event should be null');
+ is(beforeInputEvent.dataTransfer.getData("text/html"), test.payload, 'dataTransfer of "beforeinput" event should have the HTML data');
+ is(beforeInputEvent.dataTransfer.getData("text/plain"), "", 'dataTransfer of "beforeinput" event should not have have plain text');
+ let targetRanges = beforeInputEvent.getTargetRanges();
+ is(targetRanges.length, selectionRanges.length, 'getTargetRanges() of "beforeinput" event should return selection ranges');
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match`);
+ }
+ }
+ }
+ is(inputEvent.type, "input", '"input" event should be fired');
+ is(inputEvent.inputType, "insertFromPaste", `inputType of "input" event should be "insertFromPaste"`);
+ is(inputEvent.data, null, 'data of "input" event should be null');
+ is(inputEvent.dataTransfer.getData("text/html"), test.payload, 'dataTransfer of "input" event should have the HTML data');
+ is(inputEvent.dataTransfer.getData("text/plain"), "", 'dataTransfer of "input" event should not have have plain text');
+ is(inputEvent.getTargetRanges().length, 0, 'getTargetRanges() of "input" event should return empty array');
+ } else {
+ var clipboard = SpecialPowers.Services.clipboard;
+
+ clipboard.setData(trans, null, SpecialPowers.Ci.nsIClipboard.kGlobalClipboard);
+
+ synthesizeKey("V", {accelKey: true});
+ }
+
+ if ("checkResult" in test) {
+ if ("isIFrame" in test) {
+ test.checkResult(elem.contentDocument.documentElement.innerHTML,
+ elem.contentDocument.documentElement.textContent);
+ } else {
+ test.checkResult(elem.innerHTML, elem.textContent);
+ }
+ } else {
+ var iframes = test.rootElement().querySelectorAll("iframe");
+ var expectedIFrameCount = ("iframeCount" in test) ? test.iframeCount : 1;
+ is(iframes.length, expectedIFrameCount, "Only " + expectedIFrameCount + " iframe should be pasted");
+ if (expectedIFrameCount > 0) {
+ ok(!iframes[0].hasAttribute("src"), "iframe should not have a src attrib");
+ }
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ doNextTest();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug525389.html b/editor/libeditor/tests/test_bug525389.html
new file mode 100644
index 0000000000..500720d92a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug525389.html
@@ -0,0 +1,207 @@
+<!DOCTYPE HTML>
+<html><head>
+<title>Test for bug 525389</title>
+<style src="/tests/SimpleTest/test.css" type="text/css"></style>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var Cc = SpecialPowers.Cc;
+ var Ci = SpecialPowers.Ci;
+
+function getLoadContext() {
+ return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+async function runTest() {
+ var pasteCount = 0;
+ var pasteFunc = function(event) { pasteCount++; };
+
+ function verifyContent(s) {
+ var e = document.getElementById("i1");
+ var doc = e.contentDocument;
+ if (navigator.platform.includes("Win")) {
+ // On Windows ignore \n which got left over from the removal of the fragment tags
+ // <html><body>\n<!--StartFragment--> and <!--EndFragment-->\n</body>\n</html>.
+ is(doc.body.innerHTML.replace(/\n/g, ""), s, "");
+ } else {
+ is(doc.body.innerHTML, s, "");
+ }
+ }
+
+ function pasteInto(trans, html, target_id) {
+ var e = document.getElementById("i1");
+ var doc = e.contentDocument;
+ doc.designMode = "on";
+ doc.body.innerHTML = html;
+ doc.defaultView.focus();
+ if (target_id)
+ e = doc.getElementById(target_id);
+ else
+ e = doc.body;
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ selection.selectAllChildren(e);
+ selection.collapseToEnd();
+
+ pasteCount = 0;
+ e.addEventListener("paste", pasteFunc);
+ utils.sendContentCommandEvent("pasteTransferable", trans);
+ e.removeEventListener("paste", pasteFunc);
+
+ return e;
+ }
+
+ function getTransferableFromClipboard(asHTML) {
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ if (asHTML) {
+ trans.addDataFlavor("text/html");
+ } else {
+ trans.addDataFlavor("text/plain");
+ }
+ var clip = SpecialPowers.Services.clipboard;
+ clip.getData(trans, Ci.nsIClipboard.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext);
+ return trans;
+ }
+
+ // Commented out as the test for it below is also commented out.
+ // function makeTransferable(s, asHTML, target_id) {
+ // var e = document.getElementById("i2");
+ // var doc = e.contentDocument;
+ // if (asHTML) {
+ // doc.body.innerHTML = s;
+ // } else {
+ // var text = doc.createTextNode(s);
+ // doc.body.appendChild(text);
+ // }
+ // doc.designMode = "on";
+ // doc.defaultView.focus();
+ // var selection = doc.defaultView.getSelection();
+ // selection.removeAllRanges();
+ // if (!target_id) {
+ // selection.selectAllChildren(doc.body);
+ // } else {
+ // var range = document.createRange();
+ // range.selectNode(doc.getElementById(target_id));
+ // selection.addRange(range);
+ // }
+ //
+ // // We cannot use plain strings, we have to use nsSupportsString.
+ // var supportsStringClass = SpecialPowers.Components.classes["@mozilla.org/supports-string;1"];
+ // var ssData = supportsStringClass.createInstance(Ci.nsISupportsString);
+ //
+ // // Create the transferable.
+ // var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+ // trans.init(getLoadContext());
+ //
+ // // Add the data to the transferable.
+ // if (asHTML) {
+ // trans.addDataFlavor("text/html");
+ // ssData.data = doc.body.innerHTML;
+ // trans.setTransferData("text/html", ssData);
+ // } else {
+ // trans.addDataFlavor("text/plain");
+ // ssData.data = doc.body.innerHTML;
+ // trans.setTransferData("text/plain", ssData);
+ // }
+ //
+ // return trans;
+ // }
+
+ async function copyToClipBoard(s, asHTML, target_id) {
+ var e = document.getElementById("i2");
+ var doc = e.contentDocument;
+ if (asHTML) {
+ doc.body.innerHTML = s;
+ } else {
+ var text = doc.createTextNode(s);
+ doc.body.appendChild(text);
+ }
+ doc.designMode = "on";
+ doc.defaultView.focus();
+ var selection = doc.defaultView.getSelection();
+ selection.removeAllRanges();
+ if (!target_id) {
+ selection.selectAllChildren(doc.body);
+ } else {
+ var range = document.createRange();
+ range.selectNode(doc.getElementById(target_id));
+ selection.addRange(range);
+ }
+
+ await SimpleTest.promiseClipboardChange(() => true,
+ () => { SpecialPowers.wrap(doc).execCommand("copy", false, null); });
+
+ return e;
+ }
+
+ await copyToClipBoard("<span>Hello</span><span>Kitty</span>", true);
+ var trans = getTransferableFromClipboard(true);
+ pasteInto(trans, "");
+ verifyContent("<span>Hello</span><span>Kitty</span>");
+ is(pasteCount, 1, "paste event was not triggered");
+
+ // this test is not working out exactly like the clipboard test
+ // has to do with generating the nsITransferable above
+ // trans = makeTransferable('<span>Hello</span><span>Kitty</span>', true);
+ // pasteInto(trans, '');
+ // verifyContent('<span>Hello</span><span>Kitty</span>');
+
+ await copyToClipBoard("<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span>", true);
+ trans = getTransferableFromClipboard(true);
+ pasteInto(trans, '<ol><li id="paste_here">X</li></ol>', "paste_here");
+ verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span></li></ol>');
+ is(pasteCount, 1, "paste event was not triggered");
+
+// The following test doesn't do what I expected, because the special handling
+// of IsList nodes in nsHTMLEditor::InsertHTMLWithContext simply removes
+// non-list/item children. See bug 481177.
+// await copyToClipBoard("<ol><li>Hello Kitty</li><span>Hello</span></ol>", true);
+// pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here");
+// verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li><span>Hello</span></ol>');
+
+ await copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true);
+ trans = getTransferableFromClipboard(true);
+ pasteInto(trans, '<pre id="paste_here">Hello </pre>', "paste_here");
+ verifyContent('<pre id="paste_here">Hello Kitty<span>Hello</span></pre>');
+ is(pasteCount, 1, "paste event was not triggered");
+
+ await copyToClipBoard('1<span style="display: contents">2</span>3', true);
+ trans = getTransferableFromClipboard(true);
+ pasteInto(trans, '<div id="paste_here"></div>', "paste_here");
+ verifyContent('<div id="paste_here">1<span style="display: contents">2</span>3</div>');
+ is(pasteCount, 1, "paste event was not triggered");
+
+ // test that we can preventDefault pastes
+ pasteFunc = function(event) { event.preventDefault(); return false; };
+ await copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true);
+ trans = getTransferableFromClipboard(true);
+ pasteInto(trans, '<pre id="paste_here">Hello </pre>', "paste_here");
+ verifyContent('<pre id="paste_here">Hello </pre>');
+ is(pasteCount, 0, "paste event was triggered");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(() => {
+ add_task(async function test_copy() {
+ await runTest();
+ });
+});
+
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=525389">Mozilla Bug 525389</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br>
+<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug537046.html b/editor/libeditor/tests/test_bug537046.html
new file mode 100644
index 0000000000..746aba6edc
--- /dev/null
+++ b/editor/libeditor/tests/test_bug537046.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=537046
+-->
+<head>
+ <title>Test for Bug 537046</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=537046">Mozilla Bug 537046</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor" contenteditable="true">
+ Some editable content
+ </div>
+ <div id="source" contenteditable="true">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 537046 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var ed = document.getElementById("editor");
+ var src = document.getElementById("source");
+ ed.addEventListener("DOMSubtreeModified", function() {
+ src.textContent = ed.innerHTML;
+ });
+ src.addEventListener("DOMSubtreeModified", function() {
+ ed.innerHTML = ed.textContent;
+ });
+
+ // Simulate pressing Enter twice
+ ed.focus();
+ synthesizeKey("KEY_Enter");
+ synthesizeKey("KEY_Enter");
+
+ ok(true, "Didn't crash!");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug549262.html b/editor/libeditor/tests/test_bug549262.html
new file mode 100644
index 0000000000..a6848e3a66
--- /dev/null
+++ b/editor/libeditor/tests/test_bug549262.html
@@ -0,0 +1,160 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=549262
+-->
+<head>
+ <title>Test for Bug 549262</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549262">Mozilla Bug 549262</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 549262 **/
+
+var smoothScrollPref = "general.smoothScroll";
+SimpleTest.waitForExplicitFinish();
+var win = window.open("file_bug549262.html", "_blank",
+ "width=600,height=600,scrollbars=yes");
+
+function waitForScrollEvent(aWindow) {
+ return new Promise(resolve => {
+ aWindow.addEventListener("scroll", () => { SimpleTest.executeSoon(resolve); }, {once: true, capture: true});
+ });
+}
+
+function waitAndCheckNoScrollEvent(aWindow) {
+ let gotScroll = false;
+ function recordScroll() {
+ gotScroll = true;
+ }
+ aWindow.addEventListener("scroll", recordScroll, {capture: true});
+ return waitToClearOutAnyPotentialScrolls(aWindow).then(function() {
+ aWindow.removeEventListener("scroll", recordScroll, {capture: true});
+ is(gotScroll, false, "check that we didn't get a scroll");
+ });
+}
+
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set": [[smoothScrollPref, false]]}, startTest);
+}, win);
+async function startTest() {
+ // Make sure that pressing Space when a contenteditable element is not focused
+ // will scroll the page.
+ var ed = win.document.getElementById("editor");
+ var sc = win.document.querySelector("a");
+ sc.focus();
+ await waitToClearOutAnyPotentialScrolls(win);
+ is(win.scrollY, 0, "Sanity check");
+ let waitForScrolling = waitForScrollEvent(win);
+ synthesizeKey(" ", {}, win);
+
+ await waitForScrolling;
+
+ isnot(win.scrollY, 0, "Page is scrolled down");
+ is(ed.textContent, "abc", "The content of the editable element has not changed");
+ var oldY = win.scrollY;
+ waitForScrolling = waitForScrollEvent(win);
+ synthesizeKey(" ", {shiftKey: true}, win);
+
+ await waitForScrolling;
+
+ ok(win.scrollY < oldY, "Page is scrolled up");
+ is(ed.textContent, "abc", "The content of the editable element has not changed");
+
+ // Make sure that pressing Space when a contenteditable element is focused
+ // will not scroll the page, and will edit the element.
+ ed.focus();
+ win.getSelection().collapse(ed.firstChild, 1);
+ await waitToClearOutAnyPotentialScrolls(win);
+ oldY = win.scrollY;
+ let waitForNoScroll = waitAndCheckNoScrollEvent(win);
+ synthesizeKey(" ", {}, win);
+
+ await waitForNoScroll;
+
+ ok(win.scrollY <= oldY, "Page is not scrolled down");
+ is(ed.textContent, "a bc", "The content of the editable element has changed");
+ sc.focus();
+ await waitToClearOutAnyPotentialScrolls(win);
+ waitForScrolling = waitForScrollEvent(win);
+ synthesizeKey(" ", {}, win);
+
+ await waitForScrolling;
+
+ isnot(win.scrollY, 0, "Page is scrolled down");
+ is(ed.textContent, "a bc", "The content of the editable element has not changed");
+ ed.focus();
+ win.getSelection().collapse(ed.firstChild, 3);
+ await waitToClearOutAnyPotentialScrolls(win);
+ waitForNoScroll = waitAndCheckNoScrollEvent(win);
+ synthesizeKey(" ", {shiftKey: true}, win);
+
+ await waitForNoScroll;
+
+ isnot(win.scrollY, 0, "Page is not scrolled up");
+ is(ed.textContent, "a b c", "The content of the editable element has changed");
+
+ // Now let's test the down/up keys
+ sc = document.body;
+
+ ed.blur();
+ sc.focus();
+ await waitToClearOutAnyPotentialScrolls(win);
+ oldY = win.scrollY;
+ waitForScrolling = waitForScrollEvent(win);
+ synthesizeKey("VK_UP", {}, win);
+
+ await waitForScrolling;
+
+ ok(win.scrollY < oldY, "Page is scrolled up");
+ oldY = win.scrollY;
+ ed.focus();
+ win.getSelection().collapse(ed.firstChild, 3);
+ await waitToClearOutAnyPotentialScrolls(win);
+ waitForNoScroll = waitAndCheckNoScrollEvent(win);
+ synthesizeKey("VK_UP", {}, win);
+
+ await waitForNoScroll;
+
+ is(win.scrollY, oldY, "Page is not scrolled up");
+ is(win.getSelection().focusNode, ed.firstChild, "Correct element selected");
+ is(win.getSelection().focusOffset, 0, "Selection should be moved to the beginning");
+ win.getSelection().removeAllRanges();
+ await waitToClearOutAnyPotentialScrolls(win);
+ waitForScrolling = waitForScrollEvent(win);
+ synthesizeMouse(sc, 300, 300, {}, win);
+ synthesizeKey("VK_DOWN", {}, win);
+
+ await waitForScrolling;
+
+ ok(win.scrollY > oldY, "Page is scrolled down");
+ ed.focus();
+ win.getSelection().collapse(ed.firstChild, 3);
+ await waitToClearOutAnyPotentialScrolls(win);
+ oldY = win.scrollY;
+ waitForNoScroll = waitAndCheckNoScrollEvent(win);
+ synthesizeKey("VK_DOWN", {}, win);
+
+ await waitForNoScroll;
+
+ is(win.scrollY, oldY, "Page is not scrolled down");
+ is(win.getSelection().focusNode, ed.firstChild, "Correct element selected");
+ is(win.getSelection().focusOffset, ed.textContent.length, "Selection should be moved to the end");
+
+ win.close();
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug550434.html b/editor/libeditor/tests/test_bug550434.html
new file mode 100644
index 0000000000..2018701f6d
--- /dev/null
+++ b/editor/libeditor/tests/test_bug550434.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=550434
+-->
+<head>
+ <title>Test for Bug 550434</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=550434">Mozilla Bug 550434</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor" contenteditable="true"
+ style="height: 250px; height: 200px; border: 4px solid red; outline: none;"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 550434 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var ed = document.getElementById("editor");
+
+ // Simulate click twice
+ synthesizeMouse(ed, 10, 10, {});
+ synthesizeMouse(ed, 50, 50, {});
+ setTimeout(function() {
+ sendString("x");
+
+ is(ed.innerHTML, "x", "Editor should work after being clicked twice");
+ SimpleTest.finish();
+ }, 0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug551704.html b/editor/libeditor/tests/test_bug551704.html
new file mode 100644
index 0000000000..ad63904f66
--- /dev/null
+++ b/editor/libeditor/tests/test_bug551704.html
@@ -0,0 +1,126 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=551704
+-->
+<head>
+ <title>Test for Bug 551704</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=551704">Mozilla Bug 551704</a>
+<p id="display"></p>
+<div id="content">
+ <div id="preformatted" style="white-space: pre" contenteditable>a&#10;b</div>
+ <div id="test1" contenteditable><br></div>
+ <div id="test2" contenteditable>a<br></div>
+ <div id="test3" contenteditable style="white-space: pre"><br></div>
+ <div id="test4" contenteditable style="white-space: pre">a<br></div>
+ <div id="test5" contenteditable></div>
+ <div id="test6" contenteditable>a</div>
+ <div id="test7" contenteditable style="white-space: pre"></div>
+ <div id="test8" contenteditable style="white-space: pre">a</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+function testLineBreak(div, type, expectedText, expectedHTML, callback) {
+ div.focus();
+ getSelection().collapse(div, 0);
+ type();
+ is(div.innerHTML, expectedHTML, "The expected HTML after editing should be correct");
+ requestAnimationFrame(function() {
+ SimpleTest.waitForClipboard(expectedText,
+ function() {
+ getSelection().selectAllChildren(div);
+ synthesizeKey("C", {accelKey: true});
+ },
+ function() {
+ var t = document.createElement("textarea");
+ document.body.appendChild(t);
+ t.focus();
+ synthesizeKey("V", {accelKey: true});
+ is(t.value, expectedText, "The expected text should be copied to the clipboard");
+ callback();
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+ });
+}
+
+function typeABCDEF() {
+ sendString("a");
+ typeBCDEF_chars();
+}
+
+function typeBCDEF() {
+ synthesizeKey("KEY_ArrowRight");
+ typeBCDEF_chars();
+}
+
+function typeBCDEF_chars() {
+ sendString("bc");
+ synthesizeKey("KEY_Enter");
+ sendString("def");
+}
+
+/** Test for Bug 551704 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ document.execCommand("defaultParagraphSeparator", false, "div");
+
+ var preformatted = document.getElementById("preformatted");
+ is(preformatted.innerHTML, "a\nb", "No BR node should be injected for preformatted editable fields");
+
+ var iframe = document.createElement("iframe");
+ iframe.addEventListener("load", function() {
+ var sel = iframe.contentWindow.getSelection();
+ is(sel.rangeCount, 0, "There should be no range in the selection initially");
+ iframe.contentDocument.designMode = "on";
+ sel = iframe.contentWindow.getSelection();
+ is(sel.rangeCount, 1, "There should be a single range in the selection after setting designMode");
+ var range = sel.getRangeAt(0);
+ ok(range.collapsed, "The range should be collapsed");
+ is(range.startContainer, iframe.contentDocument.body.firstChild, "The range should start on the text");
+ is(range.startOffset, 0, "The start offset should be zero");
+
+ continueTest();
+ });
+ iframe.srcdoc = "foo";
+ document.getElementById("content").appendChild(iframe);
+});
+
+function continueTest() {
+ var divs = [];
+ for (var i = 0; i < 8; ++i) {
+ divs[i] = document.getElementById("test" + (i + 1));
+ }
+ var current = 0;
+ function doNextTest() {
+ if (current == divs.length) {
+ SimpleTest.finish();
+ return;
+ }
+ var div = divs[current++];
+ let type;
+ if (div.textContent == "a") {
+ type = typeBCDEF;
+ } else {
+ type = typeABCDEF;
+ }
+ var expectedHTML = "<div>abc</div><div>def<br></div>";
+ var expectedText = "abc\ndef";
+ testLineBreak(div, type, expectedText, expectedHTML, doNextTest);
+ }
+
+ doNextTest();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug552782.html b/editor/libeditor/tests/test_bug552782.html
new file mode 100644
index 0000000000..66a70cae72
--- /dev/null
+++ b/editor/libeditor/tests/test_bug552782.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=552782
+-->
+<head>
+ <title>Test for Bug 552782</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=290026">Mozilla Bug 552782</a>
+<p id="display"></p>
+<div id="editor" contenteditable></div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 552782 **/
+SimpleTest.waitForExplicitFinish();
+
+var original = "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li><li>Item 4</li></ol></ol>";
+var editor = document.getElementById("editor");
+editor.innerHTML = original;
+editor.focus();
+
+addLoadEvent(function() {
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ var lis = document.getElementsByTagName("li");
+ sel.selectAllChildren(lis[2]);
+ document.execCommand("outdent", false, false);
+ var expected = "<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li><ol><li>Item 4</li></ol></ol>";
+ is(editor.innerHTML, expected, "outdenting third item in a partially indented numbered list");
+ document.execCommand("indent", false, false);
+ todo_is(editor.innerHTML, original, "re-indenting third item in a partially indented numbered list");
+
+ // done
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug567213.html b/editor/libeditor/tests/test_bug567213.html
new file mode 100644
index 0000000000..cfad69dff7
--- /dev/null
+++ b/editor/libeditor/tests/test_bug567213.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=567213
+-->
+
+<head>
+ <title>Test for Bug 567213</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567213">Mozilla Bug 567213</a>
+ <p id="display"></p>
+ <div id="content">
+ <div id="target" contenteditable="true">test</div>
+ <button id="thief">theif</button>
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 567213 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ var target = document.getElementById("target");
+ var thief = document.getElementById("thief");
+ var sel = window.getSelection();
+
+ // select the contents of the editable area
+ sel.removeAllRanges();
+ sel.selectAllChildren(target);
+ target.focus();
+
+ // press some key
+ sendString("X");
+ is(target.textContent, "X", "Text input should work (sanity check)");
+
+ // select the contents of the editable area again
+ sel.removeAllRanges();
+ sel.selectAllChildren(target);
+ thief.focus();
+
+ // press some key with the thief having focus
+ sendString("Y");
+ is(target.textContent, "X", "Text entry should not work with another element focused");
+
+ SimpleTest.finish();
+ });
+
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug569988.html b/editor/libeditor/tests/test_bug569988.html
new file mode 100644
index 0000000000..c4d4b040ba
--- /dev/null
+++ b/editor/libeditor/tests/test_bug569988.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=569988
+-->
+<head>
+ <title>Test for Bug 569988</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=569988">Mozilla Bug 569988</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 569988 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+
+function runTest() {
+ var script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ var gPromptInput = null;
+ var os = Services.obs;
+
+ os.addObserver(onPromptLoad, "common-dialog-loaded");
+ os.addObserver(onPromptLoad, "tabmodal-dialog-loaded");
+
+ function onPromptLoad(subject, topic, data) {
+ let ui = subject.Dialog ? subject.Dialog.ui : undefined;
+ if (!ui) {
+ // subject is an tab prompt, find the elements ourselves
+ ui = {
+ loginTextbox: subject.querySelector(".tabmodalprompt-loginTextbox"),
+ button0: subject.querySelector(".tabmodalprompt-button0"),
+ };
+ }
+ sendAsyncMessage("ok", [true, "onPromptLoad is called"]);
+ gPromptInput = ui.loginTextbox;
+ gPromptInput.addEventListener("focus", onPromptFocus);
+ // shift focus to ensure it fires.
+ ui.button0.focus();
+ gPromptInput.focus();
+ }
+
+ function onPromptFocus() {
+ sendAsyncMessage("ok", [true, "onPromptFocus is called"]);
+ gPromptInput.removeEventListener("focus", onPromptFocus);
+
+ var listenerService = Services.els;
+
+ var listener = {
+ handleEvent: function _hv(aEvent) {
+ var isPrevented = aEvent.defaultPrevented;
+ sendAsyncMessage("ok", [!isPrevented,
+ "ESC key event is prevented by editor"]);
+ listenerService.removeSystemEventListener(gPromptInput, "keypress",
+ listener, false);
+ },
+ };
+ listenerService.addSystemEventListener(gPromptInput, "keypress",
+ listener, false);
+
+ sendAsyncMessage("info", "sending key");
+ var EventUtils = {};
+ EventUtils.window = {};
+ EventUtils._EU_Ci = Ci;
+ EventUtils._EU_Cc = Cc;
+ Services.scriptloader
+ .loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils);
+ EventUtils.synthesizeKey("VK_ESCAPE", {},
+ gPromptInput.ownerGlobal);
+ }
+
+ addMessageListener("destroy", function() {
+ os.removeObserver(onPromptLoad, "tabmodal-dialog-loaded");
+ os.removeObserver(onPromptLoad, "common-dialog-loaded");
+ });
+ });
+ script.addMessageListener("ok", ([val, msg]) => ok(val, msg));
+ script.addMessageListener("info", msg => info(msg));
+
+ info("opening prompt...");
+ prompt("summary", "text");
+ info("prompt is closed");
+
+ script.sendAsyncMessage("destroy");
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug570144.html b/editor/libeditor/tests/test_bug570144.html
new file mode 100644
index 0000000000..9ea4cd1d6e
--- /dev/null
+++ b/editor/libeditor/tests/test_bug570144.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=570144
+-->
+<head>
+ <title>Test for Bug 570144</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=570144">Mozilla Bug 570144</a>
+<p id="display"></p>
+<div id="content">
+ <!-- editable paragraphs in list item -->
+ <section id="test1">
+ <ol>
+ <li><p contenteditable>foo</p></li>
+ </ol>
+ <ul>
+ <li><p contenteditable>foo</p></li>
+ </ul>
+ <dl>
+ <dt>foo</dt>
+ <dd><p contenteditable>bar</p></dd>
+ </dl>
+ </section>
+ <!-- paragraphs in editable list item -->
+ <section id="test2">
+ <ol>
+ <li contenteditable><p>foo</p></li>
+ </ol>
+ <ul>
+ <li contenteditable><p>foo</p></li>
+ </ul>
+ <dl>
+ <dt>foo</dt>
+ <dd contenteditable><p>bar</p></dd>
+ </dl>
+ </section>
+ <!-- paragraphs in editable list -->
+ <section id="test3">
+ <ol contenteditable>
+ <li><p>foo</p></li>
+ </ol>
+ <ul contenteditable>
+ <li><p>foo</p></li>
+ </ul>
+ <dl contenteditable>
+ <dt>foo</dt>
+ <dd><p>bar</p></dd>
+ </dl>
+ </section>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 570144 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function try2split(list) {
+ var editor = list.hasAttribute("contenteditable")
+ ? list : list.querySelector("*[contenteditable]");
+ editor.focus();
+ // put the caret at the end of the paragraph
+ var selection = window.getSelection();
+ if (editor.nodeName.toLowerCase() == "p")
+ selection.selectAllChildren(editor);
+ else
+ selection.selectAllChildren(editor.querySelector("p"));
+ selection.collapseToEnd();
+ // simulate a [Enter] keypress
+ synthesizeKey("KEY_Enter");
+}
+
+function testSection(element, context, shouldCreateLI, shouldCreateP) {
+ var nbLI = shouldCreateLI ? 2 : 1; // number of expected list items
+ var nbP = shouldCreateP ? 2 : 1; // number of expected paragraphs
+
+ function message(nodeName, dup) {
+ return context + ":[Return] should " + (dup ? "" : "not ")
+ + "create another <" + nodeName + ">.";
+ }
+ var msgP = message("p", shouldCreateP);
+ var msgLI = message("li", shouldCreateLI);
+ var msgDT = message("dt", shouldCreateLI);
+ var msgDD = message("dd", false);
+
+ const ol = element.querySelector("ol");
+ try2split(ol);
+ is(ol.querySelectorAll("li").length, nbLI, msgLI);
+ is(ol.querySelectorAll("p").length, nbP, msgP);
+
+ const ul = element.querySelector("ul");
+ try2split(ul);
+ is(ul.querySelectorAll("li").length, nbLI, msgLI);
+ is(ul.querySelectorAll("p").length, nbP, msgP);
+
+ const dl = element.querySelector("dl");
+ try2split(dl);
+ is(dl.querySelectorAll("dt").length, nbLI, msgDT);
+ is(dl.querySelectorAll("dd").length, 1, msgDD);
+ is(dl.querySelectorAll("p").length, nbP, msgP);
+}
+
+function runTests() {
+ testSection(document.getElementById("test1"), "editable paragraph in list item", false, false);
+ testSection(document.getElementById("test2"), "paragraph in editable list item", false, true);
+ testSection(document.getElementById("test3"), "paragraph in editable list", true, false);
+ /* Note: concerning #test3, it would be preferrable that [Return] creates
+ * another paragraph in another list item (i.e. last argument = 'true').
+ * Currently it just creates an empty list item, which is acceptable.
+ */
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug578771.html b/editor/libeditor/tests/test_bug578771.html
new file mode 100644
index 0000000000..5cf7b23e52
--- /dev/null
+++ b/editor/libeditor/tests/test_bug578771.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=578771
+-->
+
+<head>
+ <title>Test for Bug 578771</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=578771">Mozilla Bug 578771</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 578771 **/
+ SimpleTest.waitForExplicitFinish();
+
+ function testElem(elem, elemTag) {
+ var ce = document.getElementById("ce");
+ ce.focus();
+
+ synthesizeMouse(elem, 5, 5, {clickCount: 2 });
+ ok(elem.selectionStart == 0 && elem.selectionEnd == 7,
+ " Double-clicking on another " + elemTag + " works correctly");
+
+ ce.focus();
+ synthesizeMouse(elem, 5, 5, {clickCount: 3 });
+ ok(elem.selectionStart == 0 && elem.selectionEnd == 14,
+ "Triple-clicking on another " + elemTag + " works correctly");
+ }
+ // Avoid platform selection differences
+ SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set": [["layout.word_select.eat_space_to_next_word", false]]}, startTest);
+ });
+
+ function startTest() {
+ var input = document.getElementById("ip");
+ testElem(input, "input");
+
+ var textarea = document.getElementById("ta");
+ testElem(textarea, "textarea");
+
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input id="ip" type="text" value="Mozilla editor" />
+ <textarea id="ta">Mozilla editor</textarea>
+ <div id="ce" contenteditable="true">Contenteditable div that could interfere with focus</div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug586662.html b/editor/libeditor/tests/test_bug586662.html
new file mode 100644
index 0000000000..c7280b4ac2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug586662.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=586662
+-->
+
+<head>
+ <title>Test for Bug 586662</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=586662">Mozilla Bug 586662</a>
+ <p id="display"><textarea onkeypress="this.style.overflow = 'hidden'"></textarea></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+ sendString("a");
+ is(getComputedStyle(t, null).overflow, "hidden", "The event handler should be executed");
+ is(t.value, "a", "The key entry should result in a character being added to the field");
+
+ var win = window.open("file_bug586662.html", "_blank",
+ "width=600,height=600,scrollbars=yes");
+ SimpleTest.waitForFocus(function() {
+ // Make sure that focusing the textarea will cause the page to scroll
+ var ed = win.document.getElementById("editor");
+ ed.focus();
+ setTimeout(function() {
+ isnot(win.scrollY, 0, "Page is scrolled down");
+ // Scroll back up
+ win.scrollTo(0, 0);
+ setTimeout(function() {
+ is(win.scrollY, 0, "Page is scrolled back up");
+ // Make sure that typing something into the textarea will cause the
+ // page to scroll down
+ synthesizeKey("a", {}, win);
+ requestAnimationFrame(function() {
+ requestAnimationFrame(function() {
+ isnot(win.scrollY, 0, "Page is scrolled down again");
+
+ win.close();
+ SimpleTest.finish();
+ });
+ });
+ }, 0);
+ }, 0);
+ }, win);
+});
+
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug590554.html b/editor/libeditor/tests/test_bug590554.html
new file mode 100644
index 0000000000..ea48b4c08f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug590554.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=590554
+-->
+
+<head>
+ <title>Test for Bug 590554</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+
+ <script type="application/javascript">
+
+ /** Test for Bug 590554 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+ synthesizeKey("KEY_Enter");
+ is(t.value, "\n", "Pressing enter should work the first time");
+ synthesizeKey("KEY_Enter");
+ is(t.value, "\n", "Pressing enter should not work the second time");
+ SimpleTest.finish();
+ });
+
+ </script>
+
+ <textarea maxlength="1"></textarea>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug592592.html b/editor/libeditor/tests/test_bug592592.html
new file mode 100644
index 0000000000..c1a74e9893
--- /dev/null
+++ b/editor/libeditor/tests/test_bug592592.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=592592
+-->
+<head>
+ <title>Test for Bug 592592</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=592592">Mozilla Bug 592592</a>
+<p id="display"></p>
+<div id="content">
+ <div id="editor" contenteditable="true" style="white-space:pre-wrap">a b</div>
+ <div id="editor2" contenteditable="true" style="white-space:pre-wrap">a b</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 592592 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var ed = document.getElementById("editor");
+
+ // Put the selection right after "a"
+ ed.focus();
+ window.getSelection().collapse(ed.firstChild, 1);
+
+ // Press space
+ sendString(" ");
+
+ // Make sure we haven't added an nbsp
+ is(ed.innerHTML, "a b", "We should not be adding an &nbsp; for preformatted text");
+
+ // Remove the preformatted style
+ ed.removeAttribute("style");
+
+ // Reset the DOM
+ ed.innerHTML = "a b";
+
+ // Reset the selection
+ ed.focus();
+ window.getSelection().collapse(ed.firstChild, 1);
+
+ // Press space
+ sendString(" ");
+
+ // Make sure that we have added an nbsp
+ is(ed.innerHTML, "a&nbsp; b", "We should add an &nbsp; for non-preformatted text");
+
+ ed = document.getElementById("editor2");
+
+ // Put the selection after the second space in the second editable field
+ ed.focus();
+ window.getSelection().collapse(ed.firstChild, 3);
+
+ // Press the back-space key
+ synthesizeKey("KEY_Backspace");
+
+ // Make sure that we've only deleted a single space
+ is(ed.innerHTML, "a b", "We should only be deleting a single space");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug596001.html b/editor/libeditor/tests/test_bug596001.html
new file mode 100644
index 0000000000..451b1f9c28
--- /dev/null
+++ b/editor/libeditor/tests/test_bug596001.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596001
+-->
+<head>
+ <title>Test for Bug 596001</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596001">Mozilla Bug 596001</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="src">a&#9;b</textarea>
+<textarea id="dst"></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 596001 **/
+
+function testTab(prefix, callback) {
+ var src = document.getElementById("src");
+ var dst = document.getElementById("dst");
+ dst.value = prefix;
+ src.focus();
+ src.select();
+ SimpleTest.waitForClipboard("a\tb",
+ function() {
+ synthesizeKey("c", {accelKey: true});
+ },
+ function() {
+ dst.focus();
+ var inputReceived = false;
+ dst.addEventListener("input", function() { inputReceived = true; });
+ synthesizeKey("v", {accelKey: true});
+ ok(inputReceived, "An input event should be raised");
+ is(dst.value, prefix + src.value, "The value should be pasted verbatim");
+ callback();
+ },
+ callback
+ );
+}
+
+SimpleTest.waitForExplicitFinish();
+testTab("", function() {
+ testTab("foo", function() {
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug596506.html b/editor/libeditor/tests/test_bug596506.html
new file mode 100644
index 0000000000..3ecf5e2363
--- /dev/null
+++ b/editor/libeditor/tests/test_bug596506.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596506
+-->
+<head>
+ <title>Test for Bug 596506</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596506">Mozilla Bug 596506</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 596506 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+const kIsMac = navigator.platform.includes("Mac");
+
+
+function runTest() {
+ var edit = document.getElementById("edit");
+ edit.focus();
+
+ sendString("First");
+ synthesizeKey("KEY_Enter");
+ sendString("Second");
+ synthesizeKey("KEY_ArrowUp");
+ synthesizeKey("KEY_ArrowUp");
+ if (kIsMac) {
+ synthesizeKey("KEY_ArrowRight", { accelKey: true });
+ } else {
+ synthesizeKey("KEY_End");
+ }
+ sendString("ly");
+ is(edit.value, "Firstly\nSecond",
+ "Pressing end should position the cursor before the terminating newline");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+
+<textarea id="edit"></textarea>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug597331.html b/editor/libeditor/tests/test_bug597331.html
new file mode 100644
index 0000000000..d165ff78a5
--- /dev/null
+++ b/editor/libeditor/tests/test_bug597331.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=597331
+-->
+<head>
+ <title>Test for Bug 597331</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ textarea { border-color: white; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597331">Mozilla Bug 597331</a>
+<p id="display"></p>
+<div id="content">
+<textarea>line1
+line2
+line3
+</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 597331 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ SimpleTest.executeSoon(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+ t.selectionStart = 4;
+ t.selectionEnd = 4;
+ SimpleTest.executeSoon(function() {
+ t.getBoundingClientRect(); // flush layout
+ var before = snapshotWindow(window, true);
+ t.selectionStart = 5;
+ t.selectionEnd = 5;
+ t.addEventListener("keydown", function() {
+ SimpleTest.executeSoon(function() {
+ t.style.display = "block";
+ document.body.offsetWidth;
+ t.style.display = "";
+ document.body.offsetWidth;
+
+ is(t.selectionStart, 4, "Cursor should be moved correctly");
+ is(t.selectionEnd, 4, "Cursor should be moved correctly");
+
+ var after = snapshotWindow(window, true);
+
+ var result = compareSnapshots(before, after, true);
+ var msg = "The caret should be displayed correctly after reframing";
+ if (!result[0]) {
+ msg += "\nRESULT:\n" + result[2];
+ msg += "\nREFERENCE:\n" + result[1];
+ }
+ ok(result[0], msg);
+
+ SimpleTest.finish();
+ });
+ }, {once: true});
+ synthesizeKey("KEY_ArrowLeft");
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug597784.html b/editor/libeditor/tests/test_bug597784.html
new file mode 100644
index 0000000000..ef0348a0a4
--- /dev/null
+++ b/editor/libeditor/tests/test_bug597784.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=597784
+-->
+<head>
+ <title>Test for Bug 597784</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597784">Mozilla Bug 597784</a>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 597784 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ document.designMode = "on";
+ var content = document.getElementById("content");
+ getSelection().collapse(content, 0);
+ var html = "<test:tag>test:tag</test:tag>" +
+ "<a href=\"http://mozilla.org/\" test:attr=\"test:attr\" custom=\"value\">link</a>";
+ document.execCommand("insertHTML", false, html);
+ is(content.innerHTML, html,
+ "The custom tags and attributes should be inserted into the document using the insertHTML command");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug599322.html b/editor/libeditor/tests/test_bug599322.html
new file mode 100644
index 0000000000..57c15cf4c7
--- /dev/null
+++ b/editor/libeditor/tests/test_bug599322.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=599322.patch
+-->
+<head>
+ <title>Test for Bug 599322.patch</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=599322.patch">Mozilla Bug 599322.patch</a>
+<p id="display"></p>
+<div id="content">
+<div id="src">src<img src="/tests/editor/libeditor/tests/green.png"></div>
+<iframe id="dst" src="javascript:;"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 599322.patch **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var src = document.getElementById("src");
+ var dst = document.getElementById("dst");
+ var doc = dst.contentDocument;
+ doc.open();
+ doc.write("<html><head><base href='http://mochi.test:8888/'></head><body></body></html>");
+ doc.close();
+ SimpleTest.waitForFocus(function() {
+ getSelection().selectAllChildren(src);
+ SimpleTest.waitForClipboard("src",
+ function() {
+ synthesizeKey("c", {accelKey: true});
+ },
+ function() {
+ dst.contentDocument.designMode = "on";
+ dst.focus();
+ dst.contentDocument.body.focus();
+ synthesizeKey("v", {accelKey: true});
+ is(dst.contentDocument.querySelector("img").src,
+ document.querySelector("img").src,
+ "The source should be correctly set based on the base URI");
+ SimpleTest.finish();
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug599983.html b/editor/libeditor/tests/test_bug599983.html
new file mode 100644
index 0000000000..08fc9a228a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug599983.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=599983
+-->
+<title>Test for Bug 599983</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=599983">Mozilla Bug 599983</a>
+<div contenteditable>foo</div>
+<script>
+getSelection().selectAllChildren(document.querySelector("div"));
+document.execCommand("bold");
+is(document.querySelector("[_moz_dirty]"), null,
+ "No _moz_dirty allowed in webpages");
+</script>
diff --git a/editor/libeditor/tests/test_bug599983.xhtml b/editor/libeditor/tests/test_bug599983.xhtml
new file mode 100644
index 0000000000..6e943929d4
--- /dev/null
+++ b/editor/libeditor/tests/test_bug599983.xhtml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=599983
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 599983" onload="runTest()">
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=599983"
+ target="_blank">Mozilla Bug 599983</a>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ editortype="html"
+ src="about:blank" />
+ </body>
+ <script type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ const kAllowInteraction = Ci.nsIEditor.eEditorAllowInteraction;
+ const kMailMask = Ci.nsIEditor.eEditorMailMask;
+
+ function runTest() {
+ testEditor(false, false);
+ testEditor(false, true);
+ testEditor(true, false);
+ testEditor(true, true);
+
+ SimpleTest.finish();
+ }
+
+ function testEditor(setAllowInteraction, setMailMask) {
+ var desc = " with " + (setAllowInteraction ? "" : "no ") +
+ "eEditorAllowInteraction and " +
+ (setMailMask ? "" : "no ") + "eEditorMailMask";
+
+ var editorElem = document.getElementById("editor");
+
+ var editorObj = editorElem.getEditor(editorElem.contentWindow);
+ editorObj.flags = (setAllowInteraction ? kAllowInteraction : 0) |
+ (setMailMask ? kMailMask : 0);
+
+ var editorDoc = editorElem.contentDocument;
+ editorDoc.body.innerHTML = "<p>foo<p>bar";
+ editorDoc.getSelection().selectAllChildren(editorDoc.body.firstChild);
+ editorDoc.execCommand("bold");
+
+ var createsDirty = !setAllowInteraction || setMailMask;
+
+ (createsDirty ? isnot : is)(editorDoc.querySelector("[_moz_dirty]"), null,
+ "Elements with _moz_dirty" + desc);
+
+ // Even if we do create _moz_dirty, we should strip it for innerHTML.
+ is(editorDoc.body.innerHTML, "<p><b>foo</b></p><p>bar</p>",
+ "innerHTML" + desc);
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/editor/libeditor/tests/test_bug600570.html b/editor/libeditor/tests/test_bug600570.html
new file mode 100644
index 0000000000..f4ef84199c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug600570.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=600570
+-->
+<head>
+ <title>Test for Bug 600570</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ textarea { border-color: white; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=600570">Mozilla Bug 600570</a>
+<p id="display"></p>
+<div id="content">
+<textarea spellcheck="false">
+aaa
+[bbb]</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 600570 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.value = "[aaa\nbbb]";
+ t.focus();
+ synthesizeKey("A", {accelKey: true});
+
+ SimpleTest.executeSoon(function() {
+ t.getBoundingClientRect(); // flush layout
+ var afterSetValue = snapshotWindow(window);
+
+ t.value = t.defaultValue;
+
+ t.selectionStart = 0;
+ t.selectionEnd = 4;
+ SimpleTest.waitForClipboard("aaa\n",
+ function() {
+ synthesizeKey("X", {accelKey: true});
+ },
+ function() {
+ t.addEventListener("input", function() {
+ setTimeout(function() { // Avoid the assertion in bug 649797
+ is(t.value, "[aaa\nbbb]", "The value of the textarea should be correct");
+ synthesizeKey("A", {accelKey: true});
+ is(t.selectionStart, 0, "Select all should set the selection start to the beginning of textarea");
+ is(t.selectionEnd, 9, "Select all should set the selection end to the end of textarea");
+
+ var afterPaste = snapshotWindow(window);
+
+ var res = compareSnapshots(afterSetValue, afterPaste, true);
+ var msg = "Pasting and setting the value directly should result in the same rendering";
+ if (!res[0]) {
+ msg += "\nRESULT:\n" + res[2] + "\nREFERENCE:\n" + res[1];
+ }
+ ok(res[0], msg);
+
+ SimpleTest.finish();
+ }, 0);
+ }, {once: true});
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("V", {accelKey: true});
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug603556.html b/editor/libeditor/tests/test_bug603556.html
new file mode 100644
index 0000000000..1e27d824e3
--- /dev/null
+++ b/editor/libeditor/tests/test_bug603556.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=603556
+-->
+<head>
+ <title>Test for Bug 603556</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=603556">Mozilla Bug 603556</a>
+<p id="display"></p>
+<div id="content">
+ <div id="src">testing</div>
+ <input maxlength="4">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 603556 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var i = document.querySelector("input");
+ var src = document.getElementById("src");
+ SimpleTest.waitForClipboard(src.textContent,
+ function() {
+ getSelection().selectAllChildren(src);
+ synthesizeKey("C", {accelKey: true});
+ },
+ function() {
+ i.focus();
+ synthesizeKey("V", {accelKey: true});
+ if (!SpecialPowers.getBoolPref("editor.truncate_user_pastes")) {
+ is(i.value, src.textContent,
+ "Pasting should paste the clipboard contents regardless of maxlength");
+ } else {
+ is(i.value, src.textContent.substr(0, i.maxLength),
+ "Pasting should paste maxlength chars worth of the clipboard contents");
+ }
+ SimpleTest.finish();
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug604532.html b/editor/libeditor/tests/test_bug604532.html
new file mode 100644
index 0000000000..cd790a58b1
--- /dev/null
+++ b/editor/libeditor/tests/test_bug604532.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=604532
+-->
+<head>
+ <title>Test for Bug 604532</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=604532">Mozilla Bug 604532</a>
+<p id="display"></p>
+<div id="content">
+<input>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 604532 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var i = document.querySelector("input");
+ i.focus();
+ i.value = "foo";
+ synthesizeKey("A", {accelKey: true});
+ is(i.selectionStart, 0, "Selection should start at 0 before appending");
+ is(i.selectionEnd, 3, "Selection should end at 3 before appending");
+ synthesizeKey("KEY_ArrowRight");
+ sendString("x");
+ is(i.value, "foox", "The text should be appended correctly");
+ synthesizeKey("A", {accelKey: true});
+ is(i.selectionStart, 0, "Selection should start at 0 after appending");
+ is(i.selectionEnd, 4, "Selection should end at 4 after appending");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug607584.html b/editor/libeditor/tests/test_bug607584.html
new file mode 100644
index 0000000000..c2595af108
--- /dev/null
+++ b/editor/libeditor/tests/test_bug607584.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=607584
+-->
+<head>
+ <title>Test for Bug 607584</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=607584">Mozilla Bug 607584</a>
+<p id="display"></p>
+<div id="content" contenteditable>
+<p id="foo">Hello world</p>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 607584 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var content = document.getElementById("content");
+ content.focus();
+ var sel = getSelection();
+ sel.collapse(document.getElementById("foo").firstChild, 5);
+ synthesizeKey("KEY_Enter");
+ var paragraphs = content.querySelectorAll("p");
+ is(paragraphs.length, 2, "The paragraph should be split in two");
+ is(paragraphs[0].textContent, "Hello", "The first paragraph should have the correct content");
+ is(paragraphs[1].textContent, "\u00A0world", "The second paragraph should have the correct content");
+ is(paragraphs[0].getAttribute("id"), "foo", "The id of the first paragraph should be retained");
+ is(paragraphs[1].hasAttribute("id"), false, "The second paragraph shouldn't have an ID");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug607584.xhtml b/editor/libeditor/tests/test_bug607584.xhtml
new file mode 100644
index 0000000000..f610de544b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug607584.xhtml
@@ -0,0 +1,112 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=607584
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 607584" onload="runTest();">
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=607584"
+ target="_blank">Mozilla Bug 607584</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ type="content"
+ primary="true"
+ editortype="html"
+ style="width: 400px; height: 100px; border: thin solid black"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function EditorContentListener(aEditor)
+ {
+ this.init(aEditor);
+ }
+
+ EditorContentListener.prototype = {
+ init(aEditor)
+ {
+ this.mEditor = aEditor;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
+ "nsISupportsWeakReference"]),
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)
+ {
+ var editor = this.mEditor.getEditor(this.mEditor.contentWindow);
+ if (editor) {
+ this.mEditor.focus();
+ editor instanceof Ci.nsIHTMLEditor;
+ editor.returnInParagraphCreatesNewParagraph = true;
+ editor.insertHTML("<p id='foo'>this is a paragraph carrying id 'foo'</p>");
+ var p = editor.document.getElementById('foo')
+ editor.beginningOfDocument();
+ sendKey("return");
+ var firstP = p.parentNode.firstElementChild;
+ var lastP = p.parentNode.lastElementChild;
+ var isOk = firstP.nodeName.toLowerCase() == "p" &&
+ firstP.id == "foo" &&
+ lastP.id == "";
+ ok(isOk, "CR in a paragraph with an ID should not create two paragraphs of same ID");
+ progress.removeProgressListener(this);
+ SimpleTest.finish();
+ }
+ }
+
+ },
+
+
+ onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress)
+ {
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ },
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ },
+
+ onSecurityChange(aWebProgress, aRequest, aState)
+ {
+ },
+
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent)
+ {
+ },
+
+ mEditor: null
+ };
+
+ var progress, progressListener;
+
+ function runTest() {
+ var newEditorElement = document.getElementById("editor");
+ newEditorElement.makeEditable("html", true);
+ var docShell = newEditorElement.docShell;
+ progress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+ progressListener = new EditorContentListener(newEditorElement);
+ progress.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ newEditorElement.setAttribute("src", "data:text/html,");
+ }
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_bug611182.html b/editor/libeditor/tests/test_bug611182.html
new file mode 100644
index 0000000000..7843156fda
--- /dev/null
+++ b/editor/libeditor/tests/test_bug611182.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=611182
+-->
+<head>
+ <title>Test for Bug 611182</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=611182">Mozilla Bug 611182</a>
+<p id="display"></p>
+<div id="content">
+ <iframe></iframe>
+ <iframe id="ref" src="./file_bug611182.html"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 611182 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var iframe = document.querySelector("iframe");
+ var refElem = document.querySelector("#ref");
+ var ref = snapshotWindow(refElem.contentWindow, false);
+
+ function findTextNode(doc) {
+ var body = doc.documentElement;
+ var result = findTextNodeWorker(body);
+ ok(result, "Failed to find the text node");
+ return result;
+ }
+
+ function findTextNodeWorker(root) {
+ if (root.isContentEditable) {
+ root.focus();
+ }
+ for (var i = 0; i < root.childNodes.length; ++i) {
+ var node = root.childNodes[i];
+ if (node.nodeType == node.TEXT_NODE &&
+ node.nodeValue == "fooz bar") {
+ return node;
+ }
+ if (node.nodeType == node.ELEMENT_NODE) {
+ node = findTextNodeWorker(node);
+ if (node) {
+ return node;
+ }
+ }
+ }
+ return null;
+ }
+
+ function testBackspace(src, callback) {
+ ok(true, "Testing " + src);
+ iframe.addEventListener("load", function() {
+ var doc = iframe.contentDocument;
+ var win = iframe.contentWindow;
+ doc.body.setAttribute("spellcheck", "false");
+
+ iframe.focus();
+ var textNode = findTextNode(doc);
+ var sel = win.getSelection();
+ sel.collapse(textNode, 4);
+ synthesizeKey("KEY_Backspace");
+ is(textNode.textContent, "foo bar", "Backspace should work correctly");
+
+ var snapshot = snapshotWindow(win, false);
+ ok(compareSnapshots(snapshot, ref, true)[0],
+ "No padding <br> element should exist in the document");
+
+ callback();
+ }, {once: true});
+ iframe.src = src;
+ }
+
+ var totalTests = 0;
+ var currentTest = 0;
+ function runAllTests() {
+ if (currentTest == totalTests) {
+ SimpleTest.finish();
+ return;
+ }
+ testBackspace("file_bug611182.sjs?" + currentTest, runAllTests);
+ currentTest++;
+ }
+
+ // query total number of tests to be run from the server and
+ // start running all tests.
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "file_bug611182.sjs?queryTotalTests");
+ myXHR.onload = function(e) {
+ totalTests = myXHR.responseText;
+ runAllTests();
+ };
+ myXHR.send();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug612128.html b/editor/libeditor/tests/test_bug612128.html
new file mode 100644
index 0000000000..3d6d7ed34e
--- /dev/null
+++ b/editor/libeditor/tests/test_bug612128.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=612128
+-->
+<head>
+ <title>Test for Bug 612128</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612128">Mozilla Bug 612128</a>
+<p id="display"></p>
+<div id="content">
+<input>
+<div contenteditable></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/* eslint-disable no-useless-concat */
+
+/** Test for Bug 612128 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ document.querySelector("input").focus();
+ try {
+ is(document.execCommand("inserthtml", null, "<span>f" + "oo</span>"),
+ false, "The insertHTML command should return false");
+ } catch (e) {
+ ok(false, "insertHTML should not throw here");
+ }
+ is(document.querySelectorAll("span").length, 0, "No span element should be injected inside the page");
+ is(document.body.innerHTML.indexOf("f" + "oo"), -1, "No text should be injected inside the page");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug612447.html b/editor/libeditor/tests/test_bug612447.html
new file mode 100644
index 0000000000..fdb3b38f25
--- /dev/null
+++ b/editor/libeditor/tests/test_bug612447.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=612447
+-->
+<head>
+ <title>Test for Bug 612447</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612447">Mozilla Bug 612447</a>
+<p id="display"></p>
+<div id="content">
+<iframe></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 612447 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ function editorCommandsEnabled() {
+ var caught = false;
+ try {
+ doc.execCommand("justifyfull", false, null);
+ } catch (e) {
+ caught = true;
+ }
+ return !caught;
+ }
+
+ var i = document.querySelector("iframe");
+ var doc = i.contentDocument;
+ var win = i.contentWindow;
+ var b = doc.body;
+ doc.designMode = "on";
+ i.focus();
+ b.focus();
+ var beforeA = snapshotWindow(win, true);
+ sendString("X");
+ var beforeB = snapshotWindow(win, true);
+ is(b.textContent, "X", "Typing should work");
+ while (b.firstChild) {
+ b.firstChild.remove();
+ }
+ ok(editorCommandsEnabled(), "The editor commands should work");
+
+ i.style.display = "block";
+ document.clientWidth;
+
+ i.focus();
+ b.focus();
+ var afterA = snapshotWindow(win, true);
+ sendString("X");
+ var afterB = snapshotWindow(win, true);
+ is(b.textContent, "X", "Typing should work");
+ while (b.firstChild) {
+ b.firstChild.remove();
+ }
+ ok(editorCommandsEnabled(), "The editor commands should work");
+
+ ok(compareSnapshots(beforeA, afterA, true)[0], "The iframes should look the same before typing");
+ ok(compareSnapshots(beforeB, afterB, true)[0], "The iframes should look the same after typing");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug616590.xhtml b/editor/libeditor/tests/test_bug616590.xhtml
new file mode 100644
index 0000000000..1f6cb3d0f8
--- /dev/null
+++ b/editor/libeditor/tests/test_bug616590.xhtml
@@ -0,0 +1,101 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=616590
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 616590" onload="runTest();">
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=616590"
+ target="_blank">Mozilla Bug 616590</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ type="content"
+ editortype="htmlmail"
+ style="width: 400px; height: 100px;"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function EditorContentListener(aEditor)
+ {
+ this.init(aEditor);
+ }
+
+ EditorContentListener.prototype = {
+ init(aEditor)
+ {
+ this.mEditor = aEditor;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
+ "nsISupportsWeakReference"]),
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)
+ {
+ var editor = this.mEditor.getEditor(this.mEditor.contentWindow);
+ if (editor) {
+ editor.QueryInterface(Ci.nsIEditorMailSupport);
+ editor.insertAsCitedQuotation("<html><body><div contenteditable>foo</div></body></html>", "", true);
+ document.documentElement.clientWidth;
+ progress.removeProgressListener(this);
+ ok(true, "Test complete");
+ SimpleTest.finish();
+ }
+ }
+ },
+
+
+ onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress)
+ {
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ },
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ },
+
+ onSecurityChange(aWebProgress, aRequest, aState)
+ {
+ },
+
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent)
+ {
+ },
+
+ mEditor: null
+ };
+
+ var progress, progressListener;
+
+ function runTest() {
+ var editorElement = document.getElementById("editor");
+ editorElement.makeEditable("htmlmail", true);
+ var docShell = editorElement.docShell;
+ progress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+ progressListener = new EditorContentListener(editorElement);
+ progress.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ editorElement.setAttribute("src", "data:text/html,");
+ }
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_bug622371.html b/editor/libeditor/tests/test_bug622371.html
new file mode 100644
index 0000000000..de52efb618
--- /dev/null
+++ b/editor/libeditor/tests/test_bug622371.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=622371
+-->
+<head>
+ <title>Test for Bug 622371</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622371">Mozilla Bug 622371</a>
+<p id="display"></p>
+<div id="content">
+ <iframe srcdoc="<body contenteditable>abc</body>"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 622371 **/
+// This test is ported to WPT:
+// html/editing/editing-0/contenteditable/selection-in-contentEditable-at-turning-designMode-on-off.tentative.html
+// But unfortunately, the random orange reported as bug 1601585 with it.
+// Therefore, this test is not removed.
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var i = document.querySelector("iframe");
+ var sel = i.contentWindow.getSelection();
+ var doc = i.contentDocument;
+ var body = doc.body;
+ i.focus();
+ sel.collapse(body, 1);
+ doc.designMode = "on";
+ doc.designMode = "off";
+ is(sel.getRangeAt(0).startOffset, 1, "The start offset of the selection shouldn't change");
+ is(sel.getRangeAt(0).endOffset, 1, "The end offset of the selection shouldn't change");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug625452.html b/editor/libeditor/tests/test_bug625452.html
new file mode 100644
index 0000000000..68ff16342f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug625452.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=625452
+-->
+<head>
+ <title>Test for Bug 625452</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=625452">Mozilla Bug 625452</a>
+<p id="display"></p>
+<div id="content">
+<input>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 625452 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var i = document.querySelector("input");
+ var inputCount = 0;
+ i.addEventListener("input", function() { inputCount++; });
+
+ // test cut
+ i.focus();
+ i.value = "foo bar";
+ i.selectionStart = 0;
+ i.selectionEnd = 4;
+ synthesizeKey("X", {accelKey: true});
+ is(i.value, "bar", "Cut should work correctly");
+ is(inputCount, 1, "input event should be raised correctly");
+
+ // test undo
+ synthesizeKey("Z", {accelKey: true});
+ is(i.value, "foo bar", "Undo should work correctly");
+ is(inputCount, 2, "input event should be raised correctly");
+
+ // test redo
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+ is(i.value, "bar", "Redo should work correctly");
+ is(inputCount, 3, "input event should be raised correctly");
+
+ // test delete
+ i.selectionStart = 0;
+ i.selectionEnd = 2;
+ synthesizeKey("KEY_Delete");
+ is(i.value, "r", "Delete should work correctly");
+ is(inputCount, 4, "input event should be raised correctly");
+
+ // test DeleteSelection(eNone)
+ i.value = "retest"; // the "r" common prefix is crucial here
+ is(inputCount, 4, "input event should not have been raised");
+
+ // paste is tested in test_bug596001.html
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug629172.html b/editor/libeditor/tests/test_bug629172.html
new file mode 100644
index 0000000000..55d1ed89b6
--- /dev/null
+++ b/editor/libeditor/tests/test_bug629172.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=629172
+-->
+<head>
+ <title>Test for Bug 629172</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=629172">Mozilla Bug 629172</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="ltr-ref">test.</textarea>
+<textarea id="rtl-ref" style="display: none; direction: rtl">test.</textarea>
+</div>
+<pre id="test">
+<script>
+
+/** Test for Bug 629172 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["test.events.async.enabled", true]],
+ });
+
+ let LTRRef = document.getElementById("ltr-ref");
+ let RTLRef = document.getElementById("rtl-ref");
+ let ReferenceScreenshots = {};
+
+ // generate the reference screenshots
+ document.body.clientWidth;
+ ReferenceScreenshots.ltr = snapshotWindow(window);
+ LTRRef.remove();
+ RTLRef.style.display = "";
+ document.body.clientWidth;
+ ReferenceScreenshots.rtl = snapshotWindow(window);
+ RTLRef.remove();
+
+ async function testDirection(initialDir) {
+ function revertDir(aDir) {
+ return aDir == "rtl" ? "ltr" : "rtl";
+ }
+
+ function promiseFormatSetBlockTextDirectionInputEvent(aElement) {
+ return new Promise(resolve => {
+ function handler(aEvent) {
+ if (aEvent.inputType !== "formatSetBlockTextDirection") {
+ ok(false, `Unexpected input event received: inputType="${aEvent.inputType}"`);
+ } else {
+ aElement.removeEventListener("input", handler, true);
+ SimpleTest.executeSoon(resolve);
+ }
+ }
+ aElement.addEventListener("input", handler, true);
+ });
+ }
+
+ let textarea = document.createElement("textarea");
+ textarea.setAttribute("dir", initialDir);
+ textarea.value = "test.";
+ document.getElementById("content").appendChild(textarea);
+ document.body.clientWidth;
+ assertSnapshots(snapshotWindow(window), ReferenceScreenshots[initialDir],
+ /* expectEqual = */ true, /* fuzz = */ null,
+ `<textarea dir="${initialDir}"> before Accel+Shift+X`,
+ `<textarea dir="${initialDir}">`);
+ textarea.focus();
+ let waitForInputEvent = promiseFormatSetBlockTextDirectionInputEvent(textarea);
+ synthesizeKey("X", {accelKey: true, shiftKey: true});
+ await waitForInputEvent;
+ is(textarea.getAttribute("dir"), revertDir(initialDir),
+ "The dir attribute must be correctly updated with first Accel+Shift+X");
+ textarea.blur();
+ assertSnapshots(snapshotWindow(window), ReferenceScreenshots[revertDir(initialDir)],
+ /* expectEqual = */ true, /* fuzz = */ null,
+ `<textarea dir="${initialDir}"> after first Accel+Shift+X`,
+ `<textarea dir="${revertDir(initialDir)}">`);
+ textarea.focus();
+ waitForInputEvent = promiseFormatSetBlockTextDirectionInputEvent(textarea);
+ synthesizeKey("X", {accelKey: true, shiftKey: true});
+ await waitForInputEvent;
+ is(textarea.getAttribute("dir"), initialDir,
+ "The dir attribute must be correctly recovered with second Accel+Shift+X");
+ textarea.blur();
+ assertSnapshots(snapshotWindow(window), ReferenceScreenshots[initialDir],
+ /* expectEqual = */ true, /* fuzz = */ null,
+ `<textarea dir="${initialDir}"> after second Accel+Shift+X`,
+ `<textarea dir="${initialDir}">`);
+ textarea.remove();
+ }
+
+ await testDirection("ltr");
+ await testDirection("rtl");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug629845.html b/editor/libeditor/tests/test_bug629845.html
new file mode 100644
index 0000000000..e7c4bd5019
--- /dev/null
+++ b/editor/libeditor/tests/test_bug629845.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=629845
+-->
+<head>
+ <title>Test for Bug 629845</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=629845">Mozilla Bug 629845</a>
+<p id="display"></p>
+
+<script>
+function initFrame(frame) {
+ frame.contentWindow.document.designMode = "on";
+ frame.contentWindow.document.writeln("<body></body>");
+
+ document.getElementsByTagName("button")[0].click();
+}
+
+function command(aName) {
+ var frame = document.getElementsByTagName("iframe")[0];
+
+ is(frame.contentDocument.designMode, "on", "design mode should be on!");
+ var caught = false;
+ try {
+ frame.contentDocument.execCommand(aName, false, null);
+ } catch (e) {
+ ok(false, "exception " + e + " was thrown");
+ caught = true;
+ }
+
+ ok(!caught, "No exception should have been thrown.");
+
+ // Stop the document load before finishing, just to be clean.
+ document.getElementsByTagName("iframe")[0].contentWindow.document.close();
+ SimpleTest.finish();
+}
+</script>
+
+<div id="content">
+ <button type="button" onclick="command('bold');">Bold</button>
+ <iframe onload="initFrame(this);"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 629845 **/
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug635636.html b/editor/libeditor/tests/test_bug635636.html
new file mode 100644
index 0000000000..3da04e18f2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug635636.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=635636
+-->
+<head>
+ <title>Test for Bug 635636</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635636">Mozilla Bug 635636</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 635636 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(async function() {
+ function openNewWindow(aURL) {
+ return new Promise(resolve => {
+ let contentWindow = window.open(aURL);
+ contentWindow.addEventListener("load", () => {
+ ok(true, aURL + " is loaded");
+ resolve(contentWindow);
+ }, { once: true });
+ });
+ }
+
+ function unloadWindow(aWindow) {
+ return new Promise(resolve => {
+ aWindow.addEventListener("unload", () => {
+ ok(true, "The window has been unloaded");
+ SimpleTest.executeSoon(() => { resolve(0); });
+ }, { once: true });
+ aWindow.location = "file_bug635636_2.html";
+ });
+ }
+
+ let contentWindow = await openNewWindow("file_bug635636.xhtml");
+
+ contentWindow.addEventListener("load", () => { ok(true, "load"); });
+ contentWindow.addEventListener("pageshow", () => { ok(true, "pageshow"); });
+
+ let div = contentWindow.document.getElementsByTagName("div")[0];
+ contentWindow.document.designMode = "on";
+
+ await unloadWindow(contentWindow);
+
+ div.remove();
+ ok(true, "Should not crash");
+ // Not needed for the crash
+ contentWindow.close();
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug638596.html b/editor/libeditor/tests/test_bug638596.html
new file mode 100644
index 0000000000..976ccb32ab
--- /dev/null
+++ b/editor/libeditor/tests/test_bug638596.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=638596
+-->
+<head>
+ <title>Test for Bug 638596</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=638596">Mozilla Bug 638596</a>
+<p id="display"></p>
+<div id="content">
+ <input type="password" style="font-size: 0">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 638596 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var i = document.querySelector("input");
+ i.focus();
+ sendString("test");
+ is(i.value, "test", "The correct value should be stored in the field");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug641466.html b/editor/libeditor/tests/test_bug641466.html
new file mode 100644
index 0000000000..5004df2d8a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug641466.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=641466
+-->
+<head>
+ <title>Test for Bug 641466</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=641466">Mozilla Bug 641466</a>
+<p id="display"></p>
+<div id="content">
+<input value="&#x10451;&#x10467;&#x10455;&#x10451;">
+<textarea>&#x10451;&#x10467;&#x10455;&#x10451;</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 641466 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ function doTest(element) {
+ element.focus();
+ element.selectionStart = 4;
+ element.selectionEnd = 4;
+ synthesizeKey("KEY_Backspace", {repeat: 4});
+
+ // XXX: Switched to from ok() to todo_is() in Bug 1467712. Follow up in 1500964
+ // This test is not working for several reasons:
+ // - race conditions between each event, we should wait before sending the next backspace
+ // - race conditions between the two tests
+ // - the value has an initial length of 8, not 4
+ todo_is(element.value, "", "4 backspaces should delete all of the characters in the " + element.localName);
+ }
+
+ doTest(document.querySelector("input"));
+ doTest(document.querySelector("textarea"));
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug645914.html b/editor/libeditor/tests/test_bug645914.html
new file mode 100644
index 0000000000..8f1a8cbfda
--- /dev/null
+++ b/editor/libeditor/tests/test_bug645914.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=645914
+-->
+<head>
+ <title>Test for Bug 645914</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=645914">Mozilla Bug 645914</a>
+<p id="display"></p>
+<div id="content">
+<textarea>foo
+bar</textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 645914 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set": [["layout.word_select.eat_space_to_next_word", true],
+ ["browser.triple_click_selects_paragraph", false]]}, startTest);
+});
+function startTest() {
+ var textarea = document.querySelector("textarea");
+ textarea.selectionStart = textarea.selectionEnd = 0;
+
+ // Simulate a double click on foo
+ synthesizeMouse(textarea, 5, 5, {clickCount: 2});
+
+ ok(true, "Testing word selection");
+ is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text");
+ is(textarea.selectionEnd, 3, "The end of the selection should not include a newline character");
+
+ textarea.selectionStart = textarea.selectionEnd = 0;
+
+ // Simulate a triple click on foo
+ synthesizeMouse(textarea, 5, 5, {clickCount: 3});
+
+ ok(true, "Testing line selection");
+ is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text");
+ is(textarea.selectionEnd, 3, "The end of the selection should not include a newline character");
+
+ textarea.selectionStart = textarea.selectionEnd = 0;
+ textarea.value = "Very very long value which would eventually overflow the visible section of the textarea";
+
+ // Simulate a quadruple click on Very
+ synthesizeMouse(textarea, 5, 5, {clickCount: 4});
+
+ ok(true, "Testing paragraph selection");
+ is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text");
+ is(textarea.selectionEnd, textarea.value.length, "The end of the selection should be the end of the paragraph");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug646194.html b/editor/libeditor/tests/test_bug646194.html
new file mode 100644
index 0000000000..5131e9cab0
--- /dev/null
+++ b/editor/libeditor/tests/test_bug646194.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<title>Mozilla Bug 646194</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=646194"
+ target="_blank">Mozilla Bug 646194</a>
+<iframe id="i" srcdoc="&lt;div contenteditable=true id=t&gt;test me now&lt;/div&gt;"></iframe>
+<script>
+function runTest() {
+ var i = document.getElementById("i");
+ i.focus();
+ var win = i.contentWindow;
+ var doc = i.contentDocument;
+ var t = doc.getElementById("t");
+ t.focus();
+ // put the caret at the end
+ win.getSelection().collapse(t.firstChild, 11);
+
+ // Simulate pression Option+Delete on Mac
+ // We do things this way because not every platform can invoke this
+ // command using the available key bindings.
+ SpecialPowers.doCommand(window, "cmd_wordPrevious");
+ SpecialPowers.doCommand(window, "cmd_wordPrevious");
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+
+ // If we reach here, we haven't crashed. Phew!
+ // But let's check the value too, now that we're here.
+ is(t.textContent, "me now", "The command has worked correctly");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
diff --git a/editor/libeditor/tests/test_bug668599.html b/editor/libeditor/tests/test_bug668599.html
new file mode 100644
index 0000000000..1bf9a64075
--- /dev/null
+++ b/editor/libeditor/tests/test_bug668599.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=668599
+-->
+<head>
+ <title>Test for Bug 668599</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=668599">Mozilla Bug 668599</a>
+<p id="display"></p>
+<div id="content">
+ <div id="test1">
+ block <span contenteditable>type here</span> block
+ </div>
+ <div id="test2">
+ <p contenteditable>
+ block <span>type here</span> block
+ </p>
+ </div>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 668599 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function select(element) {
+ // select the element text content
+ var userSelection = window.getSelection();
+ window.getSelection().removeAllRanges();
+ var range = document.createRange();
+ range.setStart(element.firstChild, 0);
+ range.setEnd(element.firstChild, element.textContent.length);
+ userSelection.addRange(range);
+}
+
+function runTests() {
+ var span = document.querySelector("#test1 span");
+
+ // editable <span> => the <span> *content* should be deleted
+ select(span);
+ span.focus();
+ sendString("x");
+ is(span.textContent, "x", "The <span> content should have been replaced by 'x'.");
+
+ // same thing, but using [Del] instead of typing some text
+ document.execCommand("Undo", false, null);
+ select(span);
+ span.focus();
+ synthesizeKey("KEY_Delete");
+ is(span.textContent, "", "The <span> content should have been deleted.");
+
+ // <span> in editable block => the <span> *element* should be deleted
+ select(document.querySelector("#test2 span"));
+ document.querySelector("#test2 [contenteditable]").focus();
+ synthesizeKey("KEY_Delete");
+ is(document.querySelector("#test2 span"), null,
+ "The <span> element should have been deleted.");
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug674770-1.html b/editor/libeditor/tests/test_bug674770-1.html
new file mode 100644
index 0000000000..0b6089e0ef
--- /dev/null
+++ b/editor/libeditor/tests/test_bug674770-1.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674770
+-->
+<head>
+ <title>Test for Bug 674770</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674770">Mozilla Bug 674770</a>
+<p id="display"></p>
+<div id="content">
+<a href="file_bug674770-1.html" id="link1">test</a>
+<div contenteditable>
+<a href="file_bug674770-1.html" id="link2">test</a>
+</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set": [["middlemouse.paste", true]]}, startTests);
+});
+
+function startTests() {
+ var tests = [
+ { description: "Testing link in <div>: ",
+ target() { return document.querySelector("#link1"); },
+ linkShouldWork: true },
+ { description: "Testing link in <div contenteditable>: ",
+ target() { return document.querySelector("#link2"); },
+ linkShouldWork: false },
+ ];
+ var currentTest;
+ function runNextTest() {
+ localStorage.removeItem("clicked");
+ currentTest = tests.shift();
+ if (!currentTest) {
+ SimpleTest.finish();
+ return;
+ }
+ ok(true, currentTest.description + "Starting to test...");
+ synthesizeMouseAtCenter(currentTest.target(), { button: 1 });
+ }
+
+
+ addEventListener("storage", function(e) {
+ is(e.key, "clicked", currentTest.description + "Key should always be 'clicked'");
+ is(e.newValue, "true", currentTest.description + "Value should always be 'true'");
+ ok(currentTest.linkShouldWork, currentTest.description + "The click operation on the link " + (currentTest.linkShouldWork ? "should work" : "shouldn't work"));
+ SimpleTest.executeSoon(runNextTest);
+ }, false);
+
+ SpecialPowers.addSystemEventListener(window, "auxclick", function(aEvent) {
+ // When the click event should cause default action, e.g., opening the link,
+ // the event shouldn't have been consumed except the link handler.
+ // However, in e10s mode, it's not consumed during propagating the event but
+ // in non-e10s mode, it's consumed during the propagation. Therefore,
+ // we cannot check defaultPrevented value when the link should work as is
+ // if there is no way to detect if it's running in e10s mode or not.
+ // So, let's skip checking Event.defaultPrevented value when the link should
+ // work. In such case, we should receive "storage" event later.
+ if (currentTest.linkShouldWork) {
+ return;
+ }
+
+ ok(SpecialPowers.defaultPreventedInAnyGroup(aEvent),
+ currentTest.description + "The default action should be consumed because the link should work as is");
+ if (SpecialPowers.defaultPreventedInAnyGroup(aEvent)) {
+ // In this case, "storage" event won't be fired.
+ SimpleTest.executeSoon(runNextTest);
+ }
+ }, false);
+
+ SimpleTest.executeSoon(runNextTest);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug674770-2.html b/editor/libeditor/tests/test_bug674770-2.html
new file mode 100644
index 0000000000..9b05277052
--- /dev/null
+++ b/editor/libeditor/tests/test_bug674770-2.html
@@ -0,0 +1,374 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674770
+-->
+<head>
+ <title>Test for Bug 674770</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674770">Mozilla Bug 674770</a>
+<p id="display"></p>
+<div id="content">
+<iframe id="iframe" style="display: block; height: 14em;"
+ srcdoc="<input id='text' value='pasted'><input id='input' style='display: block;'><p id='editor1' contenteditable>editor1:</p><p id='editor2' contenteditable>editor2:</p>"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 674770 **/
+SimpleTest.waitForExplicitFinish();
+
+var iframe = document.getElementById("iframe");
+var frameWindow, frameDocument;
+
+var gClicked = false;
+var gClicking = null;
+var gDoPreventDefault1 = null;
+var gDoPreventDefault2 = null;
+
+function clickEventHandler(aEvent) {
+ if (aEvent.button == 1 && aEvent.target == gClicking) {
+ gClicked = true;
+ }
+ if (gDoPreventDefault1 == aEvent.target) {
+ aEvent.preventDefault();
+ }
+ if (gDoPreventDefault2 == aEvent.target) {
+ aEvent.preventDefault();
+ }
+}
+
+// NOTE: tests need to check the result *after* the content is actually
+// modified. Sometimes, the modification is delayed. Therefore, there
+// are a lot of functions and SimpleTest.executeSoon()s.
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set": [["middlemouse.contentLoadURL", false],
+ ["middlemouse.paste", true]]}, startTest);
+});
+function startTest() {
+ frameWindow = iframe.contentWindow;
+ frameDocument = iframe.contentDocument;
+
+ frameDocument.getElementById("input").addEventListener("click", clickEventHandler);
+ frameDocument.getElementById("editor1").addEventListener("click", clickEventHandler);
+ frameDocument.getElementById("editor2").addEventListener("click", clickEventHandler);
+
+ var text = frameDocument.getElementById("text");
+
+ text.focus();
+ synthesizeKey("a", { accelKey: true }, frameWindow);
+ // Windows and Mac don't have primary selection, we should copy the text to
+ // the global clipboard.
+ if (!SpecialPowers.supportsSelectionClipboard()) {
+ SimpleTest.waitForClipboard("pasted",
+ function() { synthesizeKey("c", { accelKey: true }, frameWindow); },
+ function() { SimpleTest.executeSoon(runInputTests1); },
+ cleanup);
+ } else {
+ // Otherwise, don't call waitForClipboard since it breaks primary
+ // selection.
+ runInputTests1();
+ }
+}
+
+function runInputTests1() {
+ var input = frameDocument.getElementById("input");
+
+ // first, copy text.
+
+ // when middle clicked in focused input element, text should be pasted.
+ input.value = "";
+ input.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = input;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(input, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runInputTests1");
+ is(input.value, "pasted", "failed to paste in input element");
+
+ SimpleTest.executeSoon(runInputTests2);
+ });
+ });
+}
+
+function runInputTests2() {
+ var input = frameDocument.getElementById("input");
+
+ // even if the input element hasn't had focus, middle click should set focus
+ // to it and paste the text.
+ input.value = "";
+ input.blur();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = input;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(input, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runInputTests2");
+ is(input.value, "pasted",
+ "failed to paste in input element when it hasn't had focus yet");
+
+ SimpleTest.executeSoon(runInputTests3);
+ });
+ });
+}
+
+function runInputTests3() {
+ var input = frameDocument.getElementById("input");
+ var editor1 = frameDocument.getElementById("editor1");
+
+ // preventDefault() of HTML editor's click event handler shouldn't prevent
+ // middle click pasting in input element.
+ input.value = "";
+ input.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = input;
+ gDoPreventDefault1 = editor1;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(input, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runInputTests3");
+ is(input.value, "pasted",
+ "failed to paste in input element when editor1 does preventDefault()");
+
+ SimpleTest.executeSoon(runInputTests4);
+ });
+ });
+}
+
+function runInputTests4() {
+ var input = frameDocument.getElementById("input");
+
+ // preventDefault() of input element's click event handler should prevent
+ // middle click pasting in it.
+ input.value = "";
+ input.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = input;
+ gDoPreventDefault1 = input;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(input, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runInputTests4");
+ todo_is(input.value, "",
+ "pasted in input element when it does preventDefault()");
+
+ SimpleTest.executeSoon(runContentEditableTests1);
+ });
+ });
+}
+
+function runContentEditableTests1() {
+ var editor1 = frameDocument.getElementById("editor1");
+
+ // when middle clicked in focused contentediable editor, text should be
+ // pasted.
+ editor1.innerHTML = "editor1:";
+ editor1.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor1;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests1");
+ is(editor1.innerHTML, "editor1:pasted",
+ "failed to paste text in contenteditable editor");
+ SimpleTest.executeSoon(runContentEditableTests2);
+ });
+ });
+}
+
+function runContentEditableTests2() {
+ var editor1 = frameDocument.getElementById("editor1");
+
+ // even if the contenteditable editor hasn't had focus, middle click should
+ // set focus to it and paste the text.
+ editor1.innerHTML = "editor1:";
+ editor1.blur();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor1;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests2");
+ is(editor1.innerHTML, "editor1:pasted",
+ "failed to paste in contenteditable editor #1 when it hasn't had focus yet");
+ SimpleTest.executeSoon(runContentEditableTests3);
+ });
+ });
+}
+
+function runContentEditableTests3() {
+ var editor1 = frameDocument.getElementById("editor1");
+ var editor2 = frameDocument.getElementById("editor2");
+
+ // When editor1 has focus but editor2 is middle clicked, should be pasted
+ // in the editor2.
+ editor1.innerHTML = "editor1:";
+ editor2.innerHTML = "editor2:";
+ editor1.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor2;
+ gDoPreventDefault1 = null;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor2, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests3");
+ is(editor1.innerHTML, "editor1:",
+ "pasted in contenteditable editor #1 when editor2 is clicked");
+ is(editor2.innerHTML, "editor2:pasted",
+ "failed to paste in contenteditable editor #2 when editor2 is clicked");
+ SimpleTest.executeSoon(runContentEditableTests4);
+ });
+ });
+}
+
+function runContentEditableTests4() {
+ var editor1 = frameDocument.getElementById("editor1");
+
+ // preventDefault() of editor1's click event handler should prevent
+ // middle click pasting in it.
+ editor1.innerHTML = "editor1:";
+ editor1.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor1;
+ gDoPreventDefault1 = editor1;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests4");
+ todo_is(editor1.innerHTML, "editor1:",
+ "pasted in contenteditable editor #1 when it does preventDefault()");
+ SimpleTest.executeSoon(runContentEditableTests5);
+ });
+ });
+}
+
+function runContentEditableTests5() {
+ var editor1 = frameDocument.getElementById("editor1");
+ var editor2 = frameDocument.getElementById("editor2");
+
+ // preventDefault() of editor1's click event handler shouldn't prevent
+ // middle click pasting in editor2.
+ editor1.innerHTML = "editor1:";
+ editor2.innerHTML = "editor2:";
+ editor2.focus();
+
+ SimpleTest.executeSoon(function() {
+ gClicked = false;
+ gClicking = editor2;
+ gDoPreventDefault1 = editor1;
+ gDoPreventDefault2 = null;
+
+ synthesizeMouseAtCenter(editor2, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ todo(gClicked, "click event hasn't been fired for runContentEditableTests5");
+ is(editor1.innerHTML, "editor1:",
+ "pasted in contenteditable editor #1?");
+ is(editor2.innerHTML, "editor2:pasted",
+ "failed to paste in contenteditable editor #2");
+
+ SimpleTest.executeSoon(initForBodyEditableDocumentTests);
+ });
+ });
+}
+
+function initForBodyEditableDocumentTests() {
+ frameDocument.getElementById("input").removeEventListener("click", clickEventHandler);
+ frameDocument.getElementById("editor1").removeEventListener("click", clickEventHandler);
+ frameDocument.getElementById("editor2").removeEventListener("click", clickEventHandler);
+
+ iframe.onload =
+ function(aEvent) { SimpleTest.executeSoon(runBodyEditableDocumentTests1); };
+ iframe.srcdoc = "<body contenteditable>body:</body>";
+}
+
+function runBodyEditableDocumentTests1() {
+ frameWindow = iframe.contentWindow;
+ frameDocument = iframe.contentDocument;
+
+ var body = frameDocument.body;
+
+ is(body.innerHTML, "body:",
+ "failed to initialize at runBodyEditableDocumentTests1");
+
+ // middle click on html element should cause pasting text in its body.
+ synthesizeMouseAtCenter(frameDocument.documentElement, {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ is(body.innerHTML,
+ "body:pasted",
+ "failed to paste in editable body element when clicked on html element");
+
+ SimpleTest.executeSoon(runBodyEditableDocumentTests2);
+ });
+}
+
+function runBodyEditableDocumentTests2() {
+ frameDocument.body.innerHTML = "body:<span id='span' contenteditable='false'>non-editable</span>";
+
+ var body = frameDocument.body;
+
+ is(body.innerHTML, "body:<span id=\"span\" contenteditable=\"false\">non-editable</span>",
+ "failed to initialize at runBodyEditableDocumentTests2");
+
+ synthesizeMouseAtCenter(frameDocument.getElementById("span"), {button: 1}, frameWindow);
+
+ SimpleTest.executeSoon(function() {
+ is(body.innerHTML,
+ "body:<span id=\"span\" contenteditable=\"false\">non-editable</span>",
+ "pasted when middle clicked in non-editable element");
+
+ SimpleTest.executeSoon(cleanup);
+ });
+}
+
+function cleanup() {
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug674861.html b/editor/libeditor/tests/test_bug674861.html
new file mode 100644
index 0000000000..a103bb3c40
--- /dev/null
+++ b/editor/libeditor/tests/test_bug674861.html
@@ -0,0 +1,194 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674861
+-->
+<head>
+ <title>Test for Bug 674861</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674861">Mozilla Bug 674861</a>
+<p id="display"></p>
+<div id="content">
+ <section id="test1">
+ <h2> Editable Bullet List </h2>
+ <ul contenteditable>
+ <li> item A </li>
+ <li> item B </li>
+ <li> item C </li>
+ </ul>
+
+ <h2> Editable Ordered List </h2>
+ <ol contenteditable>
+ <li> item A </li>
+ <li> item B </li>
+ <li> item C </li>
+ </ol>
+
+ <h2> Editable Definition List </h2>
+ <dl contenteditable>
+ <dt> term A </dt>
+ <dd> definition A </dd>
+ <dt> term B </dt>
+ <dd> definition B </dd>
+ <dt> term C </dt>
+ <dd> definition C </dd>
+ </dl>
+ </section>
+
+ <section id="test2" contenteditable>
+ <h2> Bullet List In Editable Section </h2>
+ <ul>
+ <li> item A </li>
+ <li> item B </li>
+ <li> item C </li>
+ </ul>
+
+ <h2> Ordered List In Editable Section </h2>
+ <ol>
+ <li> item A </li>
+ <li> item B </li>
+ <li> item C </li>
+ </ol>
+
+ <h2> Definition List In Editable Section </h2>
+ <dl>
+ <dt> term A </dt>
+ <dd> definition A </dd>
+ <dt> term B </dt>
+ <dd> definition B </dd>
+ <dt> term C </dt>
+ <dd> definition C </dd>
+ </dl>
+ </section>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 674861 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+const CARET_BEGIN = 0;
+const CARET_MIDDLE = 1;
+const CARET_END = 2;
+
+function try2split(element, caretPos) {
+ // compute the requested position
+ var len = element.textContent.length;
+ var pos = -1;
+ switch (caretPos) {
+ case CARET_BEGIN:
+ pos = 0;
+ break;
+ case CARET_MIDDLE:
+ pos = Math.floor(len / 2);
+ break;
+ case CARET_END:
+ pos = len;
+ break;
+ }
+
+ // put the caret on the requested position
+ var sel = window.getSelection();
+ for (var i = 0; i < sel.rangeCount; i++) {
+ var range = sel.getRangeAt(i);
+ sel.removeRange(range);
+ }
+ range = document.createRange();
+ range.setStart(element.firstChild, pos);
+ range.setEnd(element.firstChild, pos);
+ sel.addRange(range);
+
+ // simulates two [Return] keypresses
+ synthesizeKey("KEY_Enter");
+ synthesizeKey("KEY_Enter");
+}
+
+function runTests() {
+ const test1 = document.getElementById("test1");
+ const test2 = document.getElementById("test2");
+
+ // -----------------------------------------------------------------------
+ // #test1: editable lists should NOT be splittable
+ // -----------------------------------------------------------------------
+ const ul = test1.querySelector("ul");
+ const ol = test1.querySelector("ol");
+ const dl = test1.querySelector("dl");
+
+ // bullet list
+ ul.focus();
+ try2split(ul.querySelector("li"), CARET_END);
+ is(test1.querySelectorAll("ul").length, 1,
+ "The <ul contenteditable> list should not be splittable.");
+ is(ul.querySelectorAll("li").length, 5,
+ "Two new <li> elements should have been created.");
+
+ // ordered list
+ ol.focus();
+ try2split(ol.querySelector("li"), CARET_END);
+ is(test1.querySelectorAll("ol").length, 1,
+ "The <ol contenteditable> list should not be splittable.");
+ is(ol.querySelectorAll("li").length, 5,
+ "Two new <li> elements should have been created.");
+
+ // definition list
+ dl.focus();
+ try2split(dl.querySelector("dd"), CARET_END);
+ is(test1.querySelectorAll("dl").length, 1,
+ "The <dl contenteditable> list should not be splittable.");
+ is(dl.querySelectorAll("dt").length, 5,
+ "Two new <dt> elements should have been created.");
+
+ // -----------------------------------------------------------------------
+ // #test2: lists in editable blocks should be splittable
+ // -----------------------------------------------------------------------
+ test2.focus();
+
+ function testNewParagraph(expected) {
+ // bullet list
+ try2split(test2.querySelector("ul li"), CARET_END);
+ is(test2.querySelectorAll("ul").length, 2,
+ "The <ul> list should have been splitted.");
+ is(test2.querySelectorAll("ul li").length, 3,
+ "No new <li> element should have been created.");
+ is(test2.querySelectorAll("ul+" + expected).length, 1,
+ "A new " + expected + " should have been created in the <ul>.");
+
+ // ordered list
+ try2split(test2.querySelector("ol li"), CARET_END);
+ is(test2.querySelectorAll("ol").length, 2,
+ "The <ol> list should have been splitted.");
+ is(test2.querySelectorAll("ol li").length, 3,
+ "No new <li> element should have been created.");
+ is(test2.querySelectorAll("ol+" + expected).length, 1,
+ "A new " + expected + " should have been created in the <ol>.");
+
+ // definition list
+ try2split(test2.querySelector("dl dd"), CARET_END);
+ is(test2.querySelectorAll("dl").length, 2,
+ "The <dl> list should have been splitted.");
+ is(test2.querySelectorAll("dt").length, 3,
+ "No new <dt> element should have been created.");
+ is(test2.querySelectorAll("dl+" + expected).length, 1,
+ "A new " + expected + " should have been created in the <dl>.");
+ }
+
+ document.execCommand("defaultParagraphSeparator", false, "div");
+ testNewParagraph("div");
+ document.execCommand("defaultParagraphSeparator", false, "p");
+ testNewParagraph("p");
+ document.execCommand("defaultParagraphSeparator", false, "br");
+ testNewParagraph("p");
+
+ // done
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug676401.html b/editor/libeditor/tests/test_bug676401.html
new file mode 100644
index 0000000000..35653461ae
--- /dev/null
+++ b/editor/libeditor/tests/test_bug676401.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=676401
+-->
+<head>
+ <title>Test for Bug 676401</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=676401">Mozilla Bug 676401</a>
+<p id="display"></p>
+<div id="content">
+ <!-- we need a blockquote to test the "outdent" command -->
+ <section>
+ <blockquote> not editable </blockquote>
+ </section>
+ <section contenteditable>
+ <blockquote> editable </blockquote>
+ </section>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 676401 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+var gBlock1, gBlock2;
+
+var alwaysEnabledCommands = [
+ "contentReadOnly",
+ "copy",
+ "cut",
+ "enableInlineTableEditing",
+ "enableObjectResizing",
+ "insertBrOnReturn",
+ "selectAll",
+ "styleWithCSS",
+];
+
+function ensureNobodyHasFocus() {
+ document.activeElement.blur();
+}
+
+function IsCommandEnabled(command) {
+ ensureNobodyHasFocus();
+
+ // non-editable div: should return false unless alwaysEnabled
+ window.getSelection().selectAllChildren(gBlock1);
+ is(
+ document.queryCommandEnabled(command),
+ alwaysEnabledCommands.includes(command) && document.queryCommandSupported(command),
+ "'" + command + "' should not be enabled on a non-editable block."
+ );
+
+ // editable div: should return true if it's supported
+ window.getSelection().selectAllChildren(gBlock2);
+ is(
+ document.queryCommandEnabled(command),
+ document.queryCommandSupported(command),
+ "'" + command + "' should be enabled on an editable block."
+ );
+}
+
+function runTests() {
+ var i, commands;
+ gBlock1 = document.querySelector("#content section blockquote");
+ gBlock2 = document.querySelector("#content [contenteditable] blockquote");
+
+ // common commands: test with and without "styleWithCSS"
+ commands = [
+ "bold", "italic", "underline", "strikeThrough",
+ "subscript", "superscript", "foreColor", "backColor", "hiliteColor",
+ "fontName", "fontSize",
+ "justifyLeft", "justifyCenter", "justifyRight", "justifyFull",
+ "indent", "outdent",
+ "insertOrderedList", "insertUnorderedList", "insertParagraph",
+ "heading", "formatBlock",
+ "contentReadOnly", "createLink",
+ "decreaseFontSize", "increaseFontSize",
+ "insertHTML", "insertHorizontalRule", "insertImage",
+ "removeFormat", "selectAll", "styleWithCSS",
+ ];
+ document.execCommand("styleWithCSS", false, false);
+ for (i = 0; i < commands.length; i++)
+ IsCommandEnabled(commands[i]);
+ document.execCommand("styleWithCSS", false, true);
+ for (i = 0; i < commands.length; i++)
+ IsCommandEnabled(commands[i]);
+
+ // Mozilla-specific stuff
+ commands = ["enableInlineTableEditing", "enableObjectResizing", "insertBrOnReturn"];
+ for (i = 0; i < commands.length; i++)
+ IsCommandEnabled(commands[i]);
+
+ // These are privileged, and available only to chrome.
+ ensureNobodyHasFocus();
+ window.getSelection().selectAllChildren(gBlock2);
+ commands = ["paste"];
+ for (i = 0; i < commands.length; i++) {
+ is(document.queryCommandEnabled(commands[i]), false,
+ "Command should not be enabled for non-privileged code");
+ is(SpecialPowers.wrap(document).queryCommandEnabled(commands[i]), true,
+ "Command should be enabled for privileged code");
+ is(document.execCommand(commands[i], false, false), false, "Should return false: " + commands[i]);
+ is(SpecialPowers.wrap(document).execCommand(commands[i], false, false), true, "Should return true: " + commands[i]);
+ }
+
+ // delete/undo/redo -- we have to execute this commands because:
+ // * there's nothing to undo if we haven't modified the selection first
+ // * there's nothing to redo if we haven't undone something first
+ commands = ["delete", "undo", "redo"];
+ for (i = 0; i < commands.length; i++) {
+ IsCommandEnabled(commands[i]);
+ document.execCommand(commands[i], false, false);
+ }
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug677752.html b/editor/libeditor/tests/test_bug677752.html
new file mode 100644
index 0000000000..cadbbf4c0d
--- /dev/null
+++ b/editor/libeditor/tests/test_bug677752.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=677752
+-->
+<head>
+ <title>Test for Bug 677752</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=677752">Mozilla Bug 677752</a>
+<p id="display"></p>
+<div id="content">
+ <section contenteditable> foo bar </section>
+ <div contenteditable> foo bar </div>
+ <p contenteditable> foo bar </p>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 677752 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function selectEditor(aEditor) {
+ aEditor.focus();
+ var selection = window.getSelection();
+ selection.selectAllChildren(aEditor);
+ selection.collapseToStart();
+}
+
+function runTests() {
+ var editor, node, initialHTML;
+ document.execCommand("styleWithCSS", false, true);
+
+ // editable <section>
+ editor = document.querySelector("section[contenteditable]");
+ initialHTML = editor.innerHTML;
+ selectEditor(editor);
+ // editable <section>: justify
+ document.execCommand("justifyright", false, null);
+ node = editor.querySelector("*");
+ is(node.nodeName.toLowerCase(), "div", "'justifyright' should create a <div> in the editable <section>.");
+ is(node.style.textAlign, "right", "'justifyright' should create a 'text-align: right' CSS rule.");
+ document.execCommand("undo", false, null);
+ // editable <section>: indent
+ document.execCommand("indent", false, null);
+ node = editor.querySelector("*");
+ is(node.nodeName.toLowerCase(), "div", "'indent' should create a <div> in the editable <section>.");
+ is(node.style.marginLeft, "40px", "'indent' should create a 'margin-left: 40px' CSS rule.");
+ // editable <section>: undo with outdent
+ // this should remove the whole <div> but only removing the CSS rule would be acceptable, too
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "'outdent' should undo the 'indent' action.");
+ // editable <section>: outdent again
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "another 'outdent' should not modify the <section> element.");
+
+ // editable <div>
+ editor = document.querySelector("div[contenteditable]");
+ initialHTML = editor.innerHTML;
+ selectEditor(editor);
+ // editable <div>: justify
+ document.execCommand("justifyright", false, null);
+ node = editor.querySelector("*");
+ is(node.nodeName.toLowerCase(), "div", "'justifyright' should create a <div> in the editable <div>.");
+ is(node.style.textAlign, "right", "'justifyright' should create a 'text-align: right' CSS rule.");
+ document.execCommand("undo", false, null);
+ // editable <div>: indent
+ document.execCommand("indent", false, null);
+ node = editor.querySelector("*");
+ is(node.nodeName.toLowerCase(), "div", "'indent' should create a <div> in the editable <div>.");
+ is(node.style.marginLeft, "40px", "'indent' should create a 'margin-left: 40px' CSS rule.");
+ // editable <div>: undo with outdent
+ // this should remove the whole <div> but only removing the CSS rule would be acceptable, too
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "'outdent' should undo the 'indent' action.");
+ // editable <div>: outdent again
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "another 'outdent' should not modify the <div> element.");
+
+ // editable <p>
+ // all block-level commands should be ignored (<p><div/></p> is not valid)
+ editor = document.querySelector("p[contenteditable]");
+ initialHTML = editor.innerHTML;
+ selectEditor(editor);
+ // editable <p>: justify
+ document.execCommand("justifyright", false, null);
+ is(editor.innerHTML, initialHTML, "'justifyright' should have no effect on <p>.");
+ // editable <p>: indent
+ document.execCommand("indent", false, null);
+ is(editor.innerHTML, initialHTML, "'indent' should have no effect on <p>.");
+ // editable <p>: outdent
+ document.execCommand("outdent", false, null);
+ is(editor.innerHTML, initialHTML, "'outdent' should have no effect on <p>.");
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug681229.html b/editor/libeditor/tests/test_bug681229.html
new file mode 100644
index 0000000000..f63c37329c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug681229.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=681229
+-->
+<head>
+ <title>Test for Bug 681229</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=681229">Mozilla Bug 681229</a>
+<p id="display"></p>
+<div id="content">
+<textarea spellcheck="false"></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 681229 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+
+ const kValue = "a\r\nb";
+
+ SimpleTest.waitForClipboard(
+ kValue.replace(/\r\n?/g, "\n"),
+ function() {
+ SpecialPowers.clipboardCopyString(kValue);
+ },
+ function() {
+ synthesizeKey("V", {accelKey: true});
+ is(t.value, "a\nb", "The carriage return has been correctly sanitized");
+ SimpleTest.finish();
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug686203.html b/editor/libeditor/tests/test_bug686203.html
new file mode 100644
index 0000000000..d27c3555fc
--- /dev/null
+++ b/editor/libeditor/tests/test_bug686203.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=686203
+-->
+
+<head>
+ <title>Test for Bug 686203</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=686203">Mozilla Bug 686203</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 686203 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ var ce = document.getElementById("ce");
+ var input = document.getElementById("input");
+ ce.focus();
+
+ var eventDetails = { button: 2 };
+ synthesizeMouseAtCenter(input, eventDetails);
+
+ sendString("Z");
+
+ /* check values */
+ is(input.value, "Z", "input correctly focused after right-click");
+ is(ce.textContent, "abc", "contenteditable correctly blurred after right-click on input");
+
+ SimpleTest.finish();
+ });
+ </script>
+ </pre>
+
+ <input type="text" value="" id="input" />
+ <div id="ce" contenteditable="true">abc</div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug692520.html b/editor/libeditor/tests/test_bug692520.html
new file mode 100644
index 0000000000..68c7d5d11c
--- /dev/null
+++ b/editor/libeditor/tests/test_bug692520.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=692520
+-->
+<head>
+ <title>Test for Bug 692520</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=692520">Mozilla Bug 692520</a>
+<p id="display"></p>
+<div id="content">
+<textarea></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 692520 **/
+function test(prop, value) {
+ var t = document.querySelector("textarea");
+ t.value = "testing";
+ t.selectionStart = 1;
+ t.selectionEnd = 3;
+ t.selectionDirection = "backward";
+ t.style.display = "";
+ document.body.clientWidth;
+ t.style.display = "none";
+ is(t[prop], value, "Correct value for the " + prop + " property");
+}
+
+test("selectionStart", 1);
+test("selectionEnd", 3);
+test("selectionDirection", "backward");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug697842.html b/editor/libeditor/tests/test_bug697842.html
new file mode 100644
index 0000000000..5b39abbace
--- /dev/null
+++ b/editor/libeditor/tests/test_bug697842.html
@@ -0,0 +1,114 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=697842
+-->
+<head>
+ <title>Test for Bug 697842</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <p id="editor" contenteditable style="min-height: 1.5em;"></p>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 697842 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+ var editor = document.getElementById("editor");
+ editor.focus();
+
+ SimpleTest.executeSoon(function() {
+ var composingString = "";
+
+ function handler(aEvent) {
+ switch (aEvent.type) {
+ case "compositionstart":
+ // Selected string at starting composition must be empty in this test.
+ is(aEvent.data, "", "mismatch selected string");
+ break;
+ case "compositionupdate":
+ case "compositionend":
+ is(aEvent.data, composingString, "mismatch composition string");
+ break;
+ default:
+ break;
+ }
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ }
+
+ editor.addEventListener("compositionstart", handler, true);
+ editor.addEventListener("compositionend", handler, true);
+ editor.addEventListener("compositionupdate", handler, true);
+
+ // input first character
+ composingString = "\u306B";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": composingString,
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": 1, "length": 0 },
+ });
+
+ // input second character
+ composingString = "\u306B\u3085";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": composingString,
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+
+ // convert them
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": composingString,
+ "clauses":
+ [
+ { "length": 2,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ ],
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+
+ synthesizeComposition({ type: "compositioncommitasis" });
+
+ is(editor.innerHTML, composingString,
+ "editor has unexpected result");
+
+ editor.removeEventListener("compositionstart", handler, true);
+ editor.removeEventListener("compositionend", handler, true);
+ editor.removeEventListener("compositionupdate", handler, true);
+ editor.removeEventListener("text", handler, true);
+
+ SimpleTest.finish();
+ });
+}
+
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug725069.html b/editor/libeditor/tests/test_bug725069.html
new file mode 100644
index 0000000000..dbbca5a13a
--- /dev/null
+++ b/editor/libeditor/tests/test_bug725069.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=725069
+-->
+<head>
+ <title>Test for Bug 725069</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body contenteditable>abc<!-- XXX -->def<span></span>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=725069">Mozilla Bug 725069</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 725069 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var body = document.querySelector("body");
+ is(body.firstChild.nodeType, body.TEXT_NODE, "The first node is a text node");
+ is(body.firstChild.nodeValue, "abc", "The first text node is there");
+ is(body.firstChild.nextSibling.nodeType, body.COMMENT_NODE, "The second node is a comment node");
+ is(body.firstChild.nextSibling.nodeValue, " XXX ", "The value of the comment node is not changed");
+ is(body.firstChild.nextSibling.nextSibling.nodeType, body.TEXT_NODE, "The last text node is a text node");
+ is(body.firstChild.nextSibling.nextSibling.nodeValue, "def", "The last next node is there");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug735059.html b/editor/libeditor/tests/test_bug735059.html
new file mode 100644
index 0000000000..3b81ce48ba
--- /dev/null
+++ b/editor/libeditor/tests/test_bug735059.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=735059
+-->
+<title>Test for Bug 735059</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=735059">Mozilla Bug 735059</a>
+<div id="display" contenteditable>foo</div>
+<pre id="test">
+<script>
+/** Test for Bug 735059 **/
+
+// Value defaults to the empty string, which evaluates to true, so this
+// disables CSS styling
+document.execCommand("usecss");
+getSelection().selectAllChildren(document.getElementById("display"));
+document.execCommand("bold");
+is(document.getElementById("display").innerHTML, "<b>foo</b>",
+ "execCommand() needs to work with only one parameter");
+</script>
+</pre>
diff --git a/editor/libeditor/tests/test_bug738366.html b/editor/libeditor/tests/test_bug738366.html
new file mode 100644
index 0000000000..a54aec7a2f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug738366.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=738366
+-->
+<title>Test for Bug 738366</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=738366">Mozilla Bug 738366</a>
+<div id="display" contenteditable>foobarbaz</div>
+<script>
+/** Test for Bug 738366 **/
+
+getSelection().collapse(document.getElementById("display").firstChild, 3);
+getSelection().extend(document.getElementById("display").firstChild, 6);
+document.execCommand("bold");
+is(document.getElementById("display").innerHTML, "foo<b>bar</b>baz",
+ "styleWithCSS must default to false");
+document.execCommand("stylewithcss", false, "true");
+document.execCommand("bold");
+document.execCommand("bold");
+is(document.getElementById("display").innerHTML,
+ 'foo<span style="font-weight: bold;">bar</span>baz',
+ "styleWithCSS must be settable to true");
+</script>
diff --git a/editor/libeditor/tests/test_bug740784.html b/editor/libeditor/tests/test_bug740784.html
new file mode 100644
index 0000000000..74e8374385
--- /dev/null
+++ b/editor/libeditor/tests/test_bug740784.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=740784
+-->
+
+<head>
+ <title>Test for Bug 740784</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=740784">Mozilla Bug 740784</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 740784 **/
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ var t1 = $("t1");
+
+ t1.focus();
+ synthesizeKey("KEY_End");
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("z", {accelKey: true});
+
+ is(t1.value, "a", "trailing <br> correctly ignored");
+
+ SimpleTest.finish();
+ });
+ </script>
+ </pre>
+
+ <textarea id="t1" rows="2" columns="80">a</textarea>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug742261.html b/editor/libeditor/tests/test_bug742261.html
new file mode 100644
index 0000000000..9ad41dd52b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug742261.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=742261
+-->
+<title>Test for Bug 742261</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<body>
+<script>
+is(document.execCommandShowHelp, undefined,
+ "execCommandShowHelp shouldn't exist");
+is(document.queryCommandText, undefined,
+ "queryCommandText shouldn't exist");
+</script>
diff --git a/editor/libeditor/tests/test_bug757371.html b/editor/libeditor/tests/test_bug757371.html
new file mode 100644
index 0000000000..5ca41a5951
--- /dev/null
+++ b/editor/libeditor/tests/test_bug757371.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=757371
+-->
+<title>Test for Bug 757371</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=757371">Mozilla Bug 757371</a>
+<div contenteditable></div>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.querySelector("div");
+ div.focus();
+ getSelection().collapse(div, 0);
+ document.execCommand("bold");
+ sendString("ab");
+ sendKey("BACK_SPACE");
+ sendChar("b");
+
+ is(div.innerHTML, "<b>ab</b>");
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug757771.html b/editor/libeditor/tests/test_bug757771.html
new file mode 100644
index 0000000000..9ef980b662
--- /dev/null
+++ b/editor/libeditor/tests/test_bug757771.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=757771
+-->
+<title>Test for Bug 757771</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=757771">Mozilla Bug 757771</a>
+<input value=foo maxlength=4>
+<input type=password value=password>
+<script>
+/** Test for Bug 757771 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var textInput = document.querySelector("input");
+ textInput.focus();
+ textInput.select();
+ sendString("abcde");
+
+ var passwordInput = document.querySelector("input + input");
+ passwordInput.focus();
+ passwordInput.select();
+ sendString("hunter2");
+
+ ok(true, "No real tests, just crashes/asserts");
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug772796.html b/editor/libeditor/tests/test_bug772796.html
new file mode 100644
index 0000000000..fbfab347cf
--- /dev/null
+++ b/editor/libeditor/tests/test_bug772796.html
@@ -0,0 +1,487 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=772796
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 772796</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style> .pre { white-space: pre } </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 772796</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="editable" contenteditable></div>
+
+<pre id="test">
+
+<script type="application/javascript">
+ var tests = [
+// FYI: Those tests were ported to join-pre-and-other-block.html
+/* 00*/[
+ "<div>test</div><pre>foobar\nbaz</pre>",
+ "<div>testfoobar</div><pre>baz</pre>", // expected
+],
+/* 01*/[
+ "<div>test</div><pre><b>foobar\nbaz</b></pre>",
+ "<div>test<b>foobar</b></div><pre><b>baz</b></pre>", // expected
+],
+/* 02*/[
+ "<div>test</div><pre><b>foo</b>bar\nbaz</pre>",
+ "<div>test<b>foo</b>bar</div><pre>baz</pre>", // expected
+],
+/* 03*/[
+ "<div>test</div><pre><b>foo</b>\nbar</pre>",
+ "<div>test<b>foo</b></div><pre>bar</pre>", // expected
+],
+/* 04*/[
+ "<div>test</div><pre><b>foo\n</b>bar\nbaz</pre>",
+ "<div>test<b>foo</b></div><pre>bar\nbaz</pre>", // expected
+],
+
+// The <br> after the foobar is unfortunate but is behaviour that hasn't changed in bug 772796.
+// FYI: Those tests were ported to join-pre-and-other-block.html
+/* 05*/[
+ "<div>test</div><pre>foobar<br>baz</pre>",
+ "<div>testfoobar</div><pre>baz</pre>", // expected
+],
+/* 06*/[
+ "<div>test</div><pre><b>foobar<br>baz</b></pre>",
+ "<div>test<b>foobar</b></div><pre><b>baz</b></pre>", // expected
+],
+
+// Some tests with block elements.
+// FYI: Those tests were ported to join-pre-and-other-block.html
+/* 07*/[
+ "<div>test</div><pre><div>foobar</div>baz</pre>",
+ "<div>testfoobar</div><pre>baz</pre>", // expected
+],
+/* 08*/[
+ "<div>test</div><pre>foobar<div>baz</div></pre>",
+ "<div>testfoobar</div><pre><div>baz</div></pre>", // expected
+],
+/* 09*/[
+ "<div>test</div><pre><div>foobar</div>baz\nfred</pre>",
+ "<div>testfoobar</div><pre>baz\nfred</pre>", // expected
+],
+/* 10*/[
+ "<div>test</div><pre>foobar<div>baz</div>\nfred</pre>",
+ "<div>testfoobar</div><pre><div>baz</div>\nfred</pre>", // expected
+],
+/* 11*/[
+ "<div>test</div><pre><div>foo\nbar</div>baz\nfred</pre>",
+ "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>", // expected
+],
+/* 12*/[
+ "<div>test</div><pre>foo<div>bar</div>baz\nfred</pre>",
+ "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>", // expected
+],
+
+// Repeating all tests above with the <pre> on a new line.
+// We know that backspace doesn't work (bug 1190161). Third argument shows the current outcome.
+/* 13-00*/[
+ "<div>test</div>\n<pre>foobar\nbaz</pre>",
+ "<div>testfoobar</div><pre>baz</pre>", // expected
+],
+/* 14-01*/[
+ "<div>test</div>\n<pre><b>foobar\nbaz</b></pre>",
+ "<div>test<b>foobar</b></div><pre><b>baz</b></pre>", // expected
+],
+/* 15-02*/[
+ "<div>test</div>\n<pre><b>foo</b>bar\nbaz</pre>",
+ "<div>test<b>foo</b>bar</div><pre>baz</pre>", // expected
+],
+/* 16-03*/[
+ "<div>test</div>\n<pre><b>foo</b>\nbar</pre>",
+ "<div>test<b>foo</b></div><pre>bar</pre>", // expected
+],
+/* 17-04*/[
+ "<div>test</div>\n<pre><b>foo\n</b>bar\nbaz</pre>",
+ "<div>test<b>foo</b></div><pre>bar\nbaz</pre>", // expected
+],
+/* 18-05*/[
+ "<div>test</div>\n<pre>foobar<br>baz</pre>",
+ "<div>testfoobar</div><pre>baz</pre>", // expected
+],
+/* 19-06*/[
+ "<div>test</div>\n<pre><b>foobar<br>baz</b></pre>",
+ "<div>test<b>foobar</b></div><pre><b>baz</b></pre>", // expected
+],
+/* 20-07*/[
+ "<div>test</div>\n<pre><div>foobar</div>baz</pre>",
+ "<div>testfoobar</div><pre>baz</pre>", // expected
+],
+/* 21-08*/[
+ "<div>test</div>\n<pre>foobar<div>baz</div></pre>",
+ "<div>testfoobar</div><pre><div>baz</div></pre>", // expected
+],
+/* 22-09*/[
+ "<div>test</div>\n<pre><div>foobar</div>baz\nfred</pre>",
+ "<div>testfoobar</div><pre>baz\nfred</pre>", // expected
+],
+/* 23-10*/[
+ "<div>test</div>\n<pre>foobar<div>baz</div>\nfred</pre>",
+ "<div>testfoobar</div><pre><div>baz</div>\nfred</pre>", // expected
+],
+/* 24-11*/[
+ "<div>test</div>\n<pre><div>foo\nbar</div>baz\nfred</pre>",
+ "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>", // expected
+],
+/* 25-12*/[
+ "<div>test</div>\n<pre>foo<div>bar</div>baz\nfred</pre>",
+ "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>", // expected
+],
+
+// Some tests without <div>. These exercise the MoveBlock "right in left"
+/* 26-00*/[
+ "test<pre>foobar\nbaz</pre>",
+ "testfoobar<pre>baz</pre>", // expected
+],
+/* 27-01*/[
+ "test<pre><b>foobar\nbaz</b></pre>",
+ "test<b>foobar</b><pre><b>baz</b></pre>", // expected
+],
+/* 28-02*/[
+ "test<pre><b>foo</b>bar\nbaz</pre>",
+ "test<b>foo</b>bar<pre>baz</pre>", // expected
+],
+/* 29-03*/[
+ "test<pre><b>foo</b>\nbar</pre>",
+ "test<b>foo</b><pre>bar</pre>", // expected
+],
+/* 30-04*/[
+ "test<pre><b>foo\n</b>bar\nbaz</pre>",
+ "test<b>foo</b><pre>bar\nbaz</pre>", // expected
+],
+/* 31-05*/[
+ "test<pre>foobar<br>baz</pre>",
+ "testfoobar<pre>baz</pre>", // expected
+],
+/* 32-06*/[
+ "test<pre><b>foobar<br>baz</b></pre>",
+ "test<b>foobar</b><pre><b>baz</b></pre>", // expected
+],
+/* 33-07*/[
+ "test<pre><div>foobar</div>baz</pre>",
+ "testfoobar<pre>baz</pre>", // expected
+],
+/* 34-08*/[
+ "test<pre>foobar<div>baz</div></pre>",
+ "testfoobar<pre><div>baz</div></pre>", // expected
+],
+/* 35-09*/[
+ "test<pre><div>foobar</div>baz\nfred</pre>",
+ "testfoobar<pre>baz\nfred</pre>", // expected
+],
+/* 36-10*/[
+ "test<pre>foobar<div>baz</div>\nfred</pre>",
+ "testfoobar<pre><div>baz</div>\nfred</pre>", // expected
+],
+/* 37-11*/[
+ "test<pre><div>foo\nbar</div>baz\nfred</pre>",
+ "testfoo<pre><div>bar</div>baz\nfred</pre>", // expected
+],
+/* 38-12*/[
+ "test<pre>foo<div>bar</div>baz\nfred</pre>",
+ "testfoo<pre><div>bar</div>baz\nfred</pre>", // expected
+],
+
+// Some tests with <span class="pre">
+// All these exercise MoveBlock "left in right". The "right" is the surrounding "contenteditable" div.
+// FYI: Those tests except the cases <span> having <div> were ported to
+// join-different-white-space-style-left-paragraph-and-right-line.html
+/* 39-00*/[
+ "<div>test</div><span class=\"pre\">foobar\nbaz</span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\">foobar</span></div><span class=\"pre\">baz</span>", // expected
+],
+/* 40-01*/[
+ "<div>test</div><span class=\"pre\"><b>foobar\nbaz</b></span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foobar</b></span></div><span class=\"pre\"><b>baz</b></span>", // expected
+],
+/* 41-02*/[
+ "<div>test</div><span class=\"pre\"><b>foo</b>bar\nbaz</span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foo</b>bar</span></div><span class=\"pre\">baz</span>", // expected
+],
+/* 42-03*/[
+ "<div>test</div><span class=\"pre\"><b>foo</b>\nbar</span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foo</b></span></div><span class=\"pre\">bar</span>", // expected
+],
+/* 43-04*/[
+ "<div>test</div><span class=\"pre\"><b>foo\n</b>bar\nbaz</span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foo</b></span></div><span class=\"pre\">bar\nbaz</span>", // expected
+],
+/* 44-05*/[
+ "<div>test</div><span class=\"pre\">foobar<br>baz</span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\">foobar</span></div><span class=\"pre\">baz</span>", // expected
+],
+/* 45-06*/[
+ "<div>test</div><span class=\"pre\"><b>foobar<br>baz</b></span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foobar</b></span></div><span class=\"pre\"><b>baz</b></span>", // expected
+],
+/* 46-07*/[
+ "<div>test</div><span class=\"pre\"><div>foobar</div>baz</span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\"><div>foobar</div></span></div><span class=\"pre\">baz</span>", // expected
+],
+/* 47-08*/[
+ "<div>test</div><span class=\"pre\">foobar<div>baz</div></span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\">foobar</span></div><span class=\"pre\"><div>baz</div></span>", // expected
+],
+/* 48-09*/[
+ "<div>test</div><span class=\"pre\"><div>foobar</div>baz\nfred</span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\"><div>foobar</div></span></div><span class=\"pre\">baz\nfred</span>", // expected
+],
+/* 49-10*/[
+ "<div>test</div><span class=\"pre\">foobar<div>baz</div>\nfred</span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\">foobar</span></div><span class=\"pre\"><div>baz</div>\nfred</span>", // expected
+],
+/* 50-11*/[
+ "<div>test</div><span class=\"pre\"><div>foo\nbar</div>baz\nfred</span>",
+ "<div>test<span style=\"white-space: pre;\">foo</span></div><span class=\"pre\"><div>bar</div>baz\nfred</span>", // expected
+],
+/* 51-12*/[
+ "<div>test</div><span class=\"pre\">foo<div>bar</div>baz\nfred</span>",
+ "<div>test<span class=\"pre\" style=\"white-space: pre;\">foo</span></div><span class=\"pre\"><div>bar</div>baz\nfred</span>", // expected
+],
+
+// Some tests with <div class="pre">.
+// FYI: Those tests were ported to join-different-white-space-style-paragraphs.html
+/* 52-00*/[
+ "<div>test</div><div class=\"pre\">foobar\nbaz</div>",
+ "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\">baz</div>", // expected
+],
+/* 53-01*/[
+ "<div>test</div><div class=\"pre\"><b>foobar\nbaz</b></div>",
+ "<div>test<b style=\"white-space: pre;\">foobar</b></div><div class=\"pre\"><b>baz</b></div>", // expected
+],
+/* 54-02*/[
+ "<div>test</div><div class=\"pre\"><b>foo</b>bar\nbaz</div>",
+ "<div>test<b style=\"white-space: pre;\">foo</b><span style=\"white-space: pre;\">bar</span></div><div class=\"pre\">baz</div>", // expected
+],
+/* 55-03*/[
+ "<div>test</div><div class=\"pre\"><b>foo</b>\nbar</div>",
+ "<div>test<b style=\"white-space: pre;\">foo</b></div><div class=\"pre\">bar</div>", // expected
+],
+/* 56-04*/[
+ "<div>test</div><div class=\"pre\"><b>foo\n</b>bar\nbaz</div>",
+ "<div>test<b style=\"white-space: pre;\">foo</b></div><div class=\"pre\">bar\nbaz</div>", // expected
+],
+/* 57-05*/[
+ "<div>test</div><div class=\"pre\">foobar<br>baz</div>",
+ "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\">baz</div>", // expected
+],
+/* 58-06*/[
+ "<div>test</div><div class=\"pre\"><b>foobar<br>baz</b></div>",
+ "<div>test<b style=\"white-space: pre;\">foobar</b></div><div class=\"pre\"><b>baz</b></div>", // expected
+],
+/* 59-07*/[
+ "<div>test</div><div class=\"pre\"><div>foobar</div>baz</div>",
+ "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\">baz</div>", // expected
+],
+/* 60-08*/[
+ "<div>test</div><div class=\"pre\">foobar<div>baz</div></div>",
+ "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\"><div>baz</div></div>", // expected
+],
+/* 61-09*/[
+ "<div>test</div><div class=\"pre\"><div>foobar</div>baz\nfred</div>",
+ "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\">baz\nfred</div>", // expected
+],
+/* 62-10*/[
+ "<div>test</div><div class=\"pre\">foobar<div>baz</div>\nfred</div>",
+ "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\"><div>baz</div>\nfred</div>", // expected
+],
+/* 63-11*/[
+ "<div>test</div><div class=\"pre\"><div>foo\nbar</div>baz\nfred</div>",
+ "<div>test<span style=\"white-space: pre;\">foo</span></div><div class=\"pre\"><div>bar</div>baz\nfred</div>", // expected
+],
+/* 64-12*/[
+ "<div>test</div><div class=\"pre\">foo<div>bar</div>baz\nfred</div>",
+ "<div>test<span style=\"white-space: pre;\">foo</span></div><div class=\"pre\"><div>bar</div>baz\nfred</div>", // expected
+],
+
+// Some tests with lists. These exercise the MoveBlock "left in right".
+/* 65*/[
+ "<ul><pre><li>test</li>foobar\nbaz</pre></ul>",
+ "<ul><pre><li>testfoobar</li>baz</pre></ul>", // expected
+],
+/* 66*/[
+ "<ul><pre><li>test</li><b>foobar\nbaz</b></pre></ul>",
+ "<ul><pre><li>test<b>foobar</b></li><b>baz</b></pre></ul>", // expected
+],
+/* 67*/[
+ "<ul><pre><li>test</li><b>foo</b>bar\nbaz</pre></ul>",
+ "<ul><pre><li>test<b>foo</b>bar</li>baz</pre></ul>", // expected
+],
+/* 68*/[
+ "<ul><pre><li>test</li><b>foo</b>\nbar</pre></ul>",
+ "<ul><pre><li>test<b>foo</b></li>bar</pre></ul>", // expected
+],
+/* 69*/[
+ "<ul><pre><li>test</li><b>foo\n</b>bar\nbaz</pre></ul>",
+ "<ul><pre><li>test<b>foo</b></li>bar\nbaz</pre></ul>", // expected
+],
+
+// Last not least, some simple edge case tests.
+// FYI: Those tests were ported to join-pre-and-other-block.html
+/* 70*/[
+ "<div>test</div><pre>foobar\n</pre>baz",
+ "<div>testfoobar</div>baz", // expected
+],
+/* 71*/[
+ "<div>test</div><pre>\nfoo\nbar</pre>",
+ "<div>testfoo</div><pre>bar</pre>", // expected
+],
+/* 72*/[
+ "<div>test</div><pre>\n\nfoo\nbar</pre>",
+ "<div>test</div><pre>foo\nbar</pre>", // expected
+],
+];
+
+ /** Test for Bug 772796 **/
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ var sel = window.getSelection();
+ var theEdit = document.getElementById("editable");
+ var testName;
+ var theDiv;
+
+ for (let i = 0; i < tests.length; i++) {
+ testName = "test" + i.toString();
+
+ /* Set up the selection. */
+ theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>";
+ theDiv = document.getElementById(testName);
+ theDiv.focus();
+ sel.collapse(theDiv, 0);
+ synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */
+
+ function normalizeStyeAttributeValues(aElement) {
+ for (const element of Array.from(
+ aElement.querySelectorAll("[style]")
+ )) {
+ element.setAttribute(
+ "style",
+ element
+ .getAttribute("style")
+ // Random spacing differences
+ .replace(/$/, ";")
+ .replace(/;;$/, ";")
+ // Gecko likes "transparent"
+ .replace(/transparent/g, "rgba(0, 0, 0, 0)")
+ // WebKit likes to look overly precise
+ .replace(/, 0.496094\)/g, ", 0.5)")
+ // Gecko converts anything with full alpha to "transparent" which
+ // then becomes "rgba(0, 0, 0, 0)", so we have to make other
+ // browsers match
+ .replace(/rgba\([0-9]+, [0-9]+, [0-9]+, 0\)/g, "rgba(0, 0, 0, 0)")
+ );
+ }
+ }
+
+ let todoCount = 0;
+ /** First round: Forward delete. **/
+ synthesizeKey("KEY_Delete");
+ normalizeStyeAttributeValues(theDiv);
+ if (tests[i].length == 2 || theDiv.innerHTML == tests[i][1]) {
+ is(theDiv.innerHTML, tests[i][1], "delete(collapsed): inner HTML for " + testName);
+ } else {
+ todoCount++;
+ todo_is(theDiv.innerHTML, tests[i][1], "delete(should be): inner HTML for " + testName);
+ is(theDiv.innerHTML, tests[i][2], "delete(currently is): inner HTML for " + testName);
+ }
+
+ /* Set up the selection. */
+ theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>";
+ theDiv = document.getElementById(testName);
+ theDiv.focus();
+ sel.collapse(theDiv, 0);
+ synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */
+
+ /** Second round: Backspace. **/
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("KEY_Backspace");
+ normalizeStyeAttributeValues(theDiv);
+ if (tests[i].length == 2 || theDiv.innerHTML == tests[i][1]) {
+ is(theDiv.innerHTML, tests[i][1], "backspace: inner HTML for " + testName);
+ } else {
+ todoCount++;
+ todo_is(theDiv.innerHTML, tests[i][1], "backspace(should be): inner HTML for " + testName);
+ is(theDiv.innerHTML, tests[i][2], "backspace(currently is): inner HTML for " + testName);
+ }
+
+ /* Set up the selection. */
+ theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>";
+ theDiv = document.getElementById(testName);
+ theDiv.focus();
+ sel.collapse(theDiv, 0);
+ synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */
+
+ /** Third round: Delete with non-collapsed selection. **/
+ if (i == 72) {
+ if (tests[i].length == 3) {
+ ok(!!todoCount, `All tests unexpectedly passed in ${testName}`);
+ }
+ // This test doesn't work, since we can't select only one newline using the right arrow key.
+ continue;
+ }
+ synthesizeKey("KEY_ArrowLeft");
+ /* Strangely enough we need to hit "right arrow" three times to select two characters. */
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true});
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true});
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true});
+ synthesizeKey("KEY_Delete");
+ normalizeStyeAttributeValues(theDiv);
+
+ /* We always expect to the delete the "tf" in "testfoo". */
+ function makeNonCollapsedExpectation(aExpected) {
+ return aExpected
+ .replace("testfoo",
+ "tesoo")
+ .replace("test<b>foo",
+ "tes<b>oo")
+ .replace("test<b style=\"white-space: pre;\">foo",
+ "tes<b style=\"white-space: pre;\">oo")
+ .replace("test<span style=\"white-space: pre;\">foo",
+ "tes<span style=\"white-space: pre;\">oo")
+ .replace("test<span style=\"white-space: pre;\"><b>foo",
+ "tes<span style=\"white-space: pre;\"><b>oo")
+ .replace("test<span style=\"white-space: pre;\"><div>foo",
+ "tes<span style=\"white-space: pre;\"><div>oo")
+ .replace("test<span class=\"pre\" style=\"white-space: pre;\">foo",
+ "tes<span class=\"pre\" style=\"white-space: pre;\">oo")
+ .replace("test<span class=\"pre\" style=\"white-space: pre;\"><b>foo",
+ "tes<span class=\"pre\" style=\"white-space: pre;\"><b>oo")
+ .replace("test<span class=\"pre\" style=\"white-space: pre;\"><div>foo",
+ "tes<span class=\"pre\" style=\"white-space: pre;\"><div>oo");
+ }
+ const expected = makeNonCollapsedExpectation(tests[i][1]);
+ if (tests[i].length == 2 || theDiv.innerHTML == expected) {
+ is(theDiv.innerHTML, expected, "delete(non-collapsed): inner HTML for " + testName);
+ } else {
+ todoCount++;
+ todo_is(theDiv.innerHTML, expected, "delete(non-collapsed, should be): inner HTML for " + testName);
+ is(
+ theDiv.innerHTML,
+ makeNonCollapsedExpectation(tests[i][2]),
+ "delete(non-collapsed, currently is): inner HTML for " + testName
+ );
+ }
+ if (tests[i].length == 3) {
+ ok(!!todoCount, `All tests unexpectedly passed in ${testName}`);
+ }
+ }
+
+ SimpleTest.finish();
+ });
+
+ </script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug773262.html b/editor/libeditor/tests/test_bug773262.html
new file mode 100644
index 0000000000..b0dc827559
--- /dev/null
+++ b/editor/libeditor/tests/test_bug773262.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=773262
+-->
+<title>Test for Bug 773262</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=773262">Mozilla Bug 773262</a></p>
+<iframe></iframe>
+<script>
+function runTest(doc, desc) {
+ is(doc.queryCommandEnabled("undo"), false,
+ desc + ": Undo shouldn't be enabled yet");
+ is(doc.queryCommandEnabled("redo"), false,
+ desc + ": Redo shouldn't be enabled yet");
+ is(doc.body.innerHTML, "<p>Hello</p>", desc + ": Wrong initial innerHTML");
+
+ doc.getSelection().selectAllChildren(doc.body.firstChild);
+ doc.execCommand("bold");
+ is(doc.queryCommandEnabled("undo"), true,
+ desc + ": Undo should be enabled after bold");
+ is(doc.queryCommandEnabled("redo"), false,
+ desc + ": Redo still shouldn't be enabled");
+ is(doc.body.innerHTML, "<p><b>Hello</b></p>",
+ desc + ": Wrong innerHTML after bold");
+
+ doc.execCommand("undo");
+ is(doc.queryCommandEnabled("undo"), false,
+ desc + ": Undo should be disabled again");
+ is(doc.queryCommandEnabled("redo"), true,
+ desc + ": Redo should be enabled now");
+ is(doc.body.innerHTML, "<p>Hello</p>",
+ desc + ": Wrong innerHTML after undo");
+
+ doc.execCommand("redo");
+ is(doc.queryCommandEnabled("undo"), true,
+ desc + ": Undo should be enabled after redo");
+ is(doc.queryCommandEnabled("redo"), false,
+ desc + ": Redo should be disabled again");
+ is(doc.body.innerHTML, "<p><b>Hello</b></p>",
+ desc + ": Wrong innerHTML after redo");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var doc = document.querySelector("iframe").contentDocument;
+
+ // First turn on designMode and run the test like that, as a sanity check.
+ doc.body.innerHTML = "<p>Hello</p>";
+ doc.designMode = "on";
+ runTest(doc, "1");
+
+ // Now to test the actual bug: repeat all the above, but with designMode
+ // toggled. This should clear the undo history, so everything should be
+ // exactly as before.
+ doc.designMode = "off";
+ doc.body.innerHTML = "<p>Hello</p>";
+ doc.designMode = "on";
+ runTest(doc, "2");
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug780035.html b/editor/libeditor/tests/test_bug780035.html
new file mode 100644
index 0000000000..9992314888
--- /dev/null
+++ b/editor/libeditor/tests/test_bug780035.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=780035
+-->
+<title>Test for Bug 780035</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=780035">Mozilla Bug 780035</a>
+<div contenteditable style="font-size: 13.3333px"></div>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ document.querySelector("div").focus();
+ document.execCommand("stylewithcss", false, true);
+ document.execCommand("defaultParagraphSeparator", false, "div");
+ sendKey("RETURN");
+ sendChar("x");
+ is(document.querySelector("div").innerHTML,
+ "<div><br></div><div>x<br></div>", "No <font> tag should be generated");
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_bug780908.xhtml b/editor/libeditor/tests/test_bug780908.xhtml
new file mode 100644
index 0000000000..590316ef46
--- /dev/null
+++ b/editor/libeditor/tests/test_bug780908.xhtml
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=780908
+
+adapted from test_bug607584.xhtml by Kent James <kent@caspia.com>
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 780908" onload="runTest();">
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=780908"
+ target="_blank">Mozilla Bug 780908</a>
+ <p/>
+ <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="editor"
+ type="content"
+ primary="true"
+ editortype="html"
+ style="width: 400px; height: 100px; border: thin solid black"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function EditorContentListener(aEditor)
+ {
+ this.init(aEditor);
+ }
+
+ EditorContentListener.prototype = {
+ init(aEditor)
+ {
+ this.mEditor = aEditor;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
+ "nsISupportsWeakReference"]),
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)
+ {
+ var editor = this.mEditor.getEditor(this.mEditor.contentWindow);
+ if (editor) {
+ this.mEditor.focus();
+ editor instanceof Ci.nsIHTMLEditor;
+ editor.returnInParagraphCreatesNewParagraph = true;
+ let source = "<html><body><table><head></table></body></html>";
+ editor.rebuildDocumentFromSource(source);
+ ok(true, "Don't crash when head appears after body");
+ source = "<html></head><head><body></body></html>";
+ editor.rebuildDocumentFromSource(source);
+ ok(true, "Don't crash when /head appears before head");
+ SimpleTest.finish();
+ progress.removeProgressListener(this);
+ }
+ }
+
+ },
+
+
+ onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress)
+ {
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ },
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ },
+
+ onSecurityChange(aWebProgress, aRequest, aState)
+ {
+ },
+
+ onContentBlockingEvent(aWebProgress, aRequest, aEvent)
+ {
+ },
+
+ mEditor: null
+ };
+
+ var progress, progressListener;
+
+ function runTest() {
+ var newEditorElement = document.getElementById("editor");
+ newEditorElement.makeEditable("html", true);
+ var docShell = newEditorElement.docShell;
+ progress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+ progressListener = new EditorContentListener(newEditorElement);
+ progress.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ newEditorElement.setAttribute("src", "data:text/html,");
+ }
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_bug787432.html b/editor/libeditor/tests/test_bug787432.html
new file mode 100644
index 0000000000..c73bb3c7ea
--- /dev/null
+++ b/editor/libeditor/tests/test_bug787432.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=787432
+-->
+<title>Test for Bug 787432</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=787432">Mozilla Bug 787432</a>
+<div id="test" contenteditable><span class="insert">%</span><br></div>
+<script>
+var div = document.getElementById("test");
+getSelection().collapse(div.firstChild, 0);
+getSelection().extend(div.firstChild, 1);
+document.execCommand("inserttext", false, "x");
+is(div.innerHTML, '<span class="insert">x</span><br>',
+ "Empty <span> needs to not be removed");
+</script>
diff --git a/editor/libeditor/tests/test_bug790475.html b/editor/libeditor/tests/test_bug790475.html
new file mode 100644
index 0000000000..d30b14b312
--- /dev/null
+++ b/editor/libeditor/tests/test_bug790475.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=790475
+-->
+<head>
+ <title>Test for Bug 790475</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=790475">Mozilla Bug 790475</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 790475
+ *
+ * Tests that inline spell checking works properly through adjacent text nodes.
+ */
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords;
+
+function getEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window);
+}
+
+function getSpellCheckSelection() {
+ var editor = getEditor();
+ var selcon = editor.selectionController;
+ return selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+}
+
+function runTest() {
+ gMisspeltWords = [];
+ var edit = document.getElementById("edit");
+ edit.focus();
+
+ SimpleTest.executeSoon(function() {
+ gMisspeltWords = [];
+ is(isSpellingCheckOk(), true, "Should not find any misspellings yet.");
+
+ var newTextNode = document.createTextNode("ing string");
+ edit.appendChild(newTextNode);
+ var editor = getEditor();
+ var sel = editor.selection;
+ sel.collapse(newTextNode, newTextNode.textContent.length);
+ sendString("!");
+
+ edit.blur();
+
+ SimpleTest.executeSoon(function() {
+ is(isSpellingCheckOk(), true, "Should not have found any misspellings. ");
+ SimpleTest.finish();
+ });
+ });
+}
+
+function isSpellingCheckOk() {
+ var sel = getSpellCheckSelection();
+ var numWords = sel.rangeCount;
+
+ is(numWords, gMisspeltWords.length, "Correct number of misspellings and words.");
+
+ if (numWords != gMisspeltWords.length)
+ return false;
+
+ for (var i = 0; i < numWords; i++) {
+ var word = sel.getRangeAt(i);
+ is(word, gMisspeltWords[i], "Misspelling is what we think it is.");
+ if (word != gMisspeltWords[i])
+ return false;
+ }
+ return true;
+}
+
+</script>
+</pre>
+
+<div id="edit" contenteditable="true">This is a test</div>
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-2.html b/editor/libeditor/tests/test_bug795418-2.html
new file mode 100644
index 0000000000..66330b4f79
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-2.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #2 for Bug 772796</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<!-- load content of type application/xhtml+xml using an *.sjs file -->
+<iframe src="./file_bug795418-2.sjs"></iframe>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ const div = document.getElementById("copySource");
+ getSelection().setBaseAndExtent(div.firstChild, 0, div.firstChild, "Copy this".length);
+ info(`Selected test: "${getSelection().getRangeAt(0).toString()}"`);
+
+ function checkResult() {
+ var iframe = document.querySelector("iframe");
+ var theEdit = iframe.contentDocument.firstChild;
+ theEdit.offsetHeight;
+ is(theEdit.innerHTML,
+ "<blockquote xmlns=\"http://www.w3.org/1999/xhtml\" type=\"cite\">Copy this</blockquote><span xmlns=\"http://www.w3.org/1999/xhtml\">AB</span>",
+ "unexpected HTML for test");
+ SimpleTest.finish();
+ }
+
+ function pasteQuote() {
+ var iframe = document.querySelector("iframe");
+ var iframeWindow = iframe.contentWindow;
+ var theEdit = iframe.contentDocument.firstChild;
+ theEdit.offsetHeight;
+ iframeWindow.focus();
+ SimpleTest.waitForFocus(function() {
+ var iframeSel = iframeWindow.getSelection();
+ iframeSel.removeAllRanges();
+ let span = iframe.contentDocument.querySelector("span");
+ iframeSel.collapse(span, 1);
+
+ SpecialPowers.doCommand(iframeWindow, "cmd_pasteQuote");
+ setTimeout(checkResult, 0);
+ }, iframeWindow);
+ }
+
+ SimpleTest.waitForClipboard(
+ aData => {
+ if (aData.includes(`${getSelection().getRangeAt(0)?.toString()}`)) {
+ return true;
+ }
+ info(`Text in the clipboard: "${aData}"`);
+ return false;
+ },
+ function setup() {
+ synthesizeKey("c", {accelKey: true});
+ },
+ function onSuccess() {
+ SimpleTest.executeSoon(pasteQuote);
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ // TODO: bug 1686012
+ SpecialPowers.isHeadless ? "text/plain" : "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-3.html b/editor/libeditor/tests/test_bug795418-3.html
new file mode 100644
index 0000000000..aae076069f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-3.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #3 for Bug 772796</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<iframe srcdoc="<html><body><span>AB</span>"></iframe>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ function checkResult() {
+ var iframe = document.querySelector("iframe");
+ var theEdit = iframe.contentDocument.body;
+ theEdit.offsetHeight;
+ is(theEdit.innerHTML,
+ "<span>AB<blockquote type=\"cite\">Copy this</blockquote></span>",
+ "unexpected HTML for test");
+ SimpleTest.finish();
+ }
+
+ function pasteQuote() {
+ var iframe = document.querySelector("iframe");
+ var iframeWindow = iframe.contentWindow;
+ var theEdit = iframe.contentDocument.body;
+ iframe.contentDocument.designMode = "on";
+ iframe.contentDocument.body.offsetHeight;
+ iframeWindow.focus();
+ SimpleTest.waitForFocus(function() {
+ var iframeSel = iframeWindow.getSelection();
+ iframeSel.removeAllRanges();
+ iframeSel.collapse(theEdit.firstChild, 1);
+
+ SpecialPowers.doCommand(iframeWindow, "cmd_pasteQuote");
+ setTimeout(checkResult, 0);
+ }, iframeWindow);
+ }
+
+ SimpleTest.waitForClipboard(
+ aData => {
+ // XXX Oddly, specifying `r.toString()` causes timeout in headless mode.
+ info(`copied text: "${aData}"`);
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ setTimeout(pasteQuote, 0);
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-4.html b/editor/libeditor/tests/test_bug795418-4.html
new file mode 100644
index 0000000000..1c71ee5894
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-4.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #4 for Bug 795418</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<div id="editable" contenteditable style="display:grid">AB</div>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ SimpleTest.waitForClipboard(
+ aData => {
+ // XXX Oddly, specifying `r.toString()` causes timeout in headless mode.
+ info(`copied text: "${aData}"`);
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ var theEdit = document.getElementById("editable");
+ sel.collapse(theEdit.firstChild, 2);
+
+ SpecialPowers.doCommand(window, "cmd_paste");
+ is(theEdit.innerHTML,
+ "ABCopy this",
+ "unexpected HTML for test");
+
+ SimpleTest.finish();
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-5.html b/editor/libeditor/tests/test_bug795418-5.html
new file mode 100644
index 0000000000..562a225f3e
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-5.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #5 for Bug 795418</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<div id="editable" contenteditable style="display:ruby">AB</div>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ SimpleTest.waitForClipboard(
+ aData => {
+ // XXX Oddly, specifying `r.toString()` causes timeout in headless mode.
+ info(`copied text: "${aData}"`);
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ var theEdit = document.getElementById("editable");
+ sel.collapse(theEdit.firstChild, 2);
+
+ SpecialPowers.doCommand(window, "cmd_paste");
+ is(theEdit.innerHTML,
+ "ABCopy this",
+ "unexpected HTML for test");
+ SimpleTest.finish();
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418-6.html b/editor/libeditor/tests/test_bug795418-6.html
new file mode 100644
index 0000000000..ea6a62612b
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418-6.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test #5 for Bug 795418</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<div id="editable" contenteditable style="display:table">AB</div>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ SimpleTest.waitForClipboard(
+ aData => {
+ // XXX Oddly, specifying `r.toString()` causes timeout in headless mode.
+ info(`copied text: "${aData}"`);
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ var theEdit = document.getElementById("editable");
+ sel.collapse(theEdit.firstChild, 2);
+
+ SpecialPowers.doCommand(window, "cmd_pasteQuote");
+ is(theEdit.innerHTML,
+ "AB<blockquote type=\"cite\">Copy this</blockquote>",
+ "unexpected HTML for test");
+ SimpleTest.finish();
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795418.html b/editor/libeditor/tests/test_bug795418.html
new file mode 100644
index 0000000000..8e02d0b49f
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795418.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795418
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 795418</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div id="copySource">Copy this</div>
+<div id="editable" contenteditable><span>AB</span></div>
+
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 795418 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("copySource");
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ // Select the text from the text node in div.
+ var r = document.createRange();
+ r.setStart(div.firstChild, 0);
+ r.setEnd(div.firstChild, 9);
+ sel.addRange(r);
+
+ SimpleTest.waitForClipboard(
+ aData => {
+ // XXX Oddly, specifying `r.toString()` causes timeout in headless mode.
+ info(`copied text: "${aData}"`);
+ return true;
+ },
+ function setup() {
+ synthesizeKey("C", {accelKey: true});
+ },
+ function onSuccess() {
+ var theEdit = document.getElementById("editable");
+ sel.collapse(theEdit.firstChild, 1);
+
+ SpecialPowers.doCommand(window, "cmd_pasteQuote");
+ is(theEdit.innerHTML,
+ "<span>AB<blockquote type=\"cite\">Copy this</blockquote></span>",
+ "unexpected HTML for test");
+
+ SimpleTest.finish();
+ },
+ function onFailure() {
+ SimpleTest.finish();
+ },
+ "text/html"
+ );
+});
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug795785.html b/editor/libeditor/tests/test_bug795785.html
new file mode 100644
index 0000000000..3e56207b63
--- /dev/null
+++ b/editor/libeditor/tests/test_bug795785.html
@@ -0,0 +1,139 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=795785
+-->
+<head>
+ <title>Test for Bug 795785</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795785">Mozilla Bug 795785</a>
+<div id="display">
+ <textarea style="overflow: hidden; height: 3em; width: 5em; word-wrap: normal;"></textarea>
+ <div contenteditable style="overflow: hidden; height: 3em; width: 5em;"></div>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script>
+"use strict";
+
+function waitForScroll() {
+ return waitToClearOutAnyPotentialScrolls(window);
+}
+
+async function synthesizeKeyInEachEventLoop(aKey, aEvent, aCount) {
+ for (let i = 0; i < aCount; i++) {
+ synthesizeKey(aKey, aEvent);
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ }
+}
+
+async function sendStringOneByOne(aString) {
+ for (const ch of aString) {
+ sendString(ch);
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ }
+}
+
+async function doKeyEventTest(aSelector) {
+ const element = document.querySelector(aSelector);
+ if (element.value !== undefined) {
+ element.value = "";
+ } else {
+ element.innerHTML = "";
+ }
+ element.focus();
+ element.scrollTop = 0;
+ await waitForScroll();
+ is(element.scrollTop, 0, `${aSelector}.scrollTop should be 0`);
+ await synthesizeKeyInEachEventLoop("KEY_Enter", {shiftKey: true}, 6);
+ await waitForScroll();
+ isnot(element.scrollTop, 0, `${aSelector} should be scrolled by inserting line breaks`);
+ const oldScrollTop = element.scrollTop;
+ await synthesizeKeyInEachEventLoop("KEY_ArrowUp", {}, 5);
+ await waitForScroll();
+ isnot(element.scrollTop, oldScrollTop, `${aSelector} should be scrolled by up key events`);
+ await synthesizeKeyInEachEventLoop("KEY_ArrowDown", {}, 5);
+ await waitForScroll();
+ is(element.scrollTop, oldScrollTop, `${aSelector} should be scrolled by down key events`);
+ const longWord = "aaaaaaaaaaaaaaaaaaaa";
+ await sendStringOneByOne(longWord);
+ await waitForScroll();
+ isnot(element.scrollLeft, 0, `${aSelector} should be scrolled by typing long word`);
+ const oldScrollLeft = element.scrollLeft;
+ await synthesizeKeyInEachEventLoop("KEY_ArrowLeft", {}, longWord.length);
+ await waitForScroll();
+ isnot(element.scrollLeft, oldScrollLeft, `${aSelector} should be scrolled by left key events`);
+ await synthesizeKeyInEachEventLoop("KEY_ArrowRight", {}, longWord.length);
+ await waitForScroll();
+ is(element.scrollLeft, oldScrollLeft, `${aSelector} should be scrolled by right key events`);
+}
+
+async function doCompositionTest(aSelector) {
+ const element = document.querySelector(aSelector);
+ if (element.value !== undefined) {
+ element.value = "";
+ } else {
+ element.innerHTML = "";
+ }
+ element.focus();
+ element.scrollTop = 0;
+ await waitForScroll();
+ is(element.scrollTop, 0, `${aSelector}.scrollTop should be 0`);
+ const longCompositionString =
+ "Web \u958b\u767a\u8005\u306e\u7686\u3055\u3093\u306f\u3001" +
+ "Firefox \u306b\u5b9f\u88c5\u3055\u308c\u3066\u3044\u308b HTML5" +
+ " \u3084 CSS \u306e\u65b0\u6a5f\u80fd\u3092\u6d3b\u7528\u3059" +
+ "\u308b\u3053\u3068\u3067\u3001\u9b45\u529b\u3042\u308b Web " +
+ "\u30b5\u30a4\u30c8\u3084\u9769\u65b0\u7684\u306a Web \u30a2" +
+ "\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u3088\u308a" +
+ "\u77ed\u6642\u9593\u3067\u7c21\u5358\u306b\u4f5c\u6210\u3067" +
+ "\u304d\u307e\u3059\u3002";
+ synthesizeCompositionChange(
+ {
+ composition: {
+ string: longCompositionString,
+ clauses: [
+ {
+ length: longCompositionString.length,
+ attr: COMPOSITION_ATTR_RAW_CLAUSE,
+ },
+ ],
+ },
+ caret: {
+ start: longCompositionString.length,
+ length: 0,
+ },
+ }
+ );
+ await waitForScroll();
+ isnot(element.scrollTop, 0, `${aSelector} should be scrolled by composition`);
+ synthesizeComposition({ type: "compositioncommit", data: "" });
+ await waitForScroll();
+ is(
+ element.scrollTop,
+ 0,
+ `${aSelector} should be scrolled back to the top by canceling composition`
+ );
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ await doKeyEventTest("textarea");
+ await doKeyEventTest("div[contenteditable]");
+ await doCompositionTest("textarea");
+ await doCompositionTest("div[contenteditable]");
+ SimpleTest.finish();
+});
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_bug796839.html b/editor/libeditor/tests/test_bug796839.html
new file mode 100644
index 0000000000..8fe1c36d56
--- /dev/null
+++ b/editor/libeditor/tests/test_bug796839.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=796839
+-->
+<title>Test for Bug 796839</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=796839">Mozilla Bug 796839</a>
+<div id="test" contenteditable><br></div>
+<script>
+var div = document.getElementById("test");
+var text = document.createTextNode("");
+div.insertBefore(text, div.firstChild);
+getSelection().collapse(text, 0);
+document.execCommand("inserthtml", false, "x");
+is(div.textContent, "x", "Empty textnodes should be editable");
+</script>
diff --git a/editor/libeditor/tests/test_bug830600.html b/editor/libeditor/tests/test_bug830600.html
new file mode 100644
index 0000000000..166ac187a2
--- /dev/null
+++ b/editor/libeditor/tests/test_bug830600.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=830600
+-->
+<head>
+ <title>Test for Bug 830600</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=830600">Mozilla Bug 830600</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <input type="text" id="t1" />
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 830600 **/
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ const Ci = SpecialPowers.Ci;
+ function test(str, expected, callback) {
+ var t = document.getElementById("t1");
+ t.focus();
+ t.value = "";
+ var editor = SpecialPowers.wrap(t).editor;
+ editor.newlineHandling = Ci.nsIEditor.eNewlinesStripSurroundingWhitespace;
+ SimpleTest.waitForClipboard(str,
+ function() {
+ SpecialPowers.Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(str);
+ },
+ function() {
+ synthesizeKey("V", {accelKey: true});
+ is(t.value, expected, "New line handling works correctly");
+ t.value = "";
+ callback();
+ },
+ function() {
+ ok(false, "Failed to copy the string");
+ SimpleTest.finish();
+ }
+ );
+ }
+
+ function runNextTest() {
+ if (tests.length) {
+ var currentTest = tests.shift();
+ test(currentTest[0], currentTest[1], runNextTest);
+ } else {
+ SimpleTest.finish();
+ }
+ }
+
+ var tests = [
+ ["abc", "abc"],
+ ["\n", ""],
+ [" \n", ""],
+ ["\n ", ""],
+ [" \n ", ""],
+ [" a", " a"],
+ ["a ", "a "],
+ [" a ", " a "],
+ [" \nabc", "abc"],
+ ["\n abc", "abc"],
+ [" \n abc", "abc"],
+ [" \nabc ", "abc "],
+ ["\n abc ", "abc "],
+ [" \n abc ", "abc "],
+ ["abc\n ", "abc"],
+ ["abc \n", "abc"],
+ ["abc \n ", "abc"],
+ [" abc\n ", " abc"],
+ [" abc \n", " abc"],
+ [" abc \n ", " abc"],
+ [" abc \n def \n ", " abcdef"],
+ ["\n abc \n def \n ", "abcdef"],
+ [" \n abc \n def ", "abcdef "],
+ [" abc\n\ndef ", " abcdef "],
+ [" abc \n\n def ", " abcdef "],
+ ];
+
+ runNextTest();
+ });
+
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug832025.html b/editor/libeditor/tests/test_bug832025.html
new file mode 100644
index 0000000000..181adda423
--- /dev/null
+++ b/editor/libeditor/tests/test_bug832025.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=832025
+-->
+<head>
+ <title>Test for Bug 832025</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=832025">Mozilla Bug 832025</a>
+<div id="test" contenteditable="true">header1</div>
+<script type="application/javascript">
+
+/**
+ * Test for Bug 832025
+ *
+ */
+
+document.execCommand("stylewithcss", false, "true");
+document.execCommand("defaultParagraphSeparator", false, "div");
+var test = document.getElementById("test");
+test.focus();
+
+// place caret at end of editable area
+var sel = getSelection();
+sel.collapse(test, test.childNodes.length);
+
+// make it a H1
+document.execCommand("formatBlock", false, "H1");
+// simulate a CR key
+sendKey("return");
+// insert some text
+document.execCommand("insertText", false, "abc");
+
+is(test.innerHTML, "<h1>header1</h1><div>abc<br></div>",
+ "A paragraph automatically created after a CR at the end of an H1 should not be bold");
+
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug850043.html b/editor/libeditor/tests/test_bug850043.html
new file mode 100644
index 0000000000..681a5cb5fe
--- /dev/null
+++ b/editor/libeditor/tests/test_bug850043.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=850043
+-->
+<head>
+ <title>Test for Bug 850043</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=850043">Mozilla Bug 850043</a>
+<div id="display">
+<textarea id="textarea">b&#x9080;&#xe010f;&#x8fba;&#xe0101;</textarea>
+<div contenteditable id="edit">b&#x9080;&#xe010f;&#x8fba;&#xe0101;</div>
+</div>
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+</pre>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let fm = SpecialPowers.Services.focus;
+
+ let element = document.getElementById("textarea");
+ element.setSelectionRange(element.value.length, element.value.length);
+ element.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), element, "failed to move focus");
+
+ synthesizeKey("KEY_End");
+ sendString("a");
+ is(element.value, "b\u{9080}\u{e010f}\u{8fba}\u{e0101}a", "a isn't last character");
+
+ synthesizeKey("KEY_Backspace", {repeat: 3});
+ is(element.value, "b", "cannot remove all IVS characters");
+
+ element = document.getElementById("edit");
+ element.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), element, "failed to move focus");
+
+ let sel = window.getSelection();
+ sel.collapse(element.childNodes[0], element.textContent.length);
+
+ sendString("a");
+ is(element.textContent, "b\u{9080}\u{e010f}\u{8fba}\u{e0101}a", "a isn't last character");
+
+ synthesizeKey("KEY_Backspace", {repeat: 3});
+ is(element.textContent, "b", "cannot remove all IVS characters");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug857487.html b/editor/libeditor/tests/test_bug857487.html
new file mode 100644
index 0000000000..e9cec16ece
--- /dev/null
+++ b/editor/libeditor/tests/test_bug857487.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=857487
+-->
+<head>
+ <title>Test for Bug 857487</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=857487">Mozilla Bug 857487</a>
+<div id="edit" contenteditable="true">
+ <table id="table" border="1" width="100%">
+ <tbody>
+ <tr>
+ <td>a</td>
+ <td>b</td>
+ <td>c</td>
+ </tr>
+ <tr>
+ <td>d</td>
+ <td id="cell">e</td>
+ <td>f</td>
+ </tr>
+ <tr>
+ <td>g</td>
+ <td>h</td>
+ <td>i</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<script type="application/javascript">
+
+/**
+ * Test for Bug 857487
+ *
+ * Tests that removing a table row through nsIHTMLEditor works
+ */
+
+function getEditor() {
+ const Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+var cell = document.getElementById("cell");
+cell.focus();
+
+// place caret at end of center cell
+var sel = getSelection();
+sel.collapse(cell, cell.childNodes.length);
+
+var editor = getEditor();
+editor.deleteTableRow(1);
+
+var table = document.getElementById("table");
+
+is(table.innerHTML == "\n <tbody>\n <tr>\n <td>a</td>\n <td>b</td>\n <td>c</td>\n </tr>\n \n <tr>\n <td>g</td>\n <td>h</td>\n <td>i</td>\n </tr>\n </tbody>\n ",
+ true, "editor.deleteTableRow(1) should delete the row containing the selection");
+
+</script>
+
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug858918.html b/editor/libeditor/tests/test_bug858918.html
new file mode 100644
index 0000000000..46f841bbce
--- /dev/null
+++ b/editor/libeditor/tests/test_bug858918.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=858918
+-->
+<title>Test for Bug 858918</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858918">Mozilla Bug 858918</a>
+<span contenteditable style="display:block;min-height:1em"></span>
+<script>
+var span = document.querySelector("span");
+getSelection().collapse(span, 0);
+document.execCommand("inserthtml", false, "<div>doesn't go in span</div>");
+is(span.innerHTML, "<div>doesn't go in span</div>");
+</script>
diff --git a/editor/libeditor/tests/test_bug915962.html b/editor/libeditor/tests/test_bug915962.html
new file mode 100644
index 0000000000..42cf4e3d67
--- /dev/null
+++ b/editor/libeditor/tests/test_bug915962.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=915962
+-->
+<head>
+ <title>Test for Bug 915962</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=915962">Mozilla Bug 915962</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 915962 **/
+
+var smoothScrollPref = "general.smoothScroll";
+SimpleTest.waitForExplicitFinish();
+var win = window.open("file_bug915962.html", "_blank",
+ "width=600,height=600,scrollbars=yes");
+
+
+function waitForScrollEvent(aWindow) {
+ return new Promise(resolve => {
+ aWindow.addEventListener("scroll", () => { SimpleTest.executeSoon(resolve); }, {once: true, capture: true});
+ });
+}
+
+function waitAndCheckNoScrollEvent(aWindow) {
+ let gotScroll = false;
+ function recordScroll() {
+ gotScroll = true;
+ }
+ aWindow.addEventListener("scroll", recordScroll, {capture: true});
+ return waitToClearOutAnyPotentialScrolls(aWindow).then(function() {
+ aWindow.removeEventListener("scroll", recordScroll, {capture: true});
+ is(gotScroll, false, "check that we didn't get a scroll");
+ });
+}
+
+SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set": [[smoothScrollPref, false]]}, startTest);
+}, win);
+async function startTest() {
+ // Make sure that pressing Space when a tabindex=-1 element is focused
+ // will scroll the page.
+ var button = win.document.querySelector("button");
+ var sc = win.document.querySelector("div");
+ sc.focus();
+ await waitToClearOutAnyPotentialScrolls(win);
+ is(win.scrollY, 0, "Sanity check");
+ let waitForScrolling = waitForScrollEvent(win);
+ synthesizeKey(" ", {}, win);
+
+ await waitForScrolling;
+
+ isnot(win.scrollY, 0, "Page is scrolled down");
+ var oldY = win.scrollY;
+ waitForScrolling = waitForScrollEvent(win);
+ synthesizeKey(" ", {shiftKey: true}, win);
+
+ await waitForScrolling;
+
+ ok(win.scrollY < oldY, "Page is scrolled up");
+
+ // Make sure that pressing Space when a tabindex=-1 element is focused
+ // will not scroll the page, and will activate the element.
+ button.focus();
+ await waitToClearOutAnyPotentialScrolls(win);
+ var clicked = false;
+ button.onclick = () => clicked = true;
+ oldY = win.scrollY;
+ let waitForNoScroll = waitAndCheckNoScrollEvent(win);
+ synthesizeKey(" ", {}, win);
+
+ await waitForNoScroll;
+
+ ok(win.scrollY <= oldY, "Page is not scrolled down");
+ ok(clicked, "The button should be clicked");
+ synthesizeKey("VK_TAB", {}, win);
+
+ await waitToClearOutAnyPotentialScrolls(win);
+
+ oldY = win.scrollY;
+ waitForScrolling = waitForScrollEvent(win);
+ synthesizeKey(" ", {}, win);
+
+ await waitForScrolling;
+
+ ok(win.scrollY >= oldY, "Page is scrolled down");
+
+ win.close();
+
+ win = window.open("file_bug915962.html", "_blank",
+ "width=600,height=600,scrollbars=yes");
+
+ SimpleTest.waitForFocus(async function() {
+ is(win.scrollY, 0, "Sanity check");
+ waitForScrolling = waitForScrollEvent(win);
+ synthesizeKey(" ", {}, win);
+
+ await waitForScrolling;
+
+ isnot(win.scrollY, 0, "Page is scrolled down without crashing");
+
+ win.close();
+
+ SimpleTest.finish();
+ }, win);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug966155.html b/editor/libeditor/tests/test_bug966155.html
new file mode 100644
index 0000000000..193d383b96
--- /dev/null
+++ b/editor/libeditor/tests/test_bug966155.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=966155
+-->
+<head>
+ <title>Test for Bug 966155</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=966155">Mozilla Bug 966155</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var testWindow = window.open("file_bug966155.html", "", "test-966155");
+testWindow.addEventListener("load", function() {
+ runTest(testWindow);
+}, {once: true});
+
+function runTest(win) {
+ SimpleTest.waitForFocus(function() {
+ var doc = win.document;
+ var iframe = doc.querySelector("iframe");
+ var iframeDoc = iframe.contentDocument;
+ var input = doc.querySelector("input");
+ iframe.focus();
+ iframeDoc.body.focus();
+ // Type some text
+ "test".split("").forEach(function(letter) {
+ synthesizeKey(letter, {}, win);
+ });
+ is(iframeDoc.body.textContent.trim(), "test", "entered the text");
+ // focus the input box
+ input.focus();
+ // press tab
+ synthesizeKey("VK_TAB", {}, win);
+ // Now press Ctrl+Backspace
+ synthesizeKey("VK_BACK_SPACE", {ctrlKey: true}, win);
+ is(iframeDoc.body.textContent.trim(), "", "deleted the text");
+ win.close();
+ SimpleTest.finish();
+ }, win);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug966552.html b/editor/libeditor/tests/test_bug966552.html
new file mode 100644
index 0000000000..f27ceb92d7
--- /dev/null
+++ b/editor/libeditor/tests/test_bug966552.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=966552
+-->
+<head>
+ <title>Test for Bug 966552</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=966552">Mozilla Bug 966552</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var testWindow = window.open("file_bug966552.html", "", "test-966552");
+testWindow.addEventListener("load", function() {
+ runTest(testWindow);
+}, {once: true});
+
+function runTest(win) {
+ SimpleTest.waitForFocus(function() {
+ var doc = win.document;
+ var sel = win.getSelection();
+ doc.body.focus();
+ sel.collapse(doc.body.firstChild, 2);
+ synthesizeKey("VK_BACK_SPACE", {ctrlKey: true}, win);
+ is(doc.body.textContent.trim(), "st");
+ win.close();
+ SimpleTest.finish();
+ }, win);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug974309.html b/editor/libeditor/tests/test_bug974309.html
new file mode 100644
index 0000000000..67f720f2e4
--- /dev/null
+++ b/editor/libeditor/tests/test_bug974309.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=974309
+-->
+<head>
+ <title>Test for Bug 974309</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=974309">Mozilla Bug 974309</a>
+<div id="edit_not_table_parent" contenteditable="true"></div>
+<div>
+ <table id="table" border="1" width="100%">
+ <tbody>
+ <tr>
+ <td>a</td>
+ <td>b</td>
+ <td>c</td>
+ </tr>
+ <tr>
+ <td>d</td>
+ <td id="cell">e</td>
+ <td>f</td>
+ </tr>
+ <tr>
+ <td>g</td>
+ <td>h</td>
+ <td>i</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<script type="application/javascript">
+
+/**
+ * Test for Bug 974309
+ *
+ * Tests that editing a table row fails when the table or row is _not_ a child of a contenteditable node.
+ * See bug 857487 for tests that cover when the table or row _is_ a child of a contenteditable node.
+ */
+
+function getEditor() {
+ const Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+var cell = document.getElementById("cell");
+cell.focus();
+
+// place caret at end of center cell
+var sel = getSelection();
+sel.collapse(cell, cell.childNodes.length);
+
+var table = document.getElementById("table");
+
+var tableHTML = table.innerHTML;
+
+var editor = getEditor();
+editor.deleteTableRow(1);
+
+is(table.innerHTML == tableHTML, true, "editor should not modify non-editable table" );
+
+isnot(table.innerHTML == "\n <tbody>\n <tr>\n <td>a</td>\n <td>b</td>\n <td>c</td>\n </tr>\n \n <tr>\n <td>g</td>\n <td>h</td>\n <td>i</td>\n </tr>\n </tbody>\n ",
+ true, "editor.deleteTableRow(1) should not delete a non-editable row containing the selection");
+
+</script>
+
+
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_bug998188.html b/editor/libeditor/tests/test_bug998188.html
new file mode 100644
index 0000000000..8038d2ef64
--- /dev/null
+++ b/editor/libeditor/tests/test_bug998188.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=565392
+-->
+<head>
+ <title>Test for Bug 998188</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998188">Mozilla Bug 998188</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<div id="editor" contenteditable>abc</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 998188 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function runTests() {
+ var editor = document.getElementById("editor");
+ editor.focus();
+
+ var textNode1 = document.createTextNode("def");
+ var textNode2 = document.createTextNode("ghi");
+
+ editor.appendChild(textNode1);
+ editor.appendChild(textNode2);
+
+ window.getSelection().collapse(textNode2, 3);
+
+ for (var i = 0; i < 9; i++) {
+ var caretRect = synthesizeQueryCaretRect(i);
+ ok(caretRect.succeeded, "QueryCaretRect should succeeded (" + i + ")");
+ }
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTests);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_can_undo_after_setting_value.xhtml b/editor/libeditor/tests/test_can_undo_after_setting_value.xhtml
new file mode 100644
index 0000000000..03204f8500
--- /dev/null
+++ b/editor/libeditor/tests/test_can_undo_after_setting_value.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1386222
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 1386222" onload="runTest();">
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <input xmlns="http://www.w3.org/1999/xhtml" id="t"/>
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+
+function runTest() {
+ var t = document.getElementById("t");
+ is(
+ t.editor.canUndo,
+ false,
+ "Editor shouldn't have undo transaction at start"
+ );
+ t.value = "foo";
+ is(t.value, "foo", "value setter worked");
+ is(
+ t.editor.canUndo,
+ true,
+ "Editor should have undo transaction after setting value"
+ );
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+]]>
+</script>
+</window>
diff --git a/editor/libeditor/tests/test_cannot_undo_after_reinitializing_editor.html b/editor/libeditor/tests/test_cannot_undo_after_reinitializing_editor.html
new file mode 100644
index 0000000000..b4f8239e20
--- /dev/null
+++ b/editor/libeditor/tests/test_cannot_undo_after_reinitializing_editor.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=602130
+-->
+<head>
+ <title>Test for Bug 602130</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602130">Mozilla Bug 602130</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 602130 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var i = document.createElement("input");
+ document.body.appendChild(i);
+ i.select();
+ i.focus();
+ is(
+ SpecialPowers.wrap(i).editor.canUndo,
+ false,
+ "Editor shouldn't have undo transactions at start"
+ );
+ i.style.display = "none";
+ document.offsetWidth;
+ i.style.display = "";
+ document.offsetWidth;
+ i.select();
+ i.focus();
+ is(
+ SpecialPowers.wrap(i).editor.canUndo,
+ false,
+ "Editor shouldn't have undo transaction after re-initializing the editor"
+ );
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_caret_move_in_vertical_content.html b/editor/libeditor/tests/test_caret_move_in_vertical_content.html
new file mode 100644
index 0000000000..3e0854a752
--- /dev/null
+++ b/editor/libeditor/tests/test_caret_move_in_vertical_content.html
@@ -0,0 +1,209 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1103374
+-->
+<head>
+ <title>Testing caret move in vertical content</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=289384">Mozilla Bug 1103374</a>
+<input value="ABCD"><textarea>ABCD
+EFGH</textarea>
+<div contenteditable></div>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ await (async function testContentEditable() {
+ const editor = document.querySelector("div[contenteditable]");
+ const selection = getSelection();
+ function nodeStr(node) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ return `#text ("${node.data}")`;
+ }
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ return `<${node.tagName.toLowerCase()}>`;
+ }
+ return node;
+ }
+ function rangeStr(container, offset) {
+ return `{ container: ${nodeStr(container)}, offset: ${offset} }`;
+ }
+ function selectionStr() {
+ if (selection.rangeCount === 0) {
+ return "<no range>";
+ }
+ if (selection.isCollapsed) {
+ return rangeStr(selection.focusNode, selection.focusOffset);
+ }
+ return `${
+ rangeStr(selection.anchorNode, selection.anchorOffset)
+ } - ${
+ rangeStr(selection.focusNode, selection.focusOffset)
+ }`;
+ }
+ await (async function testVerticalRL() {
+ editor.innerHTML = '<p style="font-family: monospace; writing-mode: vertical-rl">ABCD<br>EFGH</p>';
+ editor.focus();
+ const firstLine = editor.querySelector("p").firstChild;
+ const secondLine = firstLine.nextSibling.nextSibling;
+ await new Promise(resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget
+ );
+ selection.collapse(firstLine, firstLine.length / 2);
+ synthesizeKey("KEY_ArrowUp");
+ is(selectionStr(), rangeStr(firstLine, firstLine.length / 2 - 1),
+ "contenteditable: ArrowUp should move caret to previous character in vertical-rl content");
+ selection.collapse(firstLine, firstLine.length / 2 - 1);
+ synthesizeKey("KEY_ArrowDown");
+ is(selectionStr(), rangeStr(firstLine, firstLine.length / 2),
+ "contenteditable: ArrowDown should move caret to next character in vertical-rl content");
+ selection.collapse(firstLine, firstLine.length / 2);
+ synthesizeKey("KEY_ArrowLeft");
+ is(selectionStr(), rangeStr(secondLine, secondLine.length / 2),
+ "contenteditable: ArrowLeft should move caret to next line in vertical-rl content");
+ selection.collapse(secondLine, secondLine.length / 2);
+ synthesizeKey("KEY_ArrowRight");
+ is(selectionStr(), rangeStr(firstLine, firstLine.length / 2),
+ "contenteditable: ArrowRight should move caret to previous line in vertical-rl content");
+ selection.removeAllRanges();
+ editor.blur();
+ })();
+ await (async function testVerticalLR() {
+ editor.innerHTML = '<p style="font-family: monospace; writing-mode: vertical-lr">ABCD<br>EFGH</p>';
+ editor.focus();
+ const firstLine = editor.querySelector("p").firstChild;
+ const secondLine = firstLine.nextSibling.nextSibling;
+ await new Promise(resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget
+ );
+ selection.collapse(firstLine, firstLine.length / 2);
+ synthesizeKey("KEY_ArrowUp");
+ is(selectionStr(), rangeStr(firstLine, firstLine.length / 2 - 1),
+ "contenteditable: ArrowUp should move caret to previous character in vertical-lr content");
+ selection.collapse(firstLine, firstLine.length / 2 - 1);
+ synthesizeKey("KEY_ArrowDown");
+ is(selectionStr(), rangeStr(firstLine, firstLine.length / 2),
+ "contenteditable: ArrowDown should move caret to next character in vertical-lr content");
+ selection.collapse(firstLine, firstLine.length / 2);
+ synthesizeKey("KEY_ArrowRight");
+ is(selectionStr(), rangeStr(secondLine, secondLine.length / 2),
+ "contenteditable: ArrowRight should move caret to next line in vertical-lr content");
+ selection.collapse(secondLine, secondLine.length / 2);
+ synthesizeKey("KEY_ArrowLeft");
+ is(selectionStr(), rangeStr(firstLine, firstLine.length / 2),
+ "contenteditable: ArrowLeft should move caret to previous line in vertical-lr content");
+ selection.removeAllRanges();
+ editor.blur();
+ })();
+ })();
+ await (async function testInputTypeText() {
+ const editor = document.querySelector("input");
+ await (async function testVerticalRL() {
+ editor.style.writingMode = "vertical-rl";
+ editor.focus();
+ await new Promise(resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget
+ );
+ editor.selectionStart = editor.selectionEnd = editor.value.length / 2;
+ synthesizeKey("KEY_ArrowRight");
+ is(editor.selectionStart, 0,
+ "<input>: ArrowRight should move caret to beginning of the input element if vertical-rl");
+ editor.selectionStart = editor.selectionEnd = editor.value.length / 2;
+ synthesizeKey("KEY_ArrowLeft");
+ is(editor.selectionStart, editor.value.length,
+ "<input>: ArrowLeft should move caret to end of the input element if vertical-rl");
+ editor.selectionStart = editor.selectionEnd = editor.value.length / 2;
+ synthesizeKey("KEY_ArrowDown");
+ is(editor.selectionStart, editor.value.length / 2 + 1,
+ "<input>: ArrowDown should move caret to next character if vertical-rl");
+ editor.selectionStart = editor.selectionEnd = editor.value.length / 2;
+ synthesizeKey("KEY_ArrowUp");
+ is(editor.selectionStart, editor.value.length / 2 - 1,
+ "<input>: ArrowUp should move caret to previous character if vertical-rl");
+ editor.blur();
+ })();
+ await (async function testVerticalLR() {
+ editor.style.writingMode = "vertical-lr";
+ editor.focus();
+ await new Promise(resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget
+ );
+ editor.selectionStart = editor.selectionEnd = editor.value.length / 2;
+ synthesizeKey("KEY_ArrowLeft");
+ is(editor.selectionStart, 0,
+ "<input>: ArrowLeft should move caret to beginning of the input element if vertical-lr");
+ editor.selectionStart = editor.selectionEnd = editor.value.length / 2;
+ synthesizeKey("KEY_ArrowRight");
+ is(editor.selectionStart, editor.value.length,
+ "<input>: ArrowRight should move caret to end of the input element if vertical-lr");
+ editor.selectionStart = editor.selectionEnd = editor.value.length / 2;
+ synthesizeKey("KEY_ArrowDown");
+ is(editor.selectionStart, editor.value.length / 2 + 1,
+ "<input>: ArrowDown should move caret to next character if vertical-lr");
+ editor.selectionStart = editor.selectionEnd = editor.value.length / 2;
+ synthesizeKey("KEY_ArrowUp");
+ is(editor.selectionStart, editor.value.length / 2 - 1,
+ "<input>: ArrowUp should move caret to previous character if vertical-lr");
+ editor.blur();
+ })();
+ })();
+ await (async function testTextArea() {
+ const editor = document.querySelector("textarea");
+ await (async function testVerticalRL() {
+ editor.style.writingMode = "vertical-rl";
+ editor.focus();
+ await new Promise(resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget
+ );
+ editor.selectionStart = editor.selectionEnd = "ABCD\nEF".length;
+ synthesizeKey("KEY_ArrowRight");
+ isfuzzy(editor.selectionStart, "AB".length, 1,
+ "<textarea>: ArrowRight should move caret to previous line of textarea element if vertical-rl");
+ editor.selectionStart = editor.selectionEnd = "AB".length;
+ synthesizeKey("KEY_ArrowLeft");
+ isfuzzy(editor.selectionStart, "ABCD\nEF".length, 1,
+ "<textarea>: ArrowLeft should move caret to next line of textarea element if vertical-rl");
+ editor.selectionStart = editor.selectionEnd = "AB".length;
+ synthesizeKey("KEY_ArrowDown");
+ is(editor.selectionStart, "ABC".length,
+ "<textarea>: ArrowDown should move caret to next character if vertical-rl");
+ editor.selectionStart = editor.selectionEnd = "AB".length;
+ synthesizeKey("KEY_ArrowUp");
+ is(editor.selectionStart, "A".length,
+ "<textarea>: ArrowUp should move caret to previous character if vertical-rl");
+ editor.blur();
+ })();
+ await (async function testVerticalLR() {
+ editor.style.writingMode = "vertical-lr";
+ editor.focus();
+ await new Promise(resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget
+ );
+ editor.selectionStart = editor.selectionEnd = "ABCD\nEF".length;
+ synthesizeKey("KEY_ArrowLeft");
+ isfuzzy(editor.selectionStart, "AB".length, 1,
+ "<textarea>: ArrowLeft should move caret to previous line of textarea element if vertical-rl");
+ editor.selectionStart = editor.selectionEnd = "AB".length;
+ synthesizeKey("KEY_ArrowRight");
+ isfuzzy(editor.selectionStart, "ABCD\nEF".length, 1,
+ "<textarea>: ArrowRight should move caret to next line of textarea element if vertical-rl");
+ editor.selectionStart = editor.selectionEnd = "AB".length;
+ synthesizeKey("KEY_ArrowDown");
+ is(editor.selectionStart, "ABC".length,
+ "<textarea>: ArrowDown should move caret to next character if vertical-rl");
+ editor.selectionStart = editor.selectionEnd = "AB".length;
+ synthesizeKey("KEY_ArrowUp");
+ is(editor.selectionStart, "A".length,
+ "<textarea>: ArrowUp should move caret to previous character if vertical-rl");
+ editor.blur();
+ })();
+ })();
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_cmd_absPos.html b/editor/libeditor/tests/test_cmd_absPos.html
new file mode 100644
index 0000000000..c31c2c53a4
--- /dev/null
+++ b/editor/libeditor/tests/test_cmd_absPos.html
@@ -0,0 +1,296 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing cmd_absPos command</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body contenteditable></body>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ (function testTogglingPositionInBodyChildText() {
+ const description = "testTogglingPositionInBodyChildText";
+ const textNode = document.createTextNode("abc");
+ document.body.appendChild(textNode);
+ getSelection().collapse(textNode, 1);
+
+ SpecialPowers.doCommand(window, "cmd_absPos");
+ const div = textNode.parentNode;
+ is(
+ div.tagName.toLowerCase(),
+ "div",
+ `${
+ description
+ }: a <div> element should be created and the text node should be wrapped in it`
+ );
+ is(
+ div.style.position,
+ "absolute",
+ `${description}: position of the <div> should be set to "absolute"`
+ );
+ isnot(
+ div.style.top,
+ "",
+ `${description}: top of the <div> should be set`
+ );
+ isnot(
+ div.style.left,
+ "",
+ `${description}: left of the <div> should be set`
+ );
+ ok(
+ getSelection().isCollapsed,
+ `${
+ description
+ }: selection should be collapsed (making it absolutely positioned)`
+ );
+ // Collapsing selection to start of the absolutely positioned <div> seems
+ // odd. Why don't we keep selection in the new <div>?
+ is(
+ getSelection().focusNode,
+ div,
+ `${
+ description
+ }: selection should be collapsed in the absolutely positioned <div>`
+ );
+ is(
+ getSelection().focusOffset,
+ 0,
+ `${
+ description
+ }: selection should be collapsed at start of the absolutely positioned <div>`
+ );
+
+ SpecialPowers.doCommand(window, "cmd_absPos");
+ ok(
+ !div.isConnected,
+ `${description}: the <div> should be removed from the <body>`
+ );
+ is(
+ div.style.position,
+ "",
+ `${description}: position of the <div> should be unset`
+ );
+ is(
+ div.style.top,
+ "",
+ `${description}: top of the <div> should be unset`
+ );
+ is(
+ div.style.left,
+ "",
+ `${description}: left of the <div> should be unset`
+ );
+ ok(
+ getSelection().isCollapsed,
+ `${
+ description
+ }: selection should be collapsed (making it in normal flow)`
+ );
+ // If we change the above behavior, we can keep selection collapsed in the
+ // text node here.
+ is(
+ getSelection().focusNode,
+ document.body,
+ `${description}: selection should be collapsed in the <body>`
+ );
+ todo_is(
+ getSelection().focusNode.childNodes.item(
+ getSelection().focusOffset
+ ),
+ textNode,
+ `${description}: selection should be collapsed after the text node`
+ );
+ textNode.remove();
+ })();
+
+ (function testTogglingPositionOfDivContainingCaret() {
+ const description = "testTogglingPositionOfDivContainingCaret";
+ const div = document.createElement("div");
+ div.innerHTML = "abc";
+ document.body.appendChild(div);
+ const textNode = div.firstChild;
+ getSelection().collapse(textNode, 1);
+ SpecialPowers.doCommand(window, "cmd_absPos");
+ is(
+ div.style.position,
+ "absolute",
+ `${description}: position of the <div> should be set to "absolute"`
+ );
+ isnot(
+ div.style.top,
+ "",
+ `${description}: top of the <div> should be set`
+ );
+ isnot(
+ div.style.left,
+ "",
+ `${description}: left of the <div> should be set`
+ );
+ ok(
+ getSelection().isCollapsed,
+ `${
+ description
+ }: selection should be collapsed (making it absolutely positioned)`
+ );
+ is(
+ getSelection().focusNode,
+ textNode,
+ `${
+ description
+ }: selection should be collapsed in the absolutely positioned <div>`
+ );
+ is(
+ getSelection().focusOffset,
+ 1,
+ `${
+ description
+ }: selection should be collapsed at same offset in the absolutely positioned <div>`
+ );
+
+ SpecialPowers.doCommand(window, "cmd_absPos");
+ ok(
+ !div.isConnected,
+ `${description}: the <div> should be removed from the <body>`
+ );
+ is(
+ div.style.position,
+ "",
+ `${description}: position of the <div> should be unset`
+ );
+ is(
+ div.style.top,
+ "",
+ `${description}: top of the <div> should be unset`
+ );
+ is(
+ div.style.left,
+ "",
+ `${description}: left of the <div> should be unset`
+ );
+ ok(
+ getSelection().isCollapsed,
+ `${
+ description
+ }: selection should be collapsed (making it in normal flow)`
+ );
+ is(
+ getSelection().focusNode,
+ textNode,
+ `${description}: selection should be collapsed in the <div>`
+ );
+ is(
+ getSelection().focusOffset,
+ 1,
+ `${description}: selection should be collapsed at same offset in the <div>`
+ );
+ div.remove();
+ textNode.remove();
+ })();
+
+ (function testTogglingPositionOfDivContainingSelectionRange() {
+ const description = "testTogglingPositionOfDivContainingSelectionRange";
+ const div = document.createElement("div");
+ div.innerHTML = "abc";
+ document.body.appendChild(div);
+ const textNode = div.firstChild;
+ getSelection().setBaseAndExtent(textNode, 1, textNode, 2);
+ SpecialPowers.doCommand(window, "cmd_absPos");
+ is(
+ div.style.position,
+ "absolute",
+ `${description}: position of the <div> should be set to "absolute"`
+ );
+ isnot(
+ div.style.top,
+ "",
+ `${description}: top of the <div> should be set`
+ );
+ isnot(
+ div.style.left,
+ "",
+ `${description}: left of the <div> should be set`
+ );
+ is(
+ getSelection().anchorNode,
+ textNode,
+ `${
+ description
+ }: selection should start from the text node in the absolutely positioned <div>`
+ );
+ is(
+ getSelection().anchorOffset,
+ 1,
+ `${
+ description
+ }: selection should start from "b" in the text node in the absolutely positioned <div>`
+ );
+ is(
+ getSelection().focusNode,
+ textNode,
+ `${
+ description
+ }: selection should end in the text node in the absolutely positioned <div>`
+ );
+ is(
+ getSelection().focusOffset,
+ 2,
+ `${
+ description
+ }: selection should end after "b" absolutely positioned <div>`
+ );
+
+ getSelection().setBaseAndExtent(textNode, 1, textNode, 2);
+ SpecialPowers.doCommand(window, "cmd_absPos");
+ ok(
+ !div.isConnected,
+ `${description}: the <div> should be removed from the <body>`
+ );
+ is(
+ div.style.position,
+ "",
+ `${description}: position of the <div> should be unset`
+ );
+ is(
+ div.style.top,
+ "",
+ `${description}: top of the <div> should be unset`
+ );
+ is(
+ div.style.left,
+ "",
+ `${description}: left of the <div> should be unset`
+ );
+ is(
+ getSelection().anchorNode,
+ textNode,
+ `${description}: selection should start from the text node in the <div>`
+ );
+ is(
+ getSelection().anchorOffset,
+ 1,
+ `${
+ description
+ }: selection should start from "b" in the text node in the <div>`
+ );
+ is(
+ getSelection().focusNode,
+ textNode,
+ `${description}: selection should end in the text node in the <div>`
+ );
+ is(
+ getSelection().focusOffset,
+ 2,
+ `${description}: selection should end after "b" <div>`
+ );
+ div.remove();
+ textNode.remove();
+ })();
+
+ document.body.removeAttribute("contenteditable");
+ SimpleTest.finish();
+});
+</script>
+</html>
diff --git a/editor/libeditor/tests/test_cmd_backgroundColor.html b/editor/libeditor/tests/test_cmd_backgroundColor.html
new file mode 100644
index 0000000000..7e089ad9f0
--- /dev/null
+++ b/editor/libeditor/tests/test_cmd_backgroundColor.html
@@ -0,0 +1,223 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing "cmd_backgroundColor" behavior</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div contenteditable></div>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ document.execCommand("styleWithCSS", false, "true");
+ const commandManager =
+ SpecialPowers.wrap(window).
+ docShell.
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsICommandManager);
+ const editor = document.querySelector("div[contenteditable]");
+ editor.style.backgroundColor = "#ff0000";
+
+ editor.innerHTML = "<p>abc</p>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.querySelector("p").firstChild, 1);
+ let params = SpecialPowers.Cu.createCommandParams();
+ commandManager.getCommandState("cmd_backgroundColor", window, params);
+ is(
+ params.getCStringValue("state_attribute"),
+ "rgb(255, 0, 0)",
+ "cmd_backgroundColor should return the editing host's background color (should ignore the transparent paragraph)"
+ );
+
+ editor.innerHTML = "<p style=\"background-color: #00ff00\">abc</p>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.querySelector("p").firstChild, 1);
+ params = SpecialPowers.Cu.createCommandParams();
+ commandManager.getCommandState("cmd_backgroundColor", window, params);
+ is(
+ params.getCStringValue("state_attribute"),
+ "rgb(0, 255, 0)",
+ "cmd_backgroundColor should return the paragraph's background color"
+ );
+
+ editor.innerHTML = "<span style=\"background-color: #00ff00\">abc</span>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.querySelector("span").firstChild, 1);
+ params = SpecialPowers.Cu.createCommandParams();
+ commandManager.getCommandState("cmd_backgroundColor", window, params);
+ is(
+ params.getCStringValue("state_attribute"),
+ "rgb(255, 0, 0)",
+ "cmd_backgroundColor shouldn't return the span's background color due to inline element"
+ );
+
+ editor.innerHTML = "<p style=\"background-color: #00ff00\" contenteditable=\"false\">a<span style=\"background-color: #0000ff\" contenteditable=\"true\">b</span>c</p>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.querySelector("span[contenteditable=true]").firstChild, 1);
+ params = SpecialPowers.Cu.createCommandParams();
+ commandManager.getCommandState("cmd_backgroundColor", window, params);
+ is(
+ params.getCStringValue("state_attribute"),
+ "rgb(0, 255, 0)",
+ "cmd_backgroundColor should return non-editable block element's background color"
+ );
+
+ editor.innerHTML = "<p contenteditable=\"false\">a<span style=\"background-color: #0000ff\" contenteditable=\"true\">b</span>c</p>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.querySelector("span[contenteditable=true]").firstChild, 1);
+ params = SpecialPowers.Cu.createCommandParams();
+ commandManager.getCommandState("cmd_backgroundColor", window, params);
+ is(
+ params.getCStringValue("state_attribute"),
+ "rgb(255, 0, 0)",
+ "cmd_backgroundColor should return the parent editing host's background color (should ignore the transparent non-editable paragraph)"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "<div><p><span>abc</span></p></div>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.querySelector("span").firstChild, 1);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p></div></div>",
+ "cmd_backgroundColor should set background of the closest block element from the caret in a text node"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "abc";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.firstChild, 1);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(0, 255, 0);\">abc</div>",
+ "cmd_backgroundColor should set background of the editing host which is direct block parent from the caret in a text node"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "<div contenteditable=\"false\"><span contenteditable=\"true\">abc</span></div>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.firstChild, 1);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div contenteditable=\"false\"><span contenteditable=\"true\">abc</span></div></div>",
+ "cmd_backgroundColor should not set background color of inline editing host nor its non-editable parent block"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "<div><p><span>ab<br>c</span></p></div>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().setBaseAndExtent(editor.querySelector("span"), 1, editor.querySelector("span"), 2);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>ab<br>c</span></p></div></div>",
+ "cmd_backgroundColor should set background of the closest block element when selection a leaf element"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "<div><p><span>abc</span></p></div>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p></div></div>",
+ "cmd_backgroundColor should set background of the selected block element"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "<div><p><span>abc</span></p><p><span>def</span></p><p><span>ghi</span></p></div>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + p + p > span").firstChild, 1);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" +
+ "<p style=\"background-color: rgb(0, 255, 0);\"><span>def</span></p>" +
+ "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div></div>",
+ "cmd_backgroundColor should set background of the paragraph elements in the selection range"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "<div><p><span>abc</span></p><p><span contenteditable=\"false\">def</span></p><p><span>ghi</span></p></div>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + p + p > span").firstChild, 1);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" +
+ "<p style=\"background-color: rgb(0, 255, 0);\"><span contenteditable=\"false\">def</span></p>" +
+ "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div></div>",
+ "cmd_backgroundColor should set background of the paragraph elements in the selection range even if a paragraph has only non-editable content"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "<div><p><span>abc</span></p><p contenteditable=\"false\"><span>def</span></p><p><span>ghi</span></p></div>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + p + p > span").firstChild, 1);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" +
+ "<p contenteditable=\"false\"><span>def</span></p>" +
+ "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div></div>",
+ "cmd_backgroundColor should set background of the paragraph elements in the selection range except the non-editable paragraph"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "<div><p><span>abc</span></p><span>def</span><p><span>ghi</span></p></div>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + span + p > span").firstChild, 1);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div style=\"background-color: rgb(0, 255, 0);\">" +
+ "<p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" +
+ "<span>def</span>" +
+ "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div></div>",
+ "cmd_backgroundColor should set background of the paragraph elements in the selection range and the container <div> which has inline child"
+ );
+
+ editor.style.backgroundColor = "#ff0000";
+ editor.innerHTML = "<p><span>abc</span></p><span>def</span><p><span>ghi</span></p>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + span + p > span").firstChild, 1);
+ SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00");
+ is(
+ editor.outerHTML,
+ "<div contenteditable=\"\" style=\"background-color: rgb(0, 255, 0);\">" +
+ "<p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" +
+ "<span>def</span>" +
+ "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div>",
+ "cmd_backgroundColor should set background of the paragraph elements in the selection range and the editing host which has inline child"
+ );
+
+ // TODO: Add testcase for HTML styling mode.
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_cmd_fontFace_with_empty_string.html b/editor/libeditor/tests/test_cmd_fontFace_with_empty_string.html
new file mode 100644
index 0000000000..c101aaf6c6
--- /dev/null
+++ b/editor/libeditor/tests/test_cmd_fontFace_with_empty_string.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html>
+<head>
+<title>Tests typing in empty paragraph after running `cmd_fontFace` with empty string</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ document.designMode = "on";
+ document.body.innerHTML = "<p><font face=\"monospace\">abc</font><br></p>";
+ getSelection().collapse(document.querySelector("font").firstChild, "abc".length);
+ document.execCommand("insertParagraph");
+ // Calling document.execCommand("fontName", false, "") is NOOP, but HTMLEditor
+ // accepts empty string param for cmd_fontFace.
+ SpecialPowers.doCommand(window, "cmd_fontFace", "");
+ document.execCommand("insertText", false, "d");
+ is(
+ document.querySelector("p+p").innerHTML,
+ "d<br>",
+ "The typed text should not be wrapped in <font face=\"monospace\">"
+ );
+ document.designMode = "off";
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body></body>
+</html>
diff --git a/editor/libeditor/tests/test_cmd_fontFace_with_tt.html b/editor/libeditor/tests/test_cmd_fontFace_with_tt.html
new file mode 100644
index 0000000000..495076e9b5
--- /dev/null
+++ b/editor/libeditor/tests/test_cmd_fontFace_with_tt.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<html>
+<head>
+ <title>Testing font face "tt"</title>
+ <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="display">
+</div>
+
+<div id="content" contenteditable>abc</div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody">
+"use strict";
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.querySelector("div[contenteditable]");
+ editor.focus();
+ let selection = document.getSelection();
+
+ // "tt" is a magic value of "cmd_fontFace", it should work as "cmd_tt".
+ editor.innerHTML = "abc";
+ selection.setBaseAndExtent(editor.firstChild, 1, editor.firstChild, 2);
+ SpecialPowers.doCommand(window, "cmd_fontFace", "tt");
+ is(editor.innerHTML, "a<tt>b</tt>c",
+ "\"cmd_fontFace\" with \"tt\" should wrap selected text <tt> element");
+
+ editor.innerHTML = "<br>";
+ selection.collapse(editor, 0);
+ SpecialPowers.doCommand(window, "cmd_fontFace", "tt");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ is(editor.innerHTML, "<tt>tt</tt><br>",
+ "Typed text after \"cmd_fontFace\" with \"tt\" should be wrapped by <tt> element");
+
+ // But it shouldn't work with `Document.execCommand()`.
+ editor.innerHTML = "abc";
+ selection.setBaseAndExtent(editor.firstChild, 1, editor.firstChild, 2);
+ document.execCommand("fontname", false, "tt");
+ is(editor.innerHTML, "a<font face=\"tt\">b</font>c",
+ "execCommand(\"fontname\") with \"tt\" should wrap selected text with <font> element");
+
+ editor.innerHTML = "<br>";
+ selection.collapse(editor, 0);
+ document.execCommand("fontname", false, "tt");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ is(editor.innerHTML, "<font face=\"tt\">tt</font><br>",
+ "Typed text after execCommand(\"fontname\") with \"tt\" should be wrapped by <font> element");
+
+ // "cmd_fontFace" with "tt" should remove `<font>` element.
+ editor.innerHTML = "a<font face=\"sans-serif\">b</font>c";
+ selection.selectAllChildren(editor.querySelector("font"));
+ SpecialPowers.doCommand(window, "cmd_fontFace", "tt");
+ is(editor.innerHTML, "a<tt>b</tt>c",
+ "\"cmd_fontFace\" with \"tt\" should wrap selected text <tt> element after removing <font> element");
+
+ editor.innerHTML = "<font face=\"sans-serif\">abc</font>";
+ selection.setBaseAndExtent(editor.firstChild.firstChild, 1, editor.firstChild.firstChild, 2);
+ SpecialPowers.doCommand(window, "cmd_fontFace", "tt");
+ is(editor.innerHTML, "<font face=\"sans-serif\">a</font><tt>b</tt><font face=\"sans-serif\">c</font>",
+ "\"cmd_fontFace\" with \"tt\" should wrap selected text <tt> element after removing <font> element");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_cmd_increaseFont.html b/editor/libeditor/tests/test_cmd_increaseFont.html
new file mode 100644
index 0000000000..ab7b9ba766
--- /dev/null
+++ b/editor/libeditor/tests/test_cmd_increaseFont.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=767684
+-->
+<title>Test for Bug 767684</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=767684">Mozilla Bug 767684</a>
+<div contenteditable>foo<b>bar</b>baz</div>
+<script>
+SimpleTest.waitForExplicitFinish();
+(async () => {
+ getSelection().selectAllChildren(document.querySelector("div"));
+ SpecialPowers.doCommand(window, "cmd_increaseFont");
+ is(
+ document.querySelector("div").innerHTML,
+ "<big>foo<b>bar</b>baz</big>",
+ "All selected text must be embiggened"
+ );
+ SimpleTest.finish();
+})();
+</script>
diff --git a/editor/libeditor/tests/test_cmd_paragraphState.html b/editor/libeditor/tests/test_cmd_paragraphState.html
new file mode 100644
index 0000000000..9b13fc32bf
--- /dev/null
+++ b/editor/libeditor/tests/test_cmd_paragraphState.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing "cmd_paragraphState" behavior</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div contenteditable></div>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ const editor = document.querySelector("div[contenteditable]");
+
+ editor.innerHTML = "<div><p>abc</p></div>";
+ editor.focus();
+ getSelection().collapse(editor.querySelector("p").firstChild, 1);
+ editor.getBoundingClientRect();
+ SpecialPowers.doCommand(window, "cmd_paragraphState", "");
+ is(
+ editor.innerHTML,
+ "<div>abc</div>",
+ "cmd_paragraphState with empty string should remove the parent block element"
+ );
+
+ editor.innerHTML = "<div><div contenteditable=\"false\"><p contenteditable>abc</p></div></div>";
+ editor.focus();
+ getSelection().collapse(editor.querySelector("p").firstChild, 1);
+ editor.getBoundingClientRect();
+ SpecialPowers.doCommand(window, "cmd_paragraphState", "");
+ is(
+ editor.innerHTML,
+ "<div><div contenteditable=\"false\"><p contenteditable=\"\">abc</p></div></div>",
+ "cmd_paragraphState with empty string should not remove editing host"
+ );
+
+ editor.innerHTML = "<div><div contenteditable=\"false\"><p><span contenteditable>abc</span></p></div></div>";
+ editor.focus();
+ getSelection().collapse(editor.querySelector("span").firstChild, 1);
+ editor.getBoundingClientRect();
+ SpecialPowers.doCommand(window, "cmd_paragraphState", "");
+ is(
+ editor.innerHTML,
+ "<div><div contenteditable=\"false\"><p><span contenteditable=\"\">abc</span></p></div></div>",
+ "cmd_paragraphState with empty string should not remove parents of inline editing host"
+ );
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_composition_event_created_in_chrome.html b/editor/libeditor/tests/test_composition_event_created_in_chrome.html
new file mode 100644
index 0000000000..11b24d3bc1
--- /dev/null
+++ b/editor/libeditor/tests/test_composition_event_created_in_chrome.html
@@ -0,0 +1,76 @@
+<!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>
+
+<input id="input">
+
+<script type="application/javascript">
+
+// In nsEditorEventListener, when listening event is not created with proper
+// event interface, it asserts the fact.
+SimpleTest.waitForExplicitFinish();
+
+var gInputElement = document.getElementById("input");
+
+function getEditor(aInputElement) {
+ var editableElement = SpecialPowers.wrap(aInputElement);
+ ok(editableElement.editor, "There is no editor for the input element");
+ return editableElement.editor;
+}
+
+var gEditor;
+
+function testNotGenerateCompositionByCreatedEvents(aEventInterface) {
+ var compositionEvent = document.createEvent(aEventInterface);
+ if (compositionEvent.initCompositionEvent) {
+ compositionEvent.initCompositionEvent("compositionstart", true, true, window, "", "");
+ } else if (compositionEvent.initMouseEvent) {
+ compositionEvent.initMouseEvent("compositionstart", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ }
+ gInputElement.dispatchEvent(compositionEvent);
+ ok(!gEditor.composing, "Composition shouldn't be started with a created compositionstart event (" + aEventInterface + ")");
+
+ compositionEvent = document.createEvent(aEventInterface);
+ if (compositionEvent.initCompositionEvent) {
+ compositionEvent.initCompositionEvent("compositionupdate", true, false, window, "abc", "");
+ } else if (compositionEvent.initMouseEvent) {
+ compositionEvent.initMouseEvent("compositionupdate", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ }
+ gInputElement.dispatchEvent(compositionEvent);
+ ok(!gEditor.composing, "Composition shouldn't be started with a created compositionupdate event (" + aEventInterface + ")");
+ is(gInputElement.value, "", "Input element shouldn't be modified with a created compositionupdate event (" + aEventInterface + ")");
+
+ compositionEvent = document.createEvent(aEventInterface);
+ if (compositionEvent.initCompositionEvent) {
+ compositionEvent.initCompositionEvent("compositionend", true, false, window, "abc", "");
+ } else if (compositionEvent.initMouseEvent) {
+ compositionEvent.initMouseEvent("compositionend", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ }
+ gInputElement.dispatchEvent(compositionEvent);
+ ok(!gEditor.composing, "Composition shouldn't be committed with a created compositionend event (" + aEventInterface + ")");
+ is(gInputElement.value, "", "Input element shouldn't be committed with a created compositionend event (" + aEventInterface + ")");
+}
+
+function doTests() {
+ gInputElement.focus();
+ gEditor = getEditor(gInputElement);
+
+ testNotGenerateCompositionByCreatedEvents("CompositionEvent");
+ testNotGenerateCompositionByCreatedEvents("MouseEvent");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(doTests);
+
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_composition_with_highlight_in_texteditor.html b/editor/libeditor/tests/test_composition_with_highlight_in_texteditor.html
new file mode 100644
index 0000000000..d6bb703231
--- /dev/null
+++ b/editor/libeditor/tests/test_composition_with_highlight_in_texteditor.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test crash bug 1785311</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<input value="abc">
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ const input = document.querySelector("input");
+ input.focus();
+ const editor = SpecialPowers.wrap(input).editor;
+ const selectionController = editor.selectionController;
+ const findSelection = selectionController.getSelection(
+ SpecialPowers.Ci.nsISelectionController.SELECTION_FIND
+ );
+ const editActionListener = {
+ QueryInterface: SpecialPowers.ChromeUtils.generateQI(["nsIEditActionListener"]),
+ WillDeleteText: (textNode, offset, length) => {},
+ DidInsertText: (textNode, offset, aString) => {},
+ WillDeleteRanges: (rangesToDelete) => {},
+ };
+ // Highlight "a"
+ findSelection.setBaseAndExtent(
+ editor.rootElement.firstChild, 0,
+ editor.rootElement.firstChild, 1
+ );
+ try {
+ editor.addEditActionListener(editActionListener);
+ // Start composition at end of <input>
+ editor.selection.collapse(editor.rootElement.firstChild, "abc".length);
+ synthesizeCompositionChange({
+ composition: {
+ string: "d",
+ clauses: [
+ { length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ caret: {
+ start: 1,
+ length: 0,
+ },
+ });
+ synthesizeComposition({
+ type: "compositioncommitasis",
+ });
+ ok(true, "Should not crash");
+ } finally {
+ editor.removeEditActionListener(editActionListener);
+ SimpleTest.finish();
+ }
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_contenteditable_copy_empty_selection.html b/editor/libeditor/tests/test_contenteditable_copy_empty_selection.html
new file mode 100644
index 0000000000..b24ef5ca5a
--- /dev/null
+++ b/editor/libeditor/tests/test_contenteditable_copy_empty_selection.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1857303
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for contenteditable copy event fired even when selection is empty</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <div contenteditable=true id="elem">Copy</div>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(() => {
+ let copyEventFired = false;
+ const editableDiv = document.getElementById("elem");
+
+ editableDiv.addEventListener("copy", function () {
+ copyEventFired = true;
+ });
+
+ editableDiv.focus();
+ synthesizeKey("c", { accelKey: true });
+ ok(copyEventFired, "Copy event should be fired");
+
+ SimpleTest.finish();
+ });
+ </script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_contenteditable_focus.html b/editor/libeditor/tests/test_contenteditable_focus.html
new file mode 100644
index 0000000000..741a5b48bf
--- /dev/null
+++ b/editor/libeditor/tests/test_contenteditable_focus.html
@@ -0,0 +1,335 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for contenteditable focus</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ First text in this document.<br>
+ <input id="inputText" type="text"><br>
+ <input id="inputTextReadonly" type="text" readonly><br>
+ <input id="inputButton" type="button" value="input[type=button]"><br>
+ <button id="button">button</button><br>
+ <div id="primaryEditor" contenteditable="true">
+ editable contents.<br>
+ <input id="inputTextInEditor" type="text"><br>
+ <input id="inputTextReadonlyInEditor" type="text" readonly><br>
+ <input id="inputButtonInEditor" type="button" value="input[type=button]"><br>
+ <button id="buttonInEditor">button</button><br>
+ <div id="noeditableInEditor" contenteditable="false">
+ <span id="spanInNoneditableInEditor">span element in noneditable in editor</span><br>
+ <input id="inputTextInNoneditableInEditor" type="text"><br>
+ <input id="inputTextReadonlyInNoneditableInEditor" type="text" readonly><br>
+ <input id="inputButtonInNoneditableInEditor" type="button" value="input[type=button]"><br>
+ <button id="buttonInNoneditableInEditor">button</button><br>
+ </div>
+ <span id="spanInEditor">span element in editor</span><br>
+ </div>
+ <div id="otherEditor" contenteditable="true">
+ other editor.
+ </div>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ function getNodeDescription(aNode) {
+ if (aNode === undefined) {
+ return "undefined";
+ }
+ if (aNode === null) {
+ return "null";
+ }
+ switch (aNode.nodeType) {
+ case Node.TEXT_NODE:
+ return `${aNode.nodeName}, "${aNode.data.replace(/\n/g, "\\n")}"`;
+ case Node.ELEMENT_NODE:
+ return `<${aNode.tagName.toLowerCase()}${
+ aNode.getAttribute("id") !== null
+ ? ` id="${aNode.getAttribute("id")}"`
+ : ""
+ }${
+ aNode.getAttribute("readonly") !== null
+ ? " readonly"
+ : ""
+ }${
+ aNode.getAttribute("type") !== null
+ ? ` type="${aNode.getAttribute("type")}"`
+ : ""
+ }>`;
+ }
+ return aNode.nodeName;
+ }
+
+ const fm = SpecialPowers.Services.focus;
+ // XXX using selCon for checking the visibility of the caret, however,
+ // selCon is shared in document, cannot get the element of owner of the
+ // caret from javascript?
+ const selCon = SpecialPowers.wrap(window).docShell.
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsISelectionDisplay).
+ QueryInterface(SpecialPowers.Ci.nsISelectionController);
+
+ const primaryEditor = document.getElementById("primaryEditor");
+ const spanInEditor = document.getElementById("spanInEditor");
+ const otherEditor = document.getElementById("otherEditor");
+
+ (function test_initial_state_on_load() {
+ is(
+ getSelection().rangeCount,
+ 0,
+ "There should be no selection range at start"
+ );
+ ok(!selCon.caretVisible, "The caret should not be visible in the document");
+ // Move focus to <input type="text"> in the primary editor
+ primaryEditor.querySelector("input[type=text]").focus();
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ primaryEditor.querySelector("input[type=text]"),
+ '<input type="text"> in the primary editor should get focus'
+ );
+ todo_is(
+ getSelection().rangeCount,
+ 0,
+ 'There should be no selection range after calling focus() of <input type="text"> in the primary editor'
+ );
+ ok(
+ selCon.caretVisible,
+ 'The caret should not be visible in the <input type="text"> in the primary editor'
+ );
+ })();
+ // Move focus to the editor
+ (function test_move_focus_from_child_input_to_parent_editor() {
+ primaryEditor.focus();
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ primaryEditor,
+ `The editor should steal focus from <input type="text"> in the primary editor with calling its focus() (got ${
+ getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
+ }`
+ );
+ is(
+ getSelection().rangeCount,
+ 1,
+ "There should be one range after focus() of the editor is called"
+ );
+ const range = getSelection().getRangeAt(0);
+ ok(
+ range.collapsed,
+ "The selection range should be collapsed (immediately after calling focus() of the editor)"
+ );
+ is(
+ range.startContainer,
+ primaryEditor.firstChild,
+ `The selection range should be in the first text node of the editor (immediately after calling focus() of the editor, got ${
+ getNodeDescription(range.startContainer)
+ })`
+ );
+ ok(
+ selCon.caretVisible,
+ "The caret should be visible in the primary editor (immediately after calling focus() of the editor)"
+ );
+ })();
+ // Move focus to other editor
+ (function test_move_focus_from_editor_to_the_other_editor() {
+ otherEditor.focus();
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ otherEditor,
+ `The other editor should steal focus from the editor (got ${
+ getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
+ }`
+ );
+ is(
+ getSelection().rangeCount,
+ 1,
+ "There should be one range after focus() of the other editor is called"
+ );
+ const range = getSelection().getRangeAt(0);
+ ok(
+ range.collapsed,
+ "The selection range should be collapsed (immediately after calling focus() of the other editor)"
+ );
+ is(
+ range.startContainer,
+ otherEditor.firstChild,
+ `The selection range should be in the first text node of the editor (immediately after calling focus() of the other editor, got ${
+ getNodeDescription(range.startContainer)
+ })`
+ );
+ ok(
+ selCon.caretVisible,
+ "The caret should be visible in the primary editor (immediately after calling focus() of the other editor)"
+ );
+ })();
+ // Move focus to <input type="text"> in the primary editor
+ (function test_move_focus_from_the_other_editor_to_input_in_the_editor() {
+ primaryEditor.querySelector("input[type=text]").focus();
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ primaryEditor.querySelector("input[type=text]"),
+ `<input type="text"> in the primary editor should steal focus from the other editor (got ${
+ getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
+ }`);
+ is(
+ getSelection().rangeCount,
+ 1,
+ 'There should be one range after focus() of the <input type="text"> in the primary editor is called'
+ );
+ const range = getSelection().getRangeAt(0);
+ ok(
+ range.collapsed,
+ 'The selection range should be collapsed (immediately after calling focus() of the <input type="text"> in the primary editor)'
+ );
+ // XXX maybe, the caret can stay on the other editor if it's better.
+ is(
+ range.startContainer,
+ primaryEditor.firstChild,
+ `The selection range should be in the first text node of the editor (immediately after calling focus() of the <input type="text"> in the primary editor, got ${
+ getNodeDescription(range.startContainer)
+ })`
+ );
+ ok(
+ selCon.caretVisible,
+ 'The caret should be visible in the <input type="text"> (immediately after calling focus() of the <input type="text"> in the primary editor)'
+ );
+ })();
+ // Move focus to the other editor again
+ (function test_move_focus_from_the_other_editor_to_the_editor_with_Selection_API() {
+ otherEditor.focus();
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ otherEditor,
+ `The other editor should steal focus from the <input type="text"> in the primary editor with its focus() (got ${
+ getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
+ })`
+ );
+ // Set selection to the span element in the primary editor.
+ getSelection().collapse(spanInEditor.firstChild, 5);
+ is(
+ getSelection().rangeCount,
+ 1,
+ "There should be one selection range after collapsing selection into the <span> in the primary editor when the other editor has focus"
+ );
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ primaryEditor,
+ `The editor should steal focus from the other editor with Selection API (got ${
+ getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
+ }`
+ );
+ ok(
+ selCon.caretVisible,
+ "The caret should be visible in the primary editor (immediately after moving focus with Selection API)"
+ );
+ })();
+ // Move focus to the editor
+ (function test_move_focus() {
+ primaryEditor.focus();
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ primaryEditor,
+ "The editor should keep having focus (immediately after calling focus() of the editor when it has focus)"
+ );
+ is(
+ getSelection().rangeCount,
+ 1,
+ "There should be one selection range in the primary editor (immediately after calling focus() of the editor when it has focus)"
+ );
+ const range = getSelection().getRangeAt(0);
+ ok(
+ range.collapsed,
+ "The selection range should be collapsed in the primary editor (immediately after calling focus() of the editor when it has focus)"
+ );
+ is(
+ range.startOffset,
+ 5,
+ "The startOffset of the selection range shouldn't be changed (immediately after calling focus() of the editor when it has focus)"
+ );
+ is(
+ range.startContainer.parentNode,
+ spanInEditor,
+ `The startContainer of the selection range shouldn't be changed (immediately after calling focus() of the editor when it has focus, got ${
+ getNodeDescription(range.startContainer)
+ })`);
+ ok(
+ selCon.caretVisible,
+ "The caret should be visible in the primary editor (immediately after calling focus() of the editor when it has focus)"
+ );
+ })();
+
+ // Move focus to each focusable element in the primary editor.
+ function test_move_focus_from_the_editor(aTargetElement, aFocusable, aCaretVisible) {
+ primaryEditor.focus();
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ primaryEditor,
+ `The editor should have focus at preparing to move focus to ${
+ getNodeDescription(aTargetElement)
+ } (got ${
+ getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
+ }`);
+ aTargetElement.focus();
+ if (aFocusable) {
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ aTargetElement,
+ `${
+ getNodeDescription(aTargetElement)
+ } should get focus with calling its focus() (got ${
+ getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
+ }`
+ );
+ } else {
+ is(
+ SpecialPowers.unwrap(fm.focusedElement),
+ primaryEditor,
+ `${
+ getNodeDescription(aTargetElement)
+ } should not take focus with calling its focus() (got ${
+ getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
+ }`
+ );
+ }
+ is(
+ selCon.caretVisible,
+ aCaretVisible,
+ `The caret ${
+ aCaretVisible ? "should" : "should not"
+ } visible after calling focus() of ${
+ getNodeDescription(aTargetElement)
+ }`
+ );
+ }
+ test_move_focus_from_the_editor(primaryEditor.querySelector("input[type=text]"), true, true);
+ test_move_focus_from_the_editor(primaryEditor.querySelector("input[type=text][readonly]"), true, true);
+ // XXX shouldn't the caret become invisible?
+ test_move_focus_from_the_editor(primaryEditor.querySelector("input[type=button]"), true, true);
+ test_move_focus_from_the_editor(primaryEditor.querySelector("button"), true, true);
+ test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false]"), false, true);
+ test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > span"), false, true);
+ test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > input[type=text]"), true, true);
+ test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > input[type=text][readonly]"), true, true);
+ test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > input[type=button]"), true, false);
+ test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > button"), true, false);
+ test_move_focus_from_the_editor(spanInEditor, false, true);
+ test_move_focus_from_the_editor(document.querySelector("input[type=text]"), true, true);
+ test_move_focus_from_the_editor(document.querySelector("input[type=text][readonly]"), true, true);
+ test_move_focus_from_the_editor(document.querySelector("input[type=button]"), true, false);
+ test_move_focus_from_the_editor(document.querySelector("button"), true, false);
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_contenteditable_text_input_handling.html b/editor/libeditor/tests/test_contenteditable_text_input_handling.html
new file mode 100644
index 0000000000..c00056a0f0
--- /dev/null
+++ b/editor/libeditor/tests/test_contenteditable_text_input_handling.html
@@ -0,0 +1,317 @@
+<html>
+<head>
+ <title>Test for text input event handling on contenteditable editor</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <p id="static">static content<input id="inputInStatic"><textarea id="textareaInStatic"></textarea></p>
+ <p id="editor"contenteditable="true">content editable<input id="inputInEditor"><textarea id="textareaInEditor"></textarea></p>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const kLF = !navigator.platform.indexOf("Win") && false ? "\r\n" : "\n";
+
+function runTests() {
+ var fm = Services.focus;
+
+ var listener = {
+ handleEvent: function _hv(aEvent) {
+ aEvent.preventDefault(); // prevent the browser default behavior
+ },
+ };
+ var els = Services.els;
+ els.addSystemEventListener(window, "keypress", listener, false);
+
+ var staticContent = document.getElementById("static");
+ staticContent._defaultValue = getTextValue(staticContent);
+ staticContent._isFocusable = false;
+ staticContent._isEditable = false;
+ staticContent._isContentEditable = false;
+ staticContent._description = "non-editable p element";
+ var inputInStatic = document.getElementById("inputInStatic");
+ inputInStatic._defaultValue = getTextValue(inputInStatic);
+ inputInStatic._isFocusable = true;
+ inputInStatic._isEditable = true;
+ inputInStatic._isContentEditable = false;
+ inputInStatic._description = "input element in static content";
+ var textareaInStatic = document.getElementById("textareaInStatic");
+ textareaInStatic._defaultValue = getTextValue(textareaInStatic);
+ textareaInStatic._isFocusable = true;
+ textareaInStatic._isEditable = true;
+ textareaInStatic._isContentEditable = false;
+ textareaInStatic._description = "textarea element in static content";
+ var editor = document.getElementById("editor");
+ editor._defaultValue = getTextValue(editor);
+ editor._isFocusable = true;
+ editor._isEditable = true;
+ editor._isContentEditable = true;
+ editor._description = "contenteditable editor";
+ var inputInEditor = document.getElementById("inputInEditor");
+ inputInEditor._defaultValue = getTextValue(inputInEditor);
+ inputInEditor._isFocusable = true;
+ inputInEditor._isEditable = true;
+ inputInEditor._isContentEditable = false;
+ inputInEditor._description = "input element in contenteditable editor";
+ var textareaInEditor = document.getElementById("textareaInEditor");
+ textareaInEditor._defaultValue = getTextValue(textareaInEditor);
+ textareaInEditor._isFocusable = true;
+ textareaInEditor._isEditable = true;
+ textareaInEditor._isContentEditable = false;
+ textareaInEditor._description = "textarea element in contenteditable editor";
+
+ function getTextValue(aElement) {
+ if (aElement == editor) {
+ var value = "";
+ for (var node = aElement.firstChild; node; node = node.nextSibling) {
+ if (node.nodeType == Node.TEXT_NODE) {
+ value += node.data;
+ } else if (node.nodeType == Node.ELEMENT_NODE) {
+ var tagName = node.tagName.toLowerCase();
+ switch (tagName) {
+ case "input":
+ case "textarea":
+ value += kLF;
+ break;
+ default:
+ ok(false, "Undefined tag is used in the editor: " + tagName);
+ break;
+ }
+ }
+ }
+ return value;
+ }
+ return aElement.value;
+ }
+
+ function testTextInput(aFocus) {
+ var when = " when " +
+ ((aFocus && aFocus._isFocusable) ? aFocus._description + " has focus" :
+ "nobody has focus");
+
+ function checkValue(aElement, aInsertedText) {
+ if (aElement == aFocus && aElement._isEditable) {
+ is(getTextValue(aElement), aInsertedText + aElement._defaultValue,
+ aElement._description +
+ " wasn't edited by synthesized key events" + when);
+ return;
+ }
+ is(getTextValue(aElement), aElement._defaultValue,
+ aElement._description +
+ " was edited by synthesized key events" + when);
+ }
+
+ if (aFocus && aFocus._isFocusable) {
+ aFocus.focus();
+ is(fm.focusedElement, aFocus,
+ aFocus._description + " didn't get focus at preparing tests" + when);
+ } else {
+ var focusedElement = fm.focusedElement;
+ if (focusedElement) {
+ focusedElement.blur();
+ }
+ ok(!fm.focusedElement,
+ "Failed to blur at preparing tests" + when);
+ }
+
+ if (aFocus && aFocus._isFocusable) {
+ sendString("ABC");
+ checkValue(staticContent, "ABC");
+ checkValue(inputInStatic, "ABC");
+ checkValue(textareaInStatic, "ABC");
+ checkValue(editor, "ABC");
+ checkValue(inputInEditor, "ABC");
+ checkValue(textareaInEditor, "ABC");
+
+ if (aFocus._isEditable) {
+ synthesizeKey("KEY_Backspace", {repeat: 3});
+ checkValue(staticContent, "");
+ checkValue(inputInStatic, "");
+ checkValue(textareaInStatic, "");
+ checkValue(editor, "");
+ checkValue(inputInEditor, "");
+ checkValue(textareaInEditor, "");
+ }
+ }
+
+ // When key events are fired on unfocused editor.
+ function testDispatchedKeyEvent(aTarget) {
+ var targetDescription = " (dispatched to " + aTarget._description + ")";
+ function dispatchKeyEvent(aKeyCode, aChar, aDispatchTarget) {
+ var keyEvent = new KeyboardEvent("keypress", {
+ bubbles: true,
+ cancelable: true,
+ view: null,
+ keyCode: aKeyCode,
+ charCode: aChar ? aChar.charCodeAt(0) : 0
+ });
+ aDispatchTarget.dispatchEvent(keyEvent);
+ }
+
+ function checkValueForDispatchedKeyEvent(aElement, aInsertedText) {
+ if (aElement == aTarget && aElement._isEditable &&
+ (!aElement._isContentEditable || aElement == aFocus)) {
+ is(getTextValue(aElement), aInsertedText + aElement._defaultValue,
+ aElement._description +
+ " wasn't edited by dispatched key events" +
+ when + targetDescription);
+ return;
+ }
+ if (aElement == aTarget) {
+ is(getTextValue(aElement), aElement._defaultValue,
+ aElement._description +
+ " was edited by dispatched key events" +
+ when + targetDescription);
+ return;
+ }
+ is(getTextValue(aElement), aElement._defaultValue,
+ aElement._description +
+ " was edited by key events unexpectedly" +
+ when + targetDescription);
+ }
+
+ dispatchKeyEvent(0, "A", aTarget);
+ dispatchKeyEvent(0, "B", aTarget);
+ dispatchKeyEvent(0, "C", aTarget);
+
+ checkValueForDispatchedKeyEvent(staticContent, "ABC");
+ checkValueForDispatchedKeyEvent(inputInStatic, "ABC");
+ checkValueForDispatchedKeyEvent(textareaInStatic, "ABC");
+ checkValueForDispatchedKeyEvent(editor, "ABC");
+ checkValueForDispatchedKeyEvent(inputInEditor, "ABC");
+ checkValueForDispatchedKeyEvent(textareaInEditor, "ABC");
+
+ dispatchKeyEvent(KeyboardEvent.DOM_VK_BACK_SPACE, 0, aTarget);
+ dispatchKeyEvent(KeyboardEvent.DOM_VK_BACK_SPACE, 0, aTarget);
+ dispatchKeyEvent(KeyboardEvent.DOM_VK_BACK_SPACE, 0, aTarget);
+
+ checkValueForDispatchedKeyEvent(staticContent, "");
+ checkValueForDispatchedKeyEvent(inputInStatic, "");
+ checkValueForDispatchedKeyEvent(textareaInStatic, "");
+ checkValueForDispatchedKeyEvent(editor, "");
+ checkValueForDispatchedKeyEvent(inputInEditor, "");
+ checkValueForDispatchedKeyEvent(textareaInEditor, "");
+ }
+
+ testDispatchedKeyEvent(staticContent);
+ testDispatchedKeyEvent(inputInStatic);
+ testDispatchedKeyEvent(textareaInStatic);
+ testDispatchedKeyEvent(editor);
+ testDispatchedKeyEvent(inputInEditor);
+ testDispatchedKeyEvent(textareaInEditor);
+
+ if (!aFocus._isEditable) {
+ return;
+ }
+
+ // IME
+ // input first character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": 1, "length": 0 },
+ });
+ var queryText = synthesizeQueryTextContent(0, 100);
+ ok(queryText, "query text event result is null" + when);
+ if (!queryText) {
+ return;
+ }
+ ok(queryText.succeeded, "query text event failed" + when);
+ if (!queryText.succeeded) {
+ return;
+ }
+ is(queryText.text, "\u3089" + aFocus._defaultValue,
+ "composing text is incorrect" + when);
+ var querySelectedText = synthesizeQuerySelectedText();
+ ok(querySelectedText, "query selected text event result is null" + when);
+ if (!querySelectedText) {
+ return;
+ }
+ ok(querySelectedText.succeeded, "query selected text event failed" + when);
+ if (!querySelectedText.succeeded) {
+ return;
+ }
+ is(querySelectedText.offset, 1,
+ "query selected text event returns wrong offset" + when);
+ is(querySelectedText.text, "",
+ "query selected text event returns wrong selected text" + when);
+ // commit composition
+ synthesizeComposition({ type: "compositioncommitasis" });
+ queryText = synthesizeQueryTextContent(0, 100);
+ ok(queryText, "query text event result is null after commit" + when);
+ if (!queryText) {
+ return;
+ }
+ ok(queryText.succeeded, "query text event failed after commit" + when);
+ if (!queryText.succeeded) {
+ return;
+ }
+ is(queryText.text, "\u3089" + aFocus._defaultValue,
+ "composing text is incorrect after commit" + when);
+ querySelectedText = synthesizeQuerySelectedText();
+ ok(querySelectedText,
+ "query selected text event result is null after commit" + when);
+ if (!querySelectedText) {
+ return;
+ }
+ ok(querySelectedText.succeeded,
+ "query selected text event failed after commit" + when);
+ if (!querySelectedText.succeeded) {
+ return;
+ }
+ is(querySelectedText.offset, 1,
+ "query selected text event returns wrong offset after commit" + when);
+ is(querySelectedText.text, "",
+ "query selected text event returns wrong selected text after commit" +
+ when);
+
+ checkValue(staticContent, "\u3089");
+ checkValue(inputInStatic, "\u3089");
+ checkValue(textareaInStatic, "\u3089");
+ checkValue(editor, "\u3089");
+ checkValue(inputInEditor, "\u3089");
+ checkValue(textareaInEditor, "\u3089");
+
+ synthesizeKey("KEY_Backspace");
+ checkValue(staticContent, "");
+ checkValue(inputInStatic, "");
+ checkValue(textareaInStatic, "");
+ checkValue(editor, "");
+ checkValue(inputInEditor, "");
+ checkValue(textareaInEditor, "");
+ }
+
+ testTextInput(inputInStatic);
+ testTextInput(textareaInStatic);
+ testTextInput(editor);
+ testTextInput(inputInEditor);
+ testTextInput(textareaInEditor);
+
+ els.removeSystemEventListener(window, "keypress", listener, false);
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_cut_copy_delete_command_enabled.html b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.html
new file mode 100644
index 0000000000..9e70178b11
--- /dev/null
+++ b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.html
@@ -0,0 +1,227 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1067255
+-->
+
+<head>
+ <title>Test for enabled state of cut/copy/delete commands</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body onload="doTest();">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1067255">Mozilla Bug 1067255</a>
+
+ <pre id="test">
+ <script type="application/javascript">
+ /** Test for Bug 1067255 **/
+ SimpleTest.waitForExplicitFinish();
+
+ function doTest() {
+ var text = $("text-field");
+ var textWithHandlers = $("text-field-2");
+ var password = $("password-field");
+ var passwordWithHandlers = $("password-field-2");
+ var textWithParentHandlers = $("text-field-3");
+ var passwordWithParentHandlers = $("password-field-3");
+ var textarea1 = $("text-area-1");
+ var textarea2 = $("text-area-2");
+ var textarea3 = $("text-area-3");
+
+ var editor1 = SpecialPowers.wrap(text).editor;
+ var editor2 = SpecialPowers.wrap(password).editor;
+ var editor3 = SpecialPowers.wrap(textWithHandlers).editor;
+ var editor4 = SpecialPowers.wrap(passwordWithHandlers).editor;
+ var editor5 = SpecialPowers.wrap(textWithParentHandlers).editor;
+ var editor6 = SpecialPowers.wrap(passwordWithParentHandlers).editor;
+ var editor7 = SpecialPowers.wrap(textarea1).editor;
+ var editor8 = SpecialPowers.wrap(textarea2).editor;
+ var editor9 = SpecialPowers.wrap(textarea3).editor;
+
+ text.focus();
+
+ ok(!editor1.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=text> with no selection");
+ ok(!editor1.canCut(),
+ "nsIEditor.canCut() should return false in <input type=text> with no selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=text> with no selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=text> with no selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be disabled in <input type=text> with no selection");
+
+ text.select();
+
+ ok(editor1.canCopy(),
+ "nsIEditor.canCopy() should return true in <input type=text> with selection");
+ ok(editor1.canCut(),
+ "nsIEditor.canCut() should return true in <input type=text> with selection");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <input type=text> with selection");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <input type=text> with selection");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=text> with selection");
+
+ password.focus();
+
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> with no selection");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> with no selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> with no selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> with no selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be disabled in <input type=password> with no selection");
+
+ // Copy and cut commands don't do anything on password fields by default,
+ // so they remain disabled even when there is a selection...
+ password.select();
+
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> with selection");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> with selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> with selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> with selection");
+ // Delete, on the other hand, does apply to password fields.
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password with selection>");
+
+ // ...but webpages can hook up event handlers to cut/copy events, so we have to
+ // keep the cut and copy commands enabled if event handlers are attached,
+ // for both regular edit fields and password fields (even when there's no
+ // selection, as we don't know what the handler might want to do).
+ textWithHandlers.focus();
+
+ ok(editor3.canCopy(),
+ "nsIEditor.canCopy() should return true in <input type=text> with event handler");
+ ok(editor3.canCut(),
+ "nsIEditor.canCut() should return true in <input type=text> with event handler");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <input type=text> with event handler");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <input type=text> with event handler");
+
+ passwordWithHandlers.focus();
+
+ ok(editor4.canCopy(),
+ "nsIEditor.canCopy() should return true in <input type=password> with event handler");
+ ok(editor4.canCut(),
+ "nsIEditor.canCut() should return true in <input type=password> with event handler");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <input type=password> with event handler");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <input type=password> with event handler");
+
+ // Also check that the commands are enabled if there's a handler on a parent element.
+ textWithParentHandlers.focus();
+
+ ok(editor5.canCopy(),
+ "nsIEditor.canCopy() should return true in <input type=text> with event handler on an ancestor");
+ ok(editor5.canCut(),
+ "nsIEditor.canCut() should return true in <input type=text> with event handler on an ancestor");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <input type=text> with event handler on an ancestor");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <input type=text> with event handler on an ancestor");
+
+ passwordWithParentHandlers.focus();
+
+ ok(editor6.canCopy(),
+ "nsIEditor.canCopy() should return true in <input type=password> with event handler on an ancestor");
+ ok(editor6.canCut(),
+ "nsIEditor.canCut() should return true in <input type=password> with event handler on an ancestor");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <input type=password> with event handler on an ancestor");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <input type=password> with event handler on an ancestor");
+
+ // TEXTAREA tests
+
+ textarea1.focus();
+
+ ok(!editor7.canCopy(),
+ "nsIEditor.canCopy() should return false in <textarea> with no selection");
+ ok(!editor7.canCut(),
+ "nsIEditor.canCut() should return false in <textarea> with no selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <textarea> with no selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <textarea> with no selection");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be disabled in <textarea> with no selection");
+
+ textarea1.select();
+
+ ok(editor7.canCopy(),
+ "nsIEditor.canCopy() should return true in <textarea> with selection");
+ ok(editor7.canCut(),
+ "nsIEditor.canCut() should return true in <textarea> with selection");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <textarea> with selection");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <textarea> with selection");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <textarea> with selection");
+
+ textarea2.focus();
+
+ ok(!editor8.canCopy(),
+ "nsIEditor.canCopy() should return false in <textarea> with only a 'cut' handler");
+ ok(editor8.canCut(),
+ "nsIEditor.canCut() should return true in <textarea> with only a 'cut' handler");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <textarea> with only a 'cut' handler");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <textarea> with only a 'cut' handler");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be disabled in <textarea> with only a 'cut' handler");
+
+ textarea3.focus();
+
+ ok(editor9.canCopy(),
+ "nsIEditor.canCopy() should return true in <textarea> with only a 'copy' handler on ancestor");
+ ok(!editor9.canCut(),
+ "nsIEditor.canCut() should return false in <textarea> with only a 'copy' handler on ancestor");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <textarea> with only a 'copy' handler on ancestor");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <textarea> with only a 'copy' handler on ancestor");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be disabled in <textarea> with only a 'copy' handler on ancestor");
+
+ SimpleTest.finish();
+ }
+ </script>
+ </pre>
+
+ <input type="text" value="Gonzo says hi" id="text-field" />
+ <input type="password" value="Jan also" id="password-field" />
+ <input type="text" value="Hi says Gonzo" id="text-field-2" oncut="cut()" oncopy="copy()" ondelete="delete()"/>
+ <input type="password" value="Also Jan" id="password-field-2" oncut="cut()" oncopy="copy()" ondelete="delete()"/>
+ <div oncut="cut()">
+ <ul oncopy="copy()">
+ <li><input type="text" value="Hi again" id="text-field-3"/></li>
+ <li><input type="password" value="And again, hi" id="password-field-3"/></li>
+ </ul>
+ </div>
+ <textarea id="text-area-1">textarea</textarea>
+ <textarea oncut="cut()" id="text-area-2">textarea with cut handler</textarea>
+ <div oncopy="copy()">
+ <blockquote>
+ <p><textarea id="text-area-3">textarea with copy handler on parent</textarea></p>
+ </blockquote>
+ </div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_cut_copy_delete_command_enabled.xhtml b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.xhtml
new file mode 100644
index 0000000000..46c91e87d0
--- /dev/null
+++ b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.xhtml
@@ -0,0 +1,215 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Test for enabled state of cut/copy/delete commands">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(() => {
+ let text = document.getElementById("textbox");
+ let password = document.getElementById("password");
+
+ let editor1 = text.editor;
+ let editor2 = password.editor;
+
+ text.focus();
+ text.select();
+
+ ok(editor1.canCopy(),
+ "nsIEditor.canCopy() should return true in <input>");
+ ok(editor1.canCut(),
+ "nsIEditor.canCut() should return true in <input>");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <input>");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <input>");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input>");
+
+ password.focus();
+ password.select();
+
+ // Copy and cut commands should be disabled on password fields.
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password>");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password>");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password>");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password>");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password>");
+
+ // If selection is in unmasked range, allow to copy the selected
+ // password into the clipboard.
+ editor2.unmask(0);
+ ok(editor2.canCopy(),
+ "nsIEditor.canCopy() should return true in <input type=password> if the password is unmasked");
+ ok(editor2.canCut(),
+ "nsIEditor.canCut() should return true in <input type=password> if the password is unmasked");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <input type=password> if the password is unmasked");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <input type=password> if the password is unmasked");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> if the password is unmasked");
+
+ // If unmasked range will be masked automatically, we shouldn't allow to
+ // copy the selected password since the state may be changed during
+ // showing edit menu or something.
+ editor2.unmask(0, 13, 1000);
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> if the password is unmasked but will be masked automatically");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> if the password is unmasked but will be masked automatically");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> if the password is unmasked but will be masked automatically");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> if the password is unmasked but will be masked automatically");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> if the password is unmasked but will be masked automatically");
+
+ // <input type="password"> does not support setSelectionRange() oddly.
+ function setSelectionRange(aEditor, aStart, aEnd) {
+ let container = aEditor.rootElement.firstChild;
+ aEditor.selection.setBaseAndExtent(container, aStart, container, aEnd);
+ }
+
+ // Check the range boundaries.
+ editor2.unmask(3, 9);
+ setSelectionRange(editor2, 0, 2);
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 0-2)");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 0-2)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 0-2)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 0-2)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 0-2)");
+
+ setSelectionRange(editor2, 2, 3);
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 2-3)");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 2-3)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-3)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-3)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 2-3)");
+
+ setSelectionRange(editor2, 2, 5);
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 2-5)");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 2-5)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-5)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-5)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 2-5)");
+
+ setSelectionRange(editor2, 2, 10);
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 2-10)");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 2-10)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-10)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-10)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 2-10)");
+
+ setSelectionRange(editor2, 2, 10);
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 3-10)");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 3-10)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 3-10)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 3-10)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 3-10)");
+
+ setSelectionRange(editor2, 8, 12);
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 8-12)");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 8-12)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 8-12)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 8-12)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 8-12)");
+
+ setSelectionRange(editor2, 9, 12);
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 9-12)");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 9-12)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 9-12)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 9-12)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 9-12)");
+
+ setSelectionRange(editor2, 10, 12);
+ ok(!editor2.canCopy(),
+ "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 10-12)");
+ ok(!editor2.canCut(),
+ "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 10-12)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 10-12)");
+ ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 10-12)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 10-12)");
+
+ setSelectionRange(editor2, 3, 9);
+ ok(editor2.canCopy(),
+ "nsIEditor.canCopy() should return true in <input type=password> (unmasked range 3-9, selected range 3-9)");
+ ok(editor2.canCut(),
+ "nsIEditor.canCut() should return true in <input type=password> (unmasked range 3-9, selected range 3-9)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <input type=password> (unmasked range 3-9, selected range 3-9)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <input type=password> (unmasked range 3-9, selected range 3-9)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 3-9)");
+
+ setSelectionRange(editor2, 4, 8);
+ ok(editor2.canCopy(),
+ "nsIEditor.canCopy() should return true in <input type=password> (unmasked range 3-9, selected range 4-8)");
+ ok(editor2.canCut(),
+ "nsIEditor.canCut() should return true in <input type=password> (unmasked range 3-9, selected range 4-8)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"),
+ "cmd_copy command should be enabled in <input type=password> (unmasked range 3-9, selected range 4-8)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"),
+ "cmd_cut command should be enabled in <input type=password> (unmasked range 3-9, selected range 4-8)");
+ ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"),
+ "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 4-8)");
+
+ SimpleTest.finish();
+ });
+ ]]></script>
+
+ <vbox flex="1">
+ <input xmlns="http://www.w3.org/1999/xhtml" id="textbox" value="normal text"/>
+ <input xmlns="http://www.w3.org/1999/xhtml" id="password" type="password" value="password text"/>
+ </vbox>
+
+</window>
diff --git a/editor/libeditor/tests/test_cut_copy_password.html b/editor/libeditor/tests/test_cut_copy_password.html
new file mode 100644
index 0000000000..4e0319d68c
--- /dev/null
+++ b/editor/libeditor/tests/test_cut_copy_password.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test for cut/copy in password field</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <input type="password">
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ let input = document.getElementsByTagName("input")[0];
+ let editor = SpecialPowers.wrap(input).editor;
+ const kMask = editor.passwordMask;
+ async function copyToClipboard(aExpectedValue) {
+ try {
+ await SimpleTest.promiseClipboardChange(
+ aExpectedValue, () => { SpecialPowers.doCommand(window, "cmd_copy"); },
+ undefined, undefined, aExpectedValue === null);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ async function cutToClipboard(aExpectedValue) {
+ try {
+ await SimpleTest.promiseClipboardChange(
+ aExpectedValue, () => { SpecialPowers.doCommand(window, "cmd_cut"); },
+ undefined, undefined, aExpectedValue === null);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ input.value = "abcdef";
+ input.focus();
+
+ input.setSelectionRange(0, 6);
+ ok(true, "Trying to copy masked password...");
+ await copyToClipboard(null);
+ isnot(SpecialPowers.getClipboardData("text/plain"), "abcdef",
+ "Copying masked password shouldn't copy raw value into the clipboard");
+ isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "Copying masked password shouldn't copy masked value into the clipboard");
+ ok(true, "Trying to cut masked password...");
+ await cutToClipboard(null);
+ isnot(SpecialPowers.getClipboardData("text/plain"), "abcdef",
+ "Cutting masked password shouldn't copy raw value into the clipboard");
+ isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "Cutting masked password shouldn't copy masked value into the clipboard");
+ is(input.value, "abcdef",
+ "Cutting masked password shouldn't modify the value");
+
+ editor.unmask(2, 4);
+ input.setSelectionRange(0, 6);
+ ok(true, "Trying to copy partially masked password...");
+ await copyToClipboard(null);
+ isnot(SpecialPowers.getClipboardData("text/plain"), "abcdef",
+ "Copying partially masked password shouldn't copy raw value into the clipboard");
+ isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}cd${kMask}${kMask}`,
+ "Copying partially masked password shouldn't copy partially masked value into the clipboard");
+ isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "Copying partially masked password shouldn't copy masked value into the clipboard");
+ ok(true, "Trying to cut partially masked password...");
+ await cutToClipboard(null);
+ isnot(SpecialPowers.getClipboardData("text/plain"), "abcdef",
+ "Cutting partially masked password shouldn't copy raw value into the clipboard");
+ isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}cd${kMask}${kMask}`,
+ "Cutting partially masked password shouldn't copy partially masked value into the clipboard");
+ isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "Cutting partially masked password shouldn't copy masked value into the clipboard");
+ is(input.value, "abcdef",
+ "Cutting partially masked password shouldn't modify the value");
+
+ input.setSelectionRange(2, 4);
+ ok(true, "Trying to copy unmasked password...");
+ await copyToClipboard("cd");
+ is(input.value, "abcdef",
+ "Copying unmasked password shouldn't modify the value");
+
+ input.value = "012345";
+ editor.unmask(2, 4);
+ input.setSelectionRange(2, 4);
+ ok(true, "Trying to cut unmasked password...");
+ await cutToClipboard("23");
+ is(input.value, "0145",
+ "Cutting unmasked password should modify the value");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_defaultParagraphSeparatorBR_between_blocks.html b/editor/libeditor/tests/test_defaultParagraphSeparatorBR_between_blocks.html
new file mode 100644
index 0000000000..fdb673a6e1
--- /dev/null
+++ b/editor/libeditor/tests/test_defaultParagraphSeparatorBR_between_blocks.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for insertParagraph when defaultParagraphSeparator is br and between blocks</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(() => {
+ const editor = document.createElement("div");
+ editor.setAttribute("contenteditable", "");
+ editor.innerHTML = "<div>abc</div><div>def</div>";
+ document.body.appendChild(editor);
+ editor.focus();
+ document.execCommand("defaultParagraphSeparator", false, "br");
+ getSelection().collapse(editor, 1); // put caret between the <div>s
+ ok(
+ document.execCommand("insertParagraph"),
+ 'execCommand("insertParagraph") should return true'
+ )
+ is(
+ editor.innerHTML,
+ "<div>abc</div><br><div>def</div>",
+ "<br> element should be inserted between the <div> elements"
+ );
+ ok(
+ getSelection().isCollapsed,
+ "Selection should be collapsed after insertParagraph"
+ );
+ is(
+ getSelection().focusNode,
+ editor,
+ "Caret should be in the editing host"
+ );
+ is(
+ getSelection().focusOffset,
+ 1,
+ "Caret should be around the <br> element"
+ );
+ is(
+ SpecialPowers.wrap(getSelection()).interlinePosition,
+ true,
+ "Caret should be painted at start of the new line"
+ );
+ document.execCommand("insertText", false, "X");
+ todo_is(
+ editor.innerHTML,
+ "<div>abc</div><br>X<div>def</div>",
+ '"X" should be inserted after the inserted <br> element'
+ );
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_doc_scrollbar_toggled_designMode_on_mousedown.html b/editor/libeditor/tests/test_doc_scrollbar_toggled_designMode_on_mousedown.html
new file mode 100644
index 0000000000..860e87424a
--- /dev/null
+++ b/editor/libeditor/tests/test_doc_scrollbar_toggled_designMode_on_mousedown.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=620906
+-->
+<head>
+ <title>Test for Bug 620906</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=620906">Mozilla Bug 620906</a>
+<p id="display"></p>
+<div id="content">
+ <iframe srcdoc=
+ "<body contenteditable
+ onmousedown='
+ document.designMode=&quot;on&quot;;
+ document.designMode=&quot;off&quot;;
+ '
+ >
+ <div style='height: 1000px;'></div>
+ </body>">
+ </iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 620906 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const iframe = document.querySelector("iframe");
+ is(iframe.contentWindow.scrollY, 0, "Sanity check");
+ const rect = iframe.getBoundingClientRect();
+ const waitForTick = () => {
+ return new Promise(resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve))
+ );
+ };
+ await waitForTick();
+ let scrollEventFired = false;
+ iframe.contentWindow.addEventListener("scroll", () => {
+ scrollEventFired = true;
+ }, {once: true});
+ for (let i = 0; i < 10; i++) {
+ synthesizeMouse(iframe, rect.width - 4, rect.height / 2, { type: "mousemove" });
+ synthesizeMouse(iframe, rect.width - 5, rect.height / 2, { type: "mousemove" });
+ synthesizeMouse(iframe, rect.width - 5, rect.height / 2, {});
+ await waitForTick();
+ if (scrollEventFired) {
+ isnot(iframe.contentWindow.scrollY, 0, "The scrollbar should work");
+ SimpleTest.finish();
+ return;
+ }
+ }
+ ok(false, "The scrollbar didn't work");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
new file mode 100644
index 0000000000..60b1678b17
--- /dev/null
+++ b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
@@ -0,0 +1,1694 @@
+<html>
+<head>
+ <title>Test for input event of text editor</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <iframe id="editor1" srcdoc="<html><body contenteditable id='eventTarget'></body></html>"></iframe>
+ <iframe id="editor2" srcdoc="<html contenteditable id='eventTarget'><body></body></html>"></iframe>
+ <iframe id="editor3" srcdoc="<html><body><div contenteditable id='eventTarget'></div></body></html>"></iframe>
+ <iframe id="editor4" srcdoc="<html contenteditable id='eventTarget'><body><div contenteditable></div></body></html>"></iframe>
+ <iframe id="editor5" srcdoc="<html><body id='eventTarget'></body><script>document.designMode='on';</script></html>"></iframe>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+const kIsWin = navigator.platform.indexOf("Win") == 0;
+const kIsMac = navigator.platform.indexOf("Mac") == 0;
+
+function runTests() {
+ const kWordSelectEatSpaceToNextWord = SpecialPowers.getBoolPref("layout.word_select.eat_space_to_next_word");
+ const kImgURL =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEElEQVR42mNgaGD4D8YwBgAw9AX9Y9zBwwAAAABJRU5ErkJggg==";
+
+ function doTests(aDocument, aWindow, aDescription) {
+ aDescription += ": ";
+ aWindow.focus();
+
+ let body = aDocument.body;
+ let selection = aWindow.getSelection();
+
+ function getHTMLEditor() {
+ let editingSession = SpecialPowers.wrap(aWindow).docShell.editingSession;
+ if (!editingSession) {
+ return null;
+ }
+ let editor = editingSession.getEditorForWindow(aWindow);
+ if (!editor) {
+ return null;
+ }
+ return editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor);
+ }
+ let htmlEditor = getHTMLEditor();
+
+ let eventTarget = aDocument.getElementById("eventTarget");
+ // The event target must be focusable because it's the editing host.
+ eventTarget.focus();
+
+ let editTarget = aDocument.getElementById("editTarget");
+ if (!editTarget) {
+ editTarget = eventTarget;
+ }
+
+ // Root element never can be edit target. If the editTarget is the root
+ // element, replace with its body.
+ if (editTarget == aDocument.documentElement) {
+ editTarget = body;
+ }
+
+ editTarget.innerHTML = "";
+
+ // If the editTarget isn't its editing host, move caret to the start of it.
+ if (eventTarget != editTarget) {
+ aDocument.getSelection().collapse(editTarget, 0);
+ }
+
+ /**
+ * Tester function.
+ *
+ * @param aTestData Class like object to run a set of tests.
+ * - action:
+ * Short explanation what it does.
+ * - cancelBeforeInput:
+ * true if preventDefault() of "beforeinput" should be
+ * called.
+ * @param aFunc Function to run test.
+ * @param aExpected Object which has:
+ * - innerHTML [optional]:
+ * Set string value if the test needs to check content of
+ * editTarget.
+ * Set undefined if the test does not need to check it.
+ * - innerHTMLForCanceled [optional]:
+ * Set string value if canceling "beforeinput" does not
+ * keep the value before calling aFunc.
+ * - beforeInputEvent [optional]:
+ * Set object which has `cancelable`, `inputType`, `data` and
+ * `targetRanges` if a "beforeinput" event should be fired.
+ * If getTargetRanges() should return same array as selection when
+ * the beforeinput event is dispatched, set `targetRanges` to
+ * kSameAsSelection.
+ * Set null if "beforeinput" event shouldn't be fired.
+ * - inputEvent [optional]:
+ * Set object which has `inputType` and `data` if an "input" event
+ * should be fired if aTestData.cancelBeforeInput is not true.
+ * Set null if "input" event shouldn't be fired.
+ * Note that if expected "beforeinput" event is cancelable and
+ * aTestData.cancelBeforeInput is true, this is ignored.
+ */
+ const kSameAsSelection = "same as selection";
+ function runTest(aTestData, aFunc, aExpected) {
+ let beforeInputEvent = null;
+ let inputEvent = null;
+ let selectionRanges = [];
+ let beforeInputHandler = aEvent => {
+ if (aTestData.cancelBeforeInput) {
+ aEvent.preventDefault();
+ }
+ ok(!beforeInputEvent,
+ `${aDescription}Multiple "beforeinput" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+ ok(aEvent.isTrusted,
+ `${aDescription}"beforeinput" event at ${aTestData.action} must be trusted`);
+ is(aEvent.target, eventTarget,
+ `${aDescription}"beforeinput" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`);
+ is(aEvent.constructor.name, "InputEvent",
+ `${aDescription}"beforeinput" event at ${aTestData.action} should be dispatched with InputEvent interface`);
+ ok(aEvent.bubbles,
+ `${aDescription}"beforeinput" event at ${aTestData.action} must be bubbles`);
+ beforeInputEvent = aEvent;
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ };
+ let inputHandler = aEvent => {
+ ok(!inputEvent,
+ `${aDescription}Multiple "input" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+ ok(aEvent.isTrusted,
+ `${aDescription}"input" event at ${aTestData.action} must be trusted`);
+ is(aEvent.target, eventTarget,
+ `${aDescription}"input" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`);
+ is(aEvent.constructor.name, "InputEvent",
+ `${aDescription}"input" event at ${aTestData.action} should be dispatched with InputEvent interface`);
+ ok(!aEvent.cancelable,
+ `${aDescription}"input" event at ${aTestData.action} must not be cancelable`);
+ ok(aEvent.bubbles,
+ `${aDescription}"input" event at ${aTestData.action} must be bubbles`);
+ let duration = Math.abs(window.performance.now() - aEvent.timeStamp);
+ ok(duration < 30 * 1000,
+ `${aDescription}perhaps, timestamp wasn't set correctly :${aEvent.timeStamp} (expected it to be within 30s of ` +
+ `the current time but it differed by ${duration}ms)`);
+ inputEvent = aEvent;
+ };
+
+ try {
+ aWindow.addEventListener("beforeinput", beforeInputHandler, true);
+ aWindow.addEventListener("input", inputHandler, true);
+
+ let initialValue = editTarget.innerHTML;
+
+ aFunc();
+
+ (function verify() {
+ try {
+ function checkTargetRanges(aEvent, aTargetRanges) {
+ let targetRanges = aEvent.getTargetRanges();
+ if (aTargetRanges.length === 0) {
+ is(targetRanges.length, 0,
+ `${aDescription}getTargetRanges() of "${aEvent.type}" event for ${aTestData.action} should return empty array`);
+ return;
+ }
+ is(targetRanges.length, aTargetRanges.length,
+ `${aDescription}getTargetRanges() of "${aEvent.type}" event for ${aTestData.action} should return array of static range`);
+ if (targetRanges.length !== aTargetRanges.length) {
+ return;
+ }
+ for (let i = 0; i < aTargetRanges.length; i++) {
+ is(targetRanges[i].startContainer, aTargetRanges[i].startContainer,
+ `${aDescription}startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event for ${aTestData.action} does not match`);
+ is(targetRanges[i].startOffset, aTargetRanges[i].startOffset,
+ `${aDescription}startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event for ${aTestData.action} does not match`);
+ is(targetRanges[i].endContainer, aTargetRanges[i].endContainer,
+ `${aDescription}endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event for ${aTestData.action} does not match`);
+ is(targetRanges[i].endOffset, aTargetRanges[i].endOffset,
+ `${aDescription}endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event for ${aTestData.action} does not match`);
+ }
+ }
+
+ if (aExpected.innerHTML !== undefined) {
+ if (aTestData.cancelBeforeInput && aExpected.innerHTMLForCanceled === undefined) {
+ is(editTarget.innerHTML, initialValue,
+ `${aDescription}innerHTML should be "${initialValue}" after ${aTestData.action}`);
+ } else {
+ let expectedValue =
+ aTestData.cancelBeforeInput ? aExpected.innerHTMLForCanceled : aExpected.innerHTML;
+ is(editTarget.innerHTML, expectedValue,
+ `${aDescription}innerHTML should be "${expectedValue}" after ${aTestData.action}`);
+ }
+ }
+ if (aExpected.beforeInputEvent === null || aExpected.beforeInputEvent === undefined) {
+ ok(!beforeInputEvent,
+ `${aDescription}"beforeinput" event shouldn't have been fired at ${aTestData.action}`);
+ } else {
+ ok(beforeInputEvent,
+ `${aDescription}"beforeinput" event should've been fired at ${aTestData.action}`);
+ is(beforeInputEvent.cancelable, aExpected.beforeInputEvent.cancelable,
+ `${aDescription}"beforeinput" event by ${aTestData.action} should be ${
+ aExpected.beforeInputEvent.cancelable ? "cancelable" : "not cancelable"
+ }`);
+ is(beforeInputEvent.inputType, aExpected.beforeInputEvent.inputType,
+ `${aDescription}inputType of "beforeinput" event by ${aTestData.action} should be "${aExpected.beforeInputEvent.inputType}"`);
+ is(beforeInputEvent.data, aExpected.beforeInputEvent.data,
+ `${aDescription}data of "beforeinput" event by ${aTestData.action} should be ${
+ aExpected.beforeInputEvent.data === null ? "null" : `"${aExpected.beforeInputEvent.data}"`
+ }`);
+ is(beforeInputEvent.dataTransfer, null,
+ `${aDescription}dataTransfer of "beforeinput" event by ${aTestData.action} should be null`);
+ checkTargetRanges(
+ beforeInputEvent,
+ aExpected.beforeInputEvent.targetRanges === kSameAsSelection
+ ? selectionRanges
+ : aExpected.beforeInputEvent.targetRanges
+ );
+ }
+ if ((
+ aTestData.cancelBeforeInput === true &&
+ aExpected.beforeInputEvent &&
+ aExpected.beforeInputEvent.cancelable
+ ) || aExpected.inputEvent === null || aExpected.inputEvent === undefined) {
+ ok(!inputEvent,
+ `${aDescription}"input" event shouldn't have been fired at ${aTestData.action}`);
+ } else {
+ ok(inputEvent,
+ `${aDescription}"input" event should've been fired at ${aTestData.action}`);
+ is(inputEvent.cancelable, false,
+ `${aDescription}"input" event by ${aTestData.action} should be not be cancelable`);
+ is(inputEvent.inputType, aExpected.inputEvent.inputType,
+ `${aDescription}inputType of "input" event by ${aTestData.action} should be "${aExpected.inputEvent.inputType}"`);
+ is(inputEvent.data, aExpected.inputEvent.data,
+ `${aDescription}data of "input" event by ${aTestData.action} should be ${
+ aExpected.inputEvent.data === null ? "null" : `"${aExpected.inputEvent.data}"`
+ }`);
+ is(inputEvent.dataTransfer, null,
+ `${aDescription}dataTransfer of "input" event by ${aTestData.action} should be null`);
+ is(inputEvent.getTargetRanges().length, 0,
+ `${aDescription}getTargetRanges() of "input" event by ${aTestData.action} should return empty array`);
+ }
+ } catch (ex) {
+ ok(false, `${aDescription}unexpected exception at verifying test result of "${aTestData.action}": ${ex.toString()}`);
+ }
+ })();
+ } finally {
+ aWindow.removeEventListener("beforeinput", beforeInputHandler, true);
+ aWindow.removeEventListener("input", inputHandler, true);
+ }
+ }
+
+ (function test_typing_a_in_empty_editor(aTestData) {
+ editTarget.innerHTML = "";
+ editTarget.focus();
+
+ runTest(aTestData,
+ () => {
+ synthesizeKey("a", {}, aWindow);
+ },
+ {
+ innerHTML: "a",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertText",
+ data: "a",
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "insertText",
+ data: "a",
+ },
+ }
+ );
+ })({
+ action: 'typing "a" in empty editor',
+ });
+
+ function test_typing_b_at_end_of_editor(aTestData) {
+ editTarget.innerHTML = "a";
+ selection.collapse(editTarget.firstChild, 1);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("b", {}, aWindow);
+ },
+ {
+ innerHTML: "ab",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertText",
+ data: "b",
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "insertText",
+ data: "b",
+ },
+ }
+ );
+ }
+ test_typing_b_at_end_of_editor({
+ action: 'typing "b" after "a" and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_b_at_end_of_editor({
+ action: 'typing "b" after "a"',
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_backspace_to_delete_last_character(aTestData) {
+ editTarget.innerHTML = "a";
+ let textNode = editTarget.firstChild;
+ selection.collapse(textNode, 1);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Backspace", {}, aWindow);
+ },
+ {
+ innerHTML: "<br>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentBackward",
+ data: null,
+ targetRanges: [
+ {
+ startContainer: textNode,
+ startOffset: 0,
+ endContainer: textNode,
+ endOffset: 1,
+ },
+ ],
+ },
+ inputEvent: {
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_backspace_to_delete_last_character({
+ action: 'typing "Backspace" to delete the last character and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_backspace_to_delete_last_character({
+ action: 'typing "Backspace" to delete the last character',
+ cancelBeforeInput: false,
+ });
+
+ (function test_typing_backspace_in_empty_editor(aTestData) {
+ editTarget.innerHTML = "";
+ editTarget.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Backspace", {}, aWindow);
+ },
+ {
+ innerHTML: editTarget.tagName === "DIV" ? "" : "<br>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentBackward",
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ }
+ );
+ })({
+ action: 'typing "Backspace" in empty editor',
+ });
+
+ function test_typing_enter_at_end_of_editor(aTestData) {
+ editTarget.innerHTML = "B";
+ selection.collapse(editTarget.firstChild, 1);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Enter", {}, aWindow);
+ },
+ {
+ innerHTML: "<div>B</div><div><br></div>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertParagraph",
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "insertParagraph",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_enter_at_end_of_editor({
+ action: 'typing "Enter" at end of editor and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_enter_at_end_of_editor({
+ action: 'typing "Enter" at end of editor',
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_C_in_empty_last_line(aTestData) {
+ editTarget.innerHTML = "<div>B</div><div><br></div>";
+ selection.collapse(editTarget.querySelector("div + div"), 0);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("C", {shiftKey: true}, aWindow);
+ },
+ {
+ innerHTML: "<div>B</div><div>C<br></div>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertText",
+ data: "C",
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "insertText",
+ data: "C",
+ },
+ }
+ );
+ }
+ test_typing_C_in_empty_last_line({
+ action: 'typing "C" in empty last line and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_C_in_empty_last_line({
+ action: 'typing "C" in empty last line',
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_enter_in_non_empty_last_line(aTestData) {
+ editTarget.innerHTML = "<div>B</div><div>C<br></div>";
+ selection.collapse(editTarget.querySelector("div + div").firstChild, 1);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Enter", {}, aWindow);
+ },
+ {
+ innerHTML: "<div>B</div><div>C</div><div><br></div>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertParagraph",
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "insertParagraph",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_enter_in_non_empty_last_line({
+ action: 'typing "Enter" at end of non-empty line and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_enter_in_non_empty_last_line({
+ action: 'typing "Enter" at end of non-empty line',
+ cancelBeforeInput: false,
+ });
+
+ (function test_setting_innerHTML(aTestData) {
+ editTarget.innerHTML = "";
+ editTarget.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ editTarget.innerHTML = "foo-bar";
+ },
+ { innerHTML: "foo-bar" }
+ );
+ })({
+ action: "setting innerHTML to non-empty value",
+ });
+
+ (function test_setting_innerHTML_to_empty(aTestData) {
+ editTarget.innerHTML = "foo-bar";
+ editTarget.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ editTarget.innerHTML = "";
+ },
+ { innerHTML: editTarget.tagName === "DIV" ? "" : "<br>"}
+ );
+ })({
+ action: "setting innerHTML to empty value",
+ });
+
+ function test_typing_white_space_in_empty_editor(aTestData) {
+ editTarget.innerHTML = "";
+ editTarget.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey(" ", {}, aWindow);
+ },
+ {
+ innerHTML: "&nbsp;",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertText",
+ data: " ",
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "insertText",
+ data: " ",
+ },
+ }
+ );
+ }
+ test_typing_white_space_in_empty_editor({
+ action: 'typing space in empty editor and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_white_space_in_empty_editor({
+ action: "typing space in empty editor",
+ cancelBeforeInput: false,
+ });
+
+ (function test_typing_delete_at_end_of_editor(aTestData) {
+ editTarget.innerHTML = "&nbsp;";
+ selection.collapse(editTarget.firstChild, 1);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Delete", {}, aWindow);
+ },
+ {
+ innerHTML: "&nbsp;",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentForward",
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ }
+ );
+ })({
+ action: 'typing "Delete" at end of editor',
+ });
+
+ (function test_typing_arrow_left_to_move_caret(aTestData) {
+ editTarget.innerHTML = "&nbsp;";
+ selection.collapse(editTarget.firstChild, 1);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_ArrowLeft", {}, aWindow);
+ },
+ { innerHTML: "&nbsp;" }
+ );
+ })({
+ action: 'typing "ArrowLeft" to move caret',
+ });
+
+ function test_typing_delete_to_delete_last_character(aTestData) {
+ editTarget.innerHTML = "\u00A0";
+ let textNode = editTarget.firstChild;
+ selection.collapse(textNode, 0);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Delete", {}, aWindow);
+ },
+ {
+ innerHTML: "<br>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentForward",
+ data: null,
+ targetRanges: [
+ {
+ startContainer: textNode,
+ startOffset: 0,
+ endContainer: textNode,
+ endOffset: 1,
+ },
+ ],
+ },
+ inputEvent: {
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_delete_to_delete_last_character({
+ action: 'typing "Delete" to delete last character (NBSP) and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_delete_to_delete_last_character({
+ action: 'typing "Delete" to delete last character (NBSP)',
+ cancelBeforeInput: false,
+ });
+
+ function test_undoing_deleting_last_character(aTestData) {
+ editTarget.innerHTML = "\u00A0";
+ selection.collapse(editTarget.firstChild, 0);
+ synthesizeKey("KEY_Delete", {}, aWindow);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("z", {accelKey: true}, aWindow);
+ },
+ {
+ innerHTML: "&nbsp;",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "historyUndo",
+ data: null,
+ targetRanges: [],
+ },
+ inputEvent: {
+ inputType: "historyUndo",
+ data: null,
+ },
+ }
+ );
+ }
+ test_undoing_deleting_last_character({
+ action: 'undoing deleting last character and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_undoing_deleting_last_character({
+ action: 'undoing deleting last character',
+ cancelBeforeInput: false,
+ });
+
+ (function test_undoing_without_undoable_transaction(aTestData) {
+ htmlEditor.enableUndo(false);
+ htmlEditor.enableUndo(true);
+ editTarget.innerHTML = "\u00A0";
+ selection.collapse(editTarget.firstChild, 0);
+ synthesizeKey("KEY_Delete", {}, aWindow);
+ synthesizeKey("z", {accelKey: true}, aWindow);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("z", {accelKey: true}, aWindow);
+ },
+ { innerHTML: "&nbsp;" }
+ );
+ })({
+ action: "trying to undo without undoable transaction",
+ });
+
+ function test_redoing_deleting_last_character(aTestData) {
+ htmlEditor.enableUndo(false);
+ htmlEditor.enableUndo(true);
+ editTarget.innerHTML = "\u00A0";
+ selection.collapse(editTarget.firstChild, 0);
+ synthesizeKey("KEY_Delete", {}, aWindow);
+ synthesizeKey("z", {accelKey: true}, aWindow);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("z", {accelKey: true, shiftKey: true}, aWindow);
+ },
+ {
+ innerHTML: "<br>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "historyRedo",
+ data: null,
+ targetRanges: [],
+ },
+ inputEvent: {
+ inputType: "historyRedo",
+ data: null,
+ },
+ }
+ );
+ }
+ test_redoing_deleting_last_character({
+ action: 'redoing deleting last character and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_redoing_deleting_last_character({
+ action: 'redoing deleting last character',
+ cancelBeforeInput: false,
+ });
+
+ (function test_redoing_without_redoable_transaction(aTestData) {
+ htmlEditor.enableUndo(false);
+ htmlEditor.enableUndo(true);
+ editTarget.innerHTML = "\u00A0";
+ selection.collapse(editTarget.firstChild, 0);
+ synthesizeKey("KEY_Delete", {}, aWindow);
+ synthesizeKey("z", {accelKey: true}, aWindow);
+ synthesizeKey("z", {accelKey: true, shiftKey: true}, aWindow);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("z", {accelKey: true, shiftKey: true}, aWindow);
+ },
+ { innerHTML: "<br>" }
+ );
+ })({
+ action: "trying to redo without redoable transaction",
+ });
+
+ function test_inserting_linebreak(aTestData) {
+ editTarget.innerHTML = "<br>";
+ selection.collapse(editTarget, 0);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Enter", {shiftKey: true}, aWindow);
+ },
+ {
+ innerHTML: "<br><br>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertLineBreak",
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "insertLineBreak",
+ data: null,
+ },
+ }
+ );
+ }
+ test_inserting_linebreak({
+ action: 'inserting a linebreak and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_inserting_linebreak({
+ action: "inserting a linebreak",
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_backspace_to_delete_selected_characters(aTestData) {
+ editTarget.innerHTML = "a";
+ selection.selectAllChildren(editTarget);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: editTarget.firstChild,
+ startOffset: 0,
+ endContainer: editTarget.firstChild,
+ endOffset: editTarget.firstChild.length,
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Backspace", {}, aWindow);
+ },
+ {
+ innerHTML: "<br>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentBackward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_backspace_to_delete_selected_characters({
+ action: 'typing "Backspace" to delete selected characters and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_backspace_to_delete_selected_characters({
+ action: 'typing "Backspace" to delete selected characters',
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_delete_to_delete_selected_characters(aTestData) {
+ editTarget.innerHTML = "a";
+ selection.selectAllChildren(editTarget);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: editTarget.firstChild,
+ startOffset: 0,
+ endContainer: editTarget.firstChild,
+ endOffset: editTarget.firstChild.length,
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Delete", {}, aWindow);
+ },
+ {
+ innerHTML: "<br>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentForward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_delete_to_delete_selected_characters({
+ action: 'typing "Delete" to delete selected characters and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_delete_to_delete_selected_characters({
+ action: 'typing "Delete" to delete selected characters',
+ cancelBeforeInput: false,
+ });
+
+ function test_deleting_word_backward_from_its_end(aTestData) {
+ editTarget.innerHTML = "abc def";
+ let textNode = editTarget.firstChild;
+ selection.collapse(textNode, "abc def".length);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: textNode,
+ startOffset: "abc ".length,
+ endContainer: textNode,
+ endOffset: textNode.length,
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward");
+ },
+ {
+ innerHTML: "abc ",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteWordBackward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: "deleteWordBackward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_deleting_word_backward_from_its_end({
+ action: 'deleting word backward from its end and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_deleting_word_backward_from_its_end({
+ action: "deleting word backward from its end",
+ cancelBeforeInput: false,
+ });
+
+ function test_deleting_word_forward_from_its_start(aTestData) {
+ editTarget.innerHTML = "abc def";
+ let textNode = editTarget.firstChild;
+ selection.collapse(textNode, 0);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: textNode,
+ startOffset: 0,
+ endContainer: textNode,
+ endOffset: kWordSelectEatSpaceToNextWord ? "abc ".length : "abc".length,
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward");
+ },
+ {
+ innerHTML: kWordSelectEatSpaceToNextWord ? "def" : " def",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteWordForward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: "deleteWordForward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_deleting_word_forward_from_its_start({
+ action: 'deleting word forward from its start and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_deleting_word_forward_from_its_start({
+ action: "deleting word forward from its start",
+ cancelBeforeInput: false,
+ });
+
+ (function test_deleting_word_backward_from_middle_of_second_word(aTestData) {
+ editTarget.innerHTML = "abc def";
+ let textNode = editTarget.firstChild;
+ selection.setBaseAndExtent(textNode, "abc d".length, textNode, "abc de".length);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: textNode,
+ startOffset: kIsWin ? "abc ".length : "abc d".length,
+ endContainer: textNode,
+ endOffset: kIsWin ? "abc d".length : "abc de".length,
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward");
+ },
+ {
+ // Only on Windows, we collapse selection to start before handling this command.
+ innerHTML: kIsWin ? "abc ef" : "abc df",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: kIsWin ? "deleteWordBackward" : "deleteContentBackward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: kIsWin ? "deleteWordBackward" : "deleteContentBackward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters backward from middle of second word",
+ });
+
+ (function test_deleting_word_forward_from_middle_of_first_word(aTestData) {
+ editTarget.innerHTML = "abc def";
+ let textNode = editTarget.firstChild;
+ selection.setBaseAndExtent(textNode, "a".length, textNode, "ab".length);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: textNode,
+ startOffset: "a".length,
+ endContainer: textNode,
+ endOffset: (function expectedEndOffset() {
+ if (!kIsWin) {
+ return "ab".length;
+ }
+ return kWordSelectEatSpaceToNextWord ? "abc ".length : "abc".length;
+ })(),
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward");
+ },
+ {
+ // Only on Windows, we collapse selection to start before handling this command.
+ innerHTML: (function () {
+ if (!kIsWin) {
+ return "ac def";
+ }
+ return kWordSelectEatSpaceToNextWord ? "adef" : "a def";
+ })(),
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: kIsWin ? "deleteWordForward" : "deleteContentForward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: kIsWin ? "deleteWordForward" : "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters forward from middle of first word",
+ });
+
+ (function test_deleting_characters_backward_to_start_of_line(aTestData) {
+ editTarget.innerHTML = "abc def";
+ let textNode = editTarget.firstChild;
+ selection.collapse(textNode, "abc d".length);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: textNode,
+ startOffset: 0,
+ endContainer: textNode,
+ endOffset: "abc d".length,
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_deleteToBeginningOfLine");
+ },
+ {
+ innerHTML: "ef",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteSoftLineBackward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: "deleteSoftLineBackward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters backward to start of line",
+ });
+
+ (function test_deleting_characters_forward_to_end_of_line(aTestData) {
+ editTarget.innerHTML = "abc def";
+ let textNode = editTarget.firstChild;
+ selection.collapse(textNode, "ab".length);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: textNode,
+ startOffset: "ab".length,
+ endContainer: textNode,
+ endOffset: "abc def".length,
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_deleteToEndOfLine");
+ },
+ {
+ innerHTML: "ab",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteSoftLineForward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: "deleteSoftLineForward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters forward to end of line",
+ });
+
+ (function test_deleting_characters_backward_to_start_of_line_with_non_collapsed_selection(aTestData) {
+ editTarget.innerHTML = "abc def";
+ let textNode = editTarget.firstChild;
+ selection.setBaseAndExtent(textNode, "abc d".length, textNode, "abc_de".length);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: textNode,
+ startOffset: kIsWin ? 0 : "abc d".length,
+ endContainer: textNode,
+ endOffset: kIsWin ? "abc d".length : "abc de".length,
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_deleteToBeginningOfLine");
+ },
+ {
+ // Only on Windows, we collapse selection to start before handling this command.
+ innerHTML: kIsWin ? "ef" : "abc df",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: kIsWin ? "deleteSoftLineBackward" : "deleteContentBackward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: kIsWin ? "deleteSoftLineBackward" : "deleteContentBackward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters backward to start of line (with selection in second word)",
+ });
+
+ (function test_deleting_characters_forward_to_end_of_line_with_non_collapsed_selection(aTestData) {
+ editTarget.innerHTML = "abc def";
+ let textNode = editTarget.firstChild;
+ selection.setBaseAndExtent(textNode, "a".length, textNode, "ab".length);
+
+ let expectedTargetRanges = [
+ {
+ startContainer: textNode,
+ startOffset: "a".length,
+ endContainer: textNode,
+ endOffset: kIsWin ? "abc def".length : "ab".length,
+ },
+ ];
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_deleteToEndOfLine");
+ },
+ {
+ // Only on Windows, we collapse selection to start before handling this command.
+ innerHTML: kIsWin ? "a" : "ac def",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: kIsWin ? "deleteSoftLineForward" : "deleteContentForward",
+ data: null,
+ targetRanges: expectedTargetRanges,
+ },
+ inputEvent: {
+ inputType: kIsWin ? "deleteSoftLineForward" : "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters forward to end of line (with selection in second word)",
+ });
+
+ function test_switching_text_direction_from_default(aTestData) {
+ const editingHost = aDocument.getElementById("editTarget") || aDocument.getElementById("eventTarget");
+ try {
+ editingHost.removeAttribute("dir");
+ htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorRightToLeft;
+ htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorLeftToRight; // XXX flags update is required, must be a bug.
+ aDocument.documentElement.scrollTop; // XXX Update the body frame
+ editTarget.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_switchTextDirection");
+ if (aTestData.cancelBeforeInput) {
+ is(editingHost.getAttribute("dir"), null,
+ `${aDescription}dir attribute of the element shouldn't have been set by ${aTestData.action}`);
+ } else {
+ is(editingHost.getAttribute("dir"), "rtl",
+ `${aDescription}dir attribute of the element should've been set to "rtl" by ${aTestData.action}`);
+ }
+ },
+ {
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "formatSetBlockTextDirection",
+ data: "rtl",
+ targetRanges: [],
+ },
+ inputEvent: {
+ inputType: "formatSetBlockTextDirection",
+ data: "rtl",
+ },
+ }
+ );
+ } finally {
+ editingHost.removeAttribute("dir");
+ htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorRightToLeft;
+ htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorLeftToRight; // XXX flags update is required, must be a bug.
+ aDocument.documentElement.scrollTop; // XXX Update the body frame
+ }
+ }
+ test_switching_text_direction_from_default({
+ action: 'switching text direction from default to "rtl" and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_switching_text_direction_from_default({
+ action: 'switching text direction from default to "rtl"',
+ cancelBeforeInput: false,
+ });
+
+ function test_switching_text_direction_from_rtl_to_ltr(aTestData) {
+ const editingHost = aDocument.getElementById("editTarget") || aDocument.getElementById("eventTarget");
+ try {
+ editingHost.setAttribute("dir", "rtl");
+ htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorLeftToRight;
+ htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorRightToLeft; // XXX flags update is required, must be a bug.
+ aDocument.documentElement.scrollTop; // XXX Update the body frame
+ editTarget.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_switchTextDirection");
+ let expectedDirValue = aTestData.cancelBeforeInput ? "rtl" : "ltr";
+ is(editingHost.getAttribute("dir"), expectedDirValue,
+ `${aDescription}dir attribute of the element should be "${expectedDirValue}" after ${aTestData.action}`);
+ },
+ {
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "formatSetBlockTextDirection",
+ data: "ltr",
+ targetRanges: [],
+ },
+ inputEvent: {
+ inputType: "formatSetBlockTextDirection",
+ data: "ltr",
+ },
+ }
+ );
+ } finally {
+ editingHost.removeAttribute("dir");
+ htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorRightToLeft;
+ htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorLeftToRight; // XXX flags update is required, must be a bug.
+ aDocument.documentElement.scrollTop; // XXX Update the body frame
+ }
+ }
+ test_switching_text_direction_from_rtl_to_ltr({
+ action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_switching_text_direction_from_rtl_to_ltr({
+ action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"',
+ cancelBeforeInput: false,
+ });
+
+
+ function test_inserting_link(aTestData) {
+ editTarget.innerHTML = "link";
+ selection.selectAllChildren(editTarget);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_insertLinkNoUI", "https://example.com/foo/bar.html");
+ },
+ {
+ innerHTML: '<a href="https://example.com/foo/bar.html">link</a>',
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertLink",
+ data: "https://example.com/foo/bar.html",
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "insertLink",
+ data: "https://example.com/foo/bar.html",
+ },
+ }
+ );
+ }
+ test_inserting_link({
+ action: 'setting link with absolute URL and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_inserting_link({
+ action: "setting link with absolute URL",
+ cancelBeforeInput: false,
+ });
+
+ (function test_inserting_link_with_relative_url(aTestData) {
+ editTarget.innerHTML = "link";
+ selection.selectAllChildren(editTarget);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_insertLinkNoUI", "foo/bar.html");
+ },
+ {
+ innerHTML: '<a href="foo/bar.html">link</a>',
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertLink",
+ data: "foo/bar.html",
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "insertLink",
+ data: "foo/bar.html",
+ },
+ }
+ );
+ })({
+ action: "setting link with relative URL",
+ });
+
+ (function test_format_commands() {
+ for (let test of [{command: "cmd_bold",
+ tag: "b",
+ otherRemoveTags: ["strong"],
+ inputType: "formatBold"},
+ {command: "cmd_italic",
+ tag: "i",
+ otherRemoveTags: ["em"],
+ inputType: "formatItalic"},
+ {command: "cmd_underline",
+ tag: "u",
+ inputType: "formatUnderline"},
+ {command: "cmd_strikethrough",
+ tag: "strike",
+ otherRemoveTags: ["s"],
+ inputType: "formatStrikeThrough"},
+ {command: "cmd_subscript",
+ tag: "sub",
+ exclusiveTags: ["sup"],
+ inputType: "formatSubscript"},
+ {command: "cmd_superscript",
+ tag: "sup",
+ exclusiveTags: ["sub"],
+ inputType: "formatSuperscript"}]) {
+ function test_formatting_text(aTestData) {
+ editTarget.innerHTML = "format";
+ selection.selectAllChildren(editTarget);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, test.command);
+ },
+ {
+ innerHTML: `<${test.tag}>format</${test.tag}>`,
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: test.inputType,
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: test.inputType,
+ data: null,
+ },
+ }
+ );
+ }
+ test_formatting_text({
+ action: `formatting with "${test.command}" and canceling "beforeinput"`,
+ cancelBeforeInput: true,
+ });
+ test_formatting_text({
+ action: `formatting with "${test.command}" and canceling "beforeinput"`,
+ cancelBeforeInput: false,
+ });
+
+ function test_removing_format_text(aTestData) {
+ editTarget.innerHTML = `<${test.tag}>format</${test.tag}>`;
+ selection.selectAllChildren(editTarget);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, test.command);
+ },
+ {
+ innerHTML: "format",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: test.inputType,
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: test.inputType,
+ data: null,
+ },
+ }
+ );
+ }
+ test_removing_format_text({
+ action: `removing format with "${test.command}" and canceling "beforeinput"`,
+ cancelBeforeInput: true,
+ });
+ test_removing_format_text({
+ action: `removing format with "${test.command}" and canceling "beforeinput"`,
+ cancelBeforeInput: false,
+ });
+
+ (function test_removing_format_styled_by_others() {
+ if (!test.otherRemoveTags) {
+ return;
+ }
+ for (let anotherTag of test.otherRemoveTags) {
+ function test_removing_format_styled_by_another_element(aTestData) {
+ editTarget.innerHTML = `<${anotherTag}>format</${anotherTag}>`;
+ selection.selectAllChildren(editTarget);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, test.command);
+ },
+ {
+ innerHTML: "format",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: test.inputType,
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: test.inputType,
+ data: null,
+ },
+ }
+ );
+ }
+ test_removing_format_styled_by_another_element({
+ action: `removing <${anotherTag}> element with "${test.command}" and canceling "beforeinput"`,
+ cancelBeforeInput: true,
+ });
+ test_removing_format_styled_by_another_element({
+ action: `removing <${anotherTag}> element with "${test.command}"`,
+ cancelBeforeInput: false,
+ });
+
+ function test_removing_format_styled_by_both_primary_one_and_another_one(aTestData) {
+ editTarget.innerHTML = `<${test.tag}><${anotherTag}>format</${anotherTag}></${test.tag}>`;
+ selection.selectAllChildren(editTarget);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, test.command);
+ },
+ {
+ innerHTML: "format",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: test.inputType,
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: test.inputType,
+ data: null,
+ },
+ }
+ );
+ }
+ test_removing_format_styled_by_both_primary_one_and_another_one({
+ action: `removing both <${test.tag}> and <${anotherTag}> elements with "${test.command}" and canceling "beforeinput"`,
+ cancelBeforeInput: true,
+ });
+ test_removing_format_styled_by_both_primary_one_and_another_one({
+ action: `removing both <${test.tag}> and <${anotherTag}> elements with "${test.command}"`,
+ cancelBeforeInput: false,
+ });
+ }
+ })();
+ (function test_formatting_text_styled_by_exclusive_elements() {
+ if (!test.exclusiveTags) {
+ return;
+ }
+ for (let exclusiveTag of test.exclusiveTags) {
+ function test_formatting_text_styled_by_exclusive_element(aTestData) {
+ editTarget.innerHTML = `<${exclusiveTag}>format</${exclusiveTag}>`;
+ selection.selectAllChildren(editTarget);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, test.command);
+ },
+ {
+ innerHTML: `<${test.tag}>format</${test.tag}>`,
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: test.inputType,
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: test.inputType,
+ data: null,
+ },
+ }
+ );
+ }
+ test_formatting_text_styled_by_exclusive_element({
+ action: `removing <${exclusiveTag}> element with formatting with "${test.command}" and canceling "beforeinput"`,
+ cancelBeforeInput: true,
+ });
+ test_formatting_text_styled_by_exclusive_element({
+ action: `removing <${exclusiveTag}> element with formatting with "${test.command}"`,
+ cancelBeforeInput: false,
+ });
+ }
+ })();
+ }
+ })();
+
+ function test_indenting_text(aTestData) {
+ editTarget.innerHTML = "format";
+ selection.selectAllChildren(editTarget);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_indent");
+ },
+ {
+ innerHTML: "<blockquote>format</blockquote>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "formatIndent",
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "formatIndent",
+ data: null,
+ },
+ }
+ );
+ }
+ test_indenting_text({
+ action: 'indenting text and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_indenting_text({
+ action: 'indenting text',
+ cancelBeforeInput: false,
+ });
+
+ function test_outdenting_blockquote(aTestData) {
+ editTarget.innerHTML = "<blockquote>format</blockquote>";
+ selection.selectAllChildren(editTarget.firstChild);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(aWindow, "cmd_outdent");
+ },
+ {
+ innerHTML: "format",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "formatOutdent",
+ data: null,
+ targetRanges: kSameAsSelection,
+ },
+ inputEvent: {
+ inputType: "formatOutdent",
+ data: null,
+ },
+ }
+ );
+ }
+ test_outdenting_blockquote({
+ action: 'outdenting blockquote and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_outdenting_blockquote({
+ action: 'outdenting blockquote',
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_delete_to_delete_img(aTestData) {
+ editTarget.innerHTML = `<img src="${kImgURL}">`;
+ selection.collapse(editTarget, 0);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Delete", {}, aWindow);
+ },
+ {
+ innerHTML: "<br>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentForward",
+ data: null,
+ targetRanges: [
+ {
+ startContainer: editTarget,
+ startOffset: 0,
+ endContainer: editTarget,
+ endOffset: 1,
+ },
+ ],
+ },
+ inputEvent: {
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_delete_to_delete_img({
+ action: 'typing "Delete" to delete the <img> element and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_delete_to_delete_img({
+ action: 'typing "Delete" to delete the <img> element',
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_backspace_to_delete_img(aTestData) {
+ editTarget.innerHTML = `<img src="${kImgURL}">`;
+ selection.collapse(editTarget, 1);
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Backspace", {}, aWindow);
+ },
+ {
+ innerHTML: "<br>",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentBackward",
+ data: null,
+ targetRanges: [
+ {
+ startContainer: editTarget,
+ startOffset: 0,
+ endContainer: editTarget,
+ endOffset: 1,
+ },
+ ],
+ },
+ inputEvent: {
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_backspace_to_delete_img({
+ action: 'typing "Backspace" to delete the <img> element and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_backspace_to_delete_img({
+ action: 'typing "Backspace" to delete the <img> element',
+ cancelBeforeInput: false,
+ });
+ }
+
+ doTests(document.getElementById("editor1").contentDocument,
+ document.getElementById("editor1").contentWindow,
+ "Editor1, body has contenteditable attribute");
+ doTests(document.getElementById("editor2").contentDocument,
+ document.getElementById("editor2").contentWindow,
+ "Editor2, html has contenteditable attribute");
+ doTests(document.getElementById("editor3").contentDocument,
+ document.getElementById("editor3").contentWindow,
+ "Editor3, div has contenteditable attribute");
+ doTests(document.getElementById("editor4").contentDocument,
+ document.getElementById("editor4").contentWindow,
+ "Editor4, html and div have contenteditable attribute");
+ doTests(document.getElementById("editor5").contentDocument,
+ document.getElementById("editor5").contentWindow,
+ "Editor5, html and div have contenteditable attribute");
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_dom_input_event_on_texteditor.html b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
new file mode 100644
index 0000000000..e3d9eb3e5c
--- /dev/null
+++ b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
@@ -0,0 +1,926 @@
+<html>
+<head>
+ <title>Test for input event of text editor</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <input type="text" id="input">
+ <textarea id="textarea"></textarea>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(0, 1); // In a11y module
+SimpleTest.waitForFocus(runTests, window);
+
+function runTests() {
+ const kWordSelectEatSpaceToNextWord = SpecialPowers.getBoolPref("layout.word_select.eat_space_to_next_word");
+
+ function doTests(aElement, aDescription, aIsTextarea) {
+ aDescription += ": ";
+ aElement.focus();
+ aElement.value = "";
+
+ /**
+ * Tester function.
+ *
+ * @param aTestData Class like object to run a set of tests.
+ * - action:
+ * Short explanation what it does.
+ * - cancelBeforeInput:
+ * true if preventDefault() of "beforeinput" should be
+ * called.
+ * @param aFunc Function to run test.
+ * @param aExpected Object which has:
+ * - value [optional]:
+ * Set string value if the test needs to check value of
+ * aElement.
+ * Set undefined if the test does not need to check it.
+ * - valueForCanceled [optional]:
+ * Set string value if canceling "beforeinput" does not
+ * keep the value before calling aFunc.
+ * - beforeInputEvent [optional]:
+ * Set object which has `cancelable`, `inputType` and `data`
+ * if a "beforeinput" event should be fired.
+ * Set null if "beforeinput" event shouldn't be fired.
+ * - inputEvent [optional]:
+ * Set object which has `inputType` and `data` if an "input" event
+ * should be fired if aTestData.cancelBeforeInput is not true.
+ * Set null if "input" event shouldn't be fired.
+ * Note that if expected "beforeinput" event is cancelable and
+ * aTestData.cancelBeforeInput is true, this is ignored.
+ */
+ function runTest(aTestData, aFunc, aExpected) {
+ let initializing = false;
+ let beforeInputEvent = null;
+ let inputEvent = null;
+ let beforeInputHandler = (aEvent) => {
+ if (initializing) {
+ return;
+ }
+ ok(!beforeInputEvent,
+ `${aDescription}Multiple "beforeinput" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+ if (aTestData.cancelBeforeInput) {
+ aEvent.preventDefault();
+ }
+ ok(aEvent.isTrusted,
+ `${aDescription}"beforeinput" event at ${aTestData.action} must be trusted`);
+ is(aEvent.target, aElement,
+ `${aDescription}"beforeinput" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`);
+ ok(aEvent instanceof InputEvent,
+ `${aDescription}"beforeinput" event at ${aTestData.action} should be dispatched with InputEvent interface`);
+ ok(aEvent.bubbles,
+ `${aDescription}"beforeinput" event at ${aTestData.action} must be bubbles`);
+ beforeInputEvent = aEvent;
+ };
+ let inputHandler = (aEvent) => {
+ if (initializing) {
+ return;
+ }
+ ok(!inputEvent,
+ `${aDescription}Multiple "input" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
+ ok(aEvent.isTrusted,
+ `${aDescription}"input" event at ${aTestData.action} must be trusted`);
+ is(aEvent.target, aElement, `"input" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`);
+ ok(aEvent instanceof InputEvent,
+ `${aDescription}"input" event at ${aTestData.action} should be dispatched with InputEvent interface`);
+ ok(!aEvent.cancelable,
+ `${aDescription}"input" event at ${aTestData.action} must not be cancelable`);
+ ok(aEvent.bubbles,
+ `${aDescription}"input" event at ${aTestData.action} must be bubbles`);
+ let duration = Math.abs(window.performance.now() - aEvent.timeStamp);
+ ok(duration < 30 * 1000,
+ `${aDescription}perhaps, timestamp wasn't set correctly :${aEvent.timeStamp} (expected it to be within 30s of ` +
+ `the current time but it differed by ${duration}ms)`);
+ inputEvent = aEvent;
+ };
+
+ if (aTestData.cancelBeforeInput &&
+ (aExpected.beforeInputEvent === null || aExpected.beforeInputEvent === undefined)) {
+ ok(false,
+ `${aDescription}cancelBeforeInput must not be true for ${aTestData.action} because "beforeinput" event is not expected`);
+ return;
+ }
+
+ try {
+ aElement.addEventListener("beforeinput", beforeInputHandler, true);
+ aElement.addEventListener("input", inputHandler, true);
+
+ let initialValue = aElement.value;
+
+ aFunc();
+
+ (function verify() {
+ try {
+ if (aExpected.value !== undefined) {
+ if (aTestData.cancelBeforeInput && aExpected.valueForCanceled === undefined) {
+ is(aElement.value, initialValue,
+ `${aDescription}the value should be "${initialValue}" after ${aTestData.action}`);
+ } else {
+ let expectedValue =
+ aTestData.cancelBeforeInput ? aExpected.valueForCanceled : aExpected.value;
+ is(aElement.value, expectedValue,
+ `${aDescription}the value should be "${expectedValue}" after ${aTestData.action}`);
+ }
+ }
+ if (aExpected.beforeInputEvent === null || aExpected.beforeInputEvent === undefined) {
+ ok(!beforeInputEvent,
+ `${aDescription}"beforeinput" event shouldn't have been fired at ${aTestData.action}`);
+ } else {
+ ok(beforeInputEvent,
+ `${aDescription}"beforeinput" event should've been fired at ${aTestData.action}`);
+ is(beforeInputEvent.cancelable, aExpected.beforeInputEvent.cancelable,
+ `${aDescription}"beforeinput" event by ${aTestData.action} should be ${
+ aExpected.beforeInputEvent.cancelable ? "cancelable" : "not cancelable"
+ }`);
+ is(beforeInputEvent.inputType, aExpected.beforeInputEvent.inputType,
+ `${aDescription}inputType of "beforeinput" event by ${aTestData.action} should be "${aExpected.beforeInputEvent.inputType}"`);
+ is(beforeInputEvent.data, aExpected.beforeInputEvent.data,
+ `${aDescription}data of "beforeinput" event by ${aTestData.action} should be ${
+ aExpected.beforeInputEvent.data === null ? "null" : `"${aExpected.beforeInputEvent.data}"`
+ }`);
+ is(beforeInputEvent.dataTransfer, null,
+ `${aDescription}dataTransfer of "beforeinput" event by ${aTestData.action} should be null`);
+ is(beforeInputEvent.getTargetRanges().length, 0,
+ `${aDescription}getTargetRanges() of "beforeinput" event by ${aTestData.action} should return empty array`);
+ }
+ if ((
+ aTestData.cancelBeforeInput === true &&
+ aExpected.beforeInputEvent &&
+ aExpected.beforeInputEvent.cancelable
+ ) || aExpected.inputEvent === null || aExpected.inputEvent === undefined) {
+ ok(!inputEvent,
+ `${aDescription}"input" event shouldn't have been fired at ${aTestData.action}`);
+ } else {
+ ok(inputEvent,
+ `${aDescription}"input" event should've been fired at ${aTestData.action}`);
+ is(inputEvent.cancelable, false,
+ `${aDescription}"input" event by ${aTestData.action} should be not be cancelable`);
+ is(inputEvent.inputType, aExpected.inputEvent.inputType,
+ `${aDescription}inputType of "input" event by ${aTestData.action} should be "${aExpected.inputEvent.inputType}"`);
+ is(inputEvent.data, aExpected.inputEvent.data,
+ `${aDescription}data of "input" event by ${aTestData.action} should be ${
+ aExpected.inputEvent.data === null ? "null" : `"${aExpected.inputEvent.data}"`
+ }`);
+ is(inputEvent.dataTransfer, null,
+ `${aDescription}dataTransfer of "input" event by ${aTestData.action} should be null`);
+ is(inputEvent.getTargetRanges().length, 0,
+ `${aDescription}getTargetRanges() of "input" event by ${aTestData.action} should return empty array`);
+ }
+ } catch (ex) {
+ ok(false, `${aDescription}unexpected exception at verifying test result of "${aTestData.action}": ${ex.toString()}`);
+ }
+ })();
+ } finally {
+ aElement.removeEventListener("beforeinput", beforeInputHandler, true);
+ aElement.removeEventListener("input", inputHandler, true);
+ }
+ }
+
+ function test_typing_a_in_empty_editor(aTestData) {
+ aElement.value = "";
+ aElement.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ sendString("a");
+ },
+ {
+ value: "a",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertText",
+ data: "a",
+ },
+ inputEvent: {
+ inputType: "insertText",
+ data: "a",
+ },
+ }
+ );
+ }
+ test_typing_a_in_empty_editor({
+ action: 'typing "a" and canceling beforeinput',
+ cancelBeforeInput: true,
+ });
+ test_typing_a_in_empty_editor({
+ action: 'typing "a"',
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_backspace_to_delete_last_character(aTestData) {
+ aElement.value = "a";
+ aElement.focus();
+ aElement.setSelectionStart = "a".length;
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Backspace");
+ },
+ {
+ value: "",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_backspace_to_delete_last_character({
+ actin: 'typing "Backspace" to delete "a" and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_backspace_to_delete_last_character({
+ actin: 'typing "Backspace" to delete "a"',
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_enter_in_empty_editor(aTestData) {
+ aElement.value = "";
+ aElement.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Enter");
+ },
+ aIsTextarea
+ ? {
+ value: "\n",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertLineBreak",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "insertLineBreak",
+ data: null,
+ },
+ }
+ : {
+ value: "",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertLineBreak",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_enter_in_empty_editor({
+ action: 'typing "Enter" in empty editor and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_enter_in_empty_editor({
+ action: 'typing "Enter" in empty editor',
+ cancelBeforeInput: false,
+ });
+
+ (function test_setting_value(aTestData) {
+ aElement.value = "";
+ aElement.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ aElement.value = "foo-bar";
+ },
+ { value: "foo-bar" }
+ );
+ })({
+ action: "setting non-empty value",
+ });
+
+ (function test_setting_empty_value(aTestData) {
+ aElement.value = "foo-bar";
+ aElement.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ aElement.value = "";
+ },
+ { value: "" }
+ );
+ })({
+ action: "setting empty value",
+ });
+
+ (function test_typing_space_in_empty_editor(aTestData) {
+ aElement.value = "";
+ aElement.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ sendString(" ");
+ },
+ {
+ value: " ",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "insertText",
+ data: " ",
+ },
+ inputEvent: {
+ inputType: "insertText",
+ data: " ",
+ },
+ }
+ );
+ })({
+ action: "typing space",
+ });
+
+ (function test_typing_delete_at_end_of_editor(aTestData) {
+ aElement.value = " ";
+ aElement.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Delete");
+ },
+ {
+ value: " ",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: 'typing "Delete" at end of editor',
+ });
+
+ (function test_typing_arrow_left_to_move_caret(aTestData) {
+ aElement.value = " ";
+ aElement.focus();
+ aElement.selectionStart = 1;
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_ArrowLeft");
+ },
+ { value: " " }
+ );
+ })({
+ action: 'typing "ArrowLeft" to move caret',
+ });
+
+ function test_typing_delete_to_delete_last_character(aTestData) {
+ aElement.value = " ";
+ aElement.focus();
+ aElement.selectionStart = 0;
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Delete");
+ },
+ {
+ value: "",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_delete_to_delete_last_character({
+ action: 'typing "Delete" to delete space and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_delete_to_delete_last_character({
+ action: 'typing "Delete" to delete space',
+ cancelBeforeInput: false,
+ });
+
+ function test_undoing_deleting_last_character(aTestData) {
+ aElement.value = "a";
+ aElement.focus();
+ aElement.selectionStart = 0;
+ synthesizeKey("KEY_Delete");
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("z", {accelKey: true});
+ },
+ {
+ value: "a",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "historyUndo",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "historyUndo",
+ data: null,
+ },
+ }
+ );
+ }
+ test_undoing_deleting_last_character({
+ action: 'undoing deleting last character and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_undoing_deleting_last_character({
+ action: "undoing deleting last character",
+ cancelBeforeInput: false,
+ });
+
+ (function test_undoing_without_undoable_transaction(aTestData) {
+ aElement.value = "a";
+ aElement.focus();
+ aElement.selectionStart = 0;
+ synthesizeKey("KEY_Delete");
+ synthesizeKey("z", {accelKey: true});
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("z", {accelKey: true});
+ },
+ { value: "a" }
+ );
+ })({
+ action: "trying to undo without undoable transaction"
+ });
+
+ function test_redoing_deleting_last_character(aTestData) {
+ aElement.value = "a";
+ aElement.focus();
+ aElement.selectionStart = 0;
+ synthesizeKey("KEY_Delete");
+ synthesizeKey("z", {accelKey: true});
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+ },
+ {
+ value: "",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "historyRedo",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "historyRedo",
+ data: null,
+ },
+ }
+ );
+ }
+ test_redoing_deleting_last_character({
+ action: 'redoing deleting last character and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_redoing_deleting_last_character({
+ action: "redoing deleting last character",
+ cancelBeforeInput: false,
+ });
+
+ (function test_redoing_without_redoable_transaction(aTestData) {
+ aElement.value = "a";
+ aElement.focus();
+ aElement.selectionStart = 0;
+ synthesizeKey("KEY_Delete");
+ synthesizeKey("z", {accelKey: true});
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+ },
+ { value: "" }
+ );
+ })({
+ action: "trying to redo without redoable transaction"
+ });
+
+ function test_typing_backspace_with_selecting_all_characters(aTestData) {
+ aElement.value = "abc";
+ aElement.focus();
+ aElement.select();
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Backspace");
+ },
+ {
+ value: "",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_backspace_with_selecting_all_characters({
+ action: 'typing "Backspace" to delete all selected characters and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_backspace_with_selecting_all_characters({
+ action: 'typing "Backspace" to delete all selected characters',
+ cancelBeforeInput: false,
+ });
+
+ function test_typing_delete_with_selecting_all_characters(aTestData) {
+ aElement.value = "abc";
+ aElement.focus();
+ aElement.select();
+
+ runTest(
+ aTestData,
+ () => {
+ synthesizeKey("KEY_Delete");
+ },
+ {
+ value: "",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_typing_delete_with_selecting_all_characters({
+ action: 'typing "Delete" to delete all selected characters and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_typing_delete_with_selecting_all_characters({
+ action: 'typing "Delete" to delete all selected characters and canceling "beforeinput"',
+ cancelBeforeInput: false,
+ });
+
+ function test_deleting_word_backward_from_its_end(aTestData) {
+ aElement.value = "abc def";
+ aElement.focus();
+ document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+ aElement.setSelectionRange("abc def".length, "abc def".length);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ },
+ {
+ value: "abc ",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteWordBackward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteWordBackward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_deleting_word_backward_from_its_end({
+ action: 'deleting word backward from its end and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_deleting_word_backward_from_its_end({
+ action: 'deleting word backward from its end',
+ cancelBeforeInput: false,
+ });
+
+ function test_deleting_word_forward_from_its_start(aTestData) {
+ aElement.value = "abc def";
+ aElement.focus();
+ document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+ aElement.setSelectionRange(0, 0);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_deleteWordForward");
+ },
+ {
+ value: kWordSelectEatSpaceToNextWord ? "def" : " def",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteWordForward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteWordForward",
+ data: null,
+ },
+ }
+ );
+ }
+ test_deleting_word_forward_from_its_start({
+ action: 'deleting word forward from its start and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_deleting_word_forward_from_its_start({
+ action: "deleting word forward from its start",
+ cancelBeforeInput: false,
+ });
+
+ (function test_deleting_word_backward_from_middle_of_second_word(aTestData) {
+ aElement.value = "abc def";
+ aElement.focus();
+ document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+ aElement.setSelectionRange("abc d".length, "abc de".length);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
+ },
+ {
+ value: "abc df",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters backward from middle of second word",
+ });
+
+ (function test_deleting_word_forward_from_middle_of_first_word(aTestData) {
+ aElement.value = "abc def";
+ aElement.focus();
+ document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+ aElement.setSelectionRange("a".length, "ab".length);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_deleteWordForward");
+ },
+ {
+ value: "ac def",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters forward from middle of first word",
+ });
+
+ (function test_deleting_characters_backward_to_start_of_line(aTestData) {
+ aElement.value = "abc def";
+ aElement.focus();
+ document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+ aElement.setSelectionRange("abc d".length, "abc d".length);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
+ },
+ {
+ value: "ef",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteSoftLineBackward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteSoftLineBackward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters backward to start of line"
+ });
+
+ (function test_deleting_characters_forward_to_end_of_line(aTestData) {
+ aElement.value = "abc def";
+ aElement.focus();
+ document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+ aElement.setSelectionRange("ab".length, "ab".length);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_deleteToEndOfLine");
+ },
+ {
+ value: "ab",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteSoftLineForward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteSoftLineForward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters forward to end of line",
+ });
+
+ (function test_deleting_characters_backward_to_start_of_line_with_non_collapsed_selection(aTestData) {
+ aElement.value = "abc def";
+ aElement.focus();
+ document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+ aElement.setSelectionRange("abc d".length, "abc_de".length);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
+ },
+ {
+ value: "abc df",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteContentBackward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters backward to start of line (with selection in second word)",
+ });
+
+ (function test_deleting_characters_forward_to_end_of_line_with_non_collapsed_selection(aTestData) {
+ aElement.value = "abc def";
+ aElement.focus();
+ document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
+ aElement.setSelectionRange("a".length, "ab".length);
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_deleteToEndOfLine");
+ },
+ {
+ value: "ac def",
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ inputEvent: {
+ inputType: "deleteContentForward",
+ data: null,
+ },
+ }
+ );
+ })({
+ action: "removing characters forward to end of line (with selection in second word)",
+ });
+
+ function test_switching_text_direction_from_default(aTestData) {
+ try {
+ aElement.removeAttribute("dir");
+ aElement.scrollTop; // XXX Update the root frame
+ aElement.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_switchTextDirection");
+ if (aTestData.cancelBeforeInput) {
+ is(aElement.getAttribute("dir"), null,
+ `${aDescription}dir attribute of the element shouldn't have been set by ${aTestData.action}`);
+ } else {
+ is(aElement.getAttribute("dir"), "rtl",
+ `${aDescription}dir attribute of the element should've been set to "rtl" by ${aTestData.action}`);
+ }
+ },
+ {
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "formatSetBlockTextDirection",
+ data: "rtl",
+ },
+ inputEvent: {
+ inputType: "formatSetBlockTextDirection",
+ data: "rtl",
+ },
+ }
+ );
+ } finally {
+ aElement.removeAttribute("dir");
+ aElement.scrollTop; // XXX Update the root frame
+ }
+ }
+ test_switching_text_direction_from_default({
+ action: 'switching text direction from default to "rtl" and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_switching_text_direction_from_default({
+ action: 'switching text direction from default to "rtl"',
+ cancelBeforeInput: false,
+ });
+
+ function test_switching_text_direction_from_rtl_to_ltr(aTestData) {
+ try {
+ aElement.setAttribute("dir", "rtl");
+ aElement.scrollTop; // XXX Update the root frame
+ aElement.focus();
+
+ runTest(
+ aTestData,
+ () => {
+ SpecialPowers.doCommand(window, "cmd_switchTextDirection");
+ let expectedDirValue = aTestData.cancelBeforeInput ? "rtl" : "ltr";
+ is(aElement.getAttribute("dir"), expectedDirValue,
+ `${aDescription}dir attribute of the element should be "${expectedDirValue}" after ${aTestData.action}`);
+ },
+ {
+ beforeInputEvent: {
+ cancelable: true,
+ inputType: "formatSetBlockTextDirection",
+ data: "ltr",
+ },
+ inputEvent: {
+ inputType: "formatSetBlockTextDirection",
+ data: "ltr",
+ },
+ }
+ );
+ } finally {
+ aElement.removeAttribute("dir");
+ aElement.scrollTop; // XXX Update the root frame
+ }
+ }
+ test_switching_text_direction_from_rtl_to_ltr({
+ action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"',
+ cancelBeforeInput: true,
+ });
+ test_switching_text_direction_from_rtl_to_ltr({
+ action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"',
+ cancelBeforeInput: false,
+ });
+ }
+
+ doTests(document.getElementById("input"), "<input type=\"text\">", false);
+ doTests(document.getElementById("textarea"), "<textarea>", true);
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_dragdrop.html b/editor/libeditor/tests/test_dragdrop.html
new file mode 100644
index 0000000000..6295661faa
--- /dev/null
+++ b/editor/libeditor/tests/test_dragdrop.html
@@ -0,0 +1,3451 @@
+<!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 = aEvent => {
+ 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 = aEvent => {
+ 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 = aEvent => {
+ input.style.display = "none";
+ document.documentElement.scrollTop;
+ input.style.display = "";
+ document.documentElement.scrollTop;
+ };
+ const onMouseDown = aEvent => {
+ 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 = aEvent => {
+ textarea.style.display = "none";
+ document.documentElement.scrollTop;
+ textarea.style.display = "";
+ document.documentElement.scrollTop;
+ };
+ const onMouseDown = aEvent => {
+ 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>
diff --git a/editor/libeditor/tests/test_execCommandPaste_noTarget.html b/editor/libeditor/tests/test_execCommandPaste_noTarget.html
new file mode 100644
index 0000000000..6586ca768d
--- /dev/null
+++ b/editor/libeditor/tests/test_execCommandPaste_noTarget.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script>
+
+ add_task(async function() {
+ let seenPaste = false;
+ let seenCopy = false;
+ document.addEventListener("copy", function oncpy(e) {
+ e.clipboardData.setData("text/plain", "my text");
+ e.preventDefault();
+ seenCopy = true;
+ }, {once: true});
+ document.addEventListener("paste", function onpst(e) {
+ is(e.clipboardData.getData("text/plain"), "my text",
+ "The correct text was read from the clipboard");
+ e.preventDefault();
+ seenPaste = true;
+ }, {once: true});
+
+ ok(SpecialPowers.wrap(document).execCommand("copy"),
+ "Call should succeed");
+ ok(seenCopy, "Successfully copied the text to the clipboard");
+ ok(SpecialPowers.wrap(document).execCommand("paste"),
+ "Call should succeed");
+ ok(seenPaste, "Successfully read text from the clipboard");
+
+ // Check that reading text from the clipboard in non-privileged contexts
+ // still doesn't work.
+ function onpstfail(e) {
+ ok(false, "Should not see paste event triggered by non-privileged call");
+ }
+ document.addEventListener("paste", onpstfail);
+ ok(!document.execCommand("paste"), "Call should fail");
+ document.removeEventListener("paste", onpstfail);
+ });
+
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_focus_caret_navigation_between_nested_editors.html b/editor/libeditor/tests/test_focus_caret_navigation_between_nested_editors.html
new file mode 100644
index 0000000000..c0ff01ae10
--- /dev/null
+++ b/editor/libeditor/tests/test_focus_caret_navigation_between_nested_editors.html
@@ -0,0 +1,207 @@
+<!DOCTYPE>
+<html>
+<head>
+<title>Test for bug1318312</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<style type="text/css">
+</style>
+</head>
+<body>
+<div id="outerEditor" contenteditable><p>editable in outer editor</p>
+<div id="staticInEditor" contenteditable="false"><p>non-editable in outer editor</p>
+<div id="innerEditor" contenteditable><p>editable in inner editor</p></div></div></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var outerEditor = document.getElementById("outerEditor");
+var innerEditor = document.getElementById("innerEditor");
+
+function getNodeDescription(node) {
+ if (!node) {
+ return "null";
+ }
+ switch (node.nodeType) {
+ case Node.TEXT_NODE:
+ case Node.COMMENT_NODE:
+ case Node.CDATA_SECTION_NODE:
+ return `${node.nodeName} "${node.data}"`;
+ case Node.ELEMENT_NODE:
+ return `<${node.nodeName.toLowerCase()}>`;
+ default:
+ return `${node.nodeName}`;
+ }
+}
+
+function getRangeDescription(range) {
+ if (range === null) {
+ return "null";
+ }
+ if (range === undefined) {
+ return "undefined";
+ }
+ return range.startContainer == range.endContainer &&
+ range.startOffset == range.endOffset
+ ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
+ : `(${getNodeDescription(range.startContainer)}, ${
+ range.startOffset
+ }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
+}
+
+function runTests() {
+ outerEditor.focus();
+ is(document.activeElement, outerEditor,
+ "outerEditor should have focus");
+
+ // Move cursor into the innerEditor with ArrowDown key. Then, focus shouldn't
+ // be moved to innerEditor from outerEditor.
+ // Note that Chrome moves focus in this case. However, we should align the
+ // behavior to Chrome for now because we don't support move caret from a
+ // selection limiter to outside of it.
+ (() => {
+ const description = "Press ArrowDown from start of the outer editor";
+ synthesizeKey("KEY_ArrowDown");
+ is(
+ document.activeElement,
+ outerEditor,
+ `${description}: The outer editor should keep having focus`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(#text "editable in inner editor", 0)',
+ `${description}: Caret should be moved to start of the inner editor`
+ );
+ })();
+
+ (() => {
+ const description = 'Typing "a" at start of the inner editor';
+ sendString("a");
+ is(
+ document.activeElement,
+ outerEditor,
+ `${description}: The outer editor should keep having focus`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(#text "aeditable in inner editor", 1)',
+ `${description}: Caret should be moved to after typed character, "a"`
+ );
+ })();
+
+ (() => {
+ const description = 'Pressing Enter next to the typed character "a"';
+ synthesizeKey("KEY_Enter");
+ is(
+ document.activeElement,
+ outerEditor,
+ `${description}: The outer editor should keep having focus`
+ );
+ is(
+ innerEditor.innerHTML,
+ "<p>a</p><p>editable in inner editor</p>",
+ `${description}: The paragraph in the inner editor should be split after "a"`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(#text "editable in inner editor", 0)',
+ `${description}: Caret should be moved to start of the second paragraph`
+ );
+ })();
+
+ (() => {
+ const description = 'Pressing Backspace at start of the second paragraph';
+ synthesizeKey("KEY_Backspace");
+ is(
+ document.activeElement,
+ outerEditor,
+ `${description}: The outer editor should keep having focus`
+ );
+ is(
+ innerEditor.innerHTML,
+ "<p>aeditable in inner editor</p>",
+ `${description}: The paragraphs should be joined`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(#text "aeditable in inner editor", 1)',
+ `${description}: Caret should be moved to end of text which was in the first paragraph`
+ );
+ })();
+
+ (() => {
+ const description = 'Pressing Shift-ArrowLeft from next to the first character, "a"';
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ is(
+ document.activeElement,
+ outerEditor,
+ `${description}: The outer editor should keep having focus`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(#text "aeditable in inner editor", 0) - (#text "aeditable in inner editor", 1)',
+ `${description}: The first character, "a", should be selected`
+ );
+ })();
+
+ (() => {
+ const description = 'Pressing Delete to delete selected "a"';
+ synthesizeKey("KEY_Delete");
+ is(
+ document.activeElement,
+ outerEditor,
+ `${description}: The outer editor should keep having focus`
+ );
+ is(
+ innerEditor.innerHTML,
+ "<p>editable in inner editor</p>",
+ `${description}: The selected "a" should be deleted`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(#text "editable in inner editor", 0)',
+ `${description}: Selection should be collapsed at start of the inner editor`
+ );
+ })();
+
+ (() => {
+ const description = 'Pressing ArrowUp from start of the inner editor';
+ synthesizeKey("KEY_ArrowUp");
+ is(
+ document.activeElement,
+ outerEditor,
+ `${description}: The outer editor should keep having focus`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(#text "editable in outer editor", 0)',
+ `${description}: Caret should be moved to start of the outer editor`
+ );
+ })();
+
+ // However, clicking in innerEditor should move focus.
+ (() => {
+ const description = 'Clicking the inner editor when caret is in the outer editor';
+ synthesizeMouseAtCenter(innerEditor, {});
+ is(
+ document.activeElement,
+ innerEditor,
+ `${description}: The inner editor should get focus`
+ );
+ is(
+ getNodeDescription(getSelection().focusNode),
+ '#text "editable in inner editor"',
+ `${description}: Caret should move into the inner editor`
+ );
+ })();
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_focused_document_element_becoming_editable.html b/editor/libeditor/tests/test_focused_document_element_becoming_editable.html
new file mode 100644
index 0000000000..98ddf54f2b
--- /dev/null
+++ b/editor/libeditor/tests/test_focused_document_element_becoming_editable.html
@@ -0,0 +1,157 @@
+<!doctype html>
+<html>
+<head>
+<meta chareset="utf-8">
+<title>Testing non-editable root becomes editable after getting focus</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+SimpleTest.waitForExplicitFinish();
+addEventListener("load", async () => {
+ await SimpleTest.promiseFocus(window);
+
+ await (async () => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ iframe.addEventListener("load", async () => {
+ const doc = iframe.contentDocument;
+ const win = iframe.contentWindow;
+ win.focus();
+ doc.documentElement.focus();
+ doc.designMode = "on";
+ await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(r)));
+ is(
+ SpecialPowers.getDOMWindowUtils(win).IMEStatus,
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be enabled in the design mode document"
+ );
+ is(
+ SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver),
+ doc.body,
+ "The <body> should be observed by IMEContentObserver in design mode"
+ );
+ doc.designMode = "off";
+ iframe.remove();
+ resolve();
+ }, {once: true});
+ info("Waiting for load of sub-document for testing design mode");
+ iframe.srcdoc = "<!doctype html><html><meta charset=\"utf-8\"></head><body></body></html>";
+ });
+ })();
+
+ await (async () => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ iframe.addEventListener("load", async () => {
+ const doc = iframe.contentDocument;
+ const win = iframe.contentWindow;
+ win.focus()
+ doc.documentElement.focus();
+ doc.documentElement.contentEditable = "true";
+ await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(r)));
+ is(
+ SpecialPowers.getDOMWindowUtils(win).IMEStatus,
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be enabled when the <html> element whose contenteditable is set to true"
+ );
+ is(
+ SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver),
+ doc.documentElement,
+ "The <html> should be observed by IMEContentObserver when <html contenteditable=\"true\">"
+ );
+ iframe.remove();
+ resolve();
+ }, {once: true});
+ info("Waiting for load of sub-document for testing <html> element becomes editable");
+ iframe.srcdoc = "<!doctype html><html><meta charset=\"utf-8\"></head><body></body></html>";
+ });
+ })();
+
+ await (async () => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ iframe.addEventListener("load", async () => {
+ const doc = iframe.contentDocument;
+ const win = iframe.contentWindow;
+ win.focus();
+ doc.body.focus();
+ doc.body.contentEditable = "true";
+ await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(r)));
+ if (doc.activeElement === doc.body && doc.hasFocus()) {
+ todo_is(
+ SpecialPowers.getDOMWindowUtils(win).IMEStatus,
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be enabled when the <body> element whose contenteditable is set to true and it has focus"
+ );
+ todo_is(
+ SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver),
+ doc.body,
+ "The <body> should be observed by IMEContentObserver when <body contenteditable=\"true\"> and it has focus"
+ );
+ } else {
+ is(
+ SpecialPowers.getDOMWindowUtils(win).IMEStatus,
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME should be disabled when the <body> element whose contenteditable is set to true but it does not have focus"
+ );
+ is(
+ SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver),
+ null,
+ "Nobody should be observed by IMEContentObserver when <body contenteditable=\"true\"> but it does not have focus"
+ );
+ }
+ iframe.remove();
+ resolve();
+ }, {once: true});
+ info("Waiting for load of sub-document for testing <body> element becomes editable");
+ iframe.srcdoc = "<!doctype html><html><meta charset=\"utf-8\"></head><body></body></html>";
+ });
+ })();
+
+ await (async () => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ iframe.addEventListener("load", async () => {
+ const doc = iframe.contentDocument;
+ const win = iframe.contentWindow;
+ win.focus();
+ const editingHost = doc.createElement("div");
+ doc.documentElement.remove();
+ doc.appendChild(editingHost);
+ editingHost.focus();
+ is(
+ SpecialPowers.unwrap(SpecialPowers.focusManager.focusedElement),
+ editingHost,
+ "The <div contenteditable> should have focus because of only child of the Document node"
+ );
+ editingHost.contentEditable = "true";
+ await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(r)));
+ is(
+ SpecialPowers.getDOMWindowUtils(win).IMEStatus,
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be enabled in the root element"
+ );
+ is(
+ SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver),
+ editingHost,
+ "The <div contenteditable> should be observed by IMEContentObserver"
+ );
+ iframe.srcdoc = "";
+ resolve();
+ }, {once: true});
+ info("Waiting for load of sub-document for testing root <div> element becomes editable");
+ iframe.srcdoc = "<!doctype html><html><meta charset=\"utf-8\"></head><body></body></html>";
+ });
+ })();
+
+ SimpleTest.finish();
+}, false);
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_handle_new_lines.html b/editor/libeditor/tests/test_handle_new_lines.html
new file mode 100644
index 0000000000..0e5825aaad
--- /dev/null
+++ b/editor/libeditor/tests/test_handle_new_lines.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for TextEditor::HandleNewLinesInStringForSingleLineEditor()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="container"></div>
+
+<textarea id="toCopyPlaintext" style="display: none;"></textarea>
+
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+
+async function copyPlaintext(aText) {
+ return new Promise(resolve => {
+ SimpleTest.waitForClipboard(
+ aText.replace(/\r\n?/g, "\n"),
+ () => {
+ let element = document.getElementById("toCopyPlaintext");
+ element.style.display = "block";
+ element.focus();
+ element.value = aText;
+ synthesizeKey("a", {accelKey: true});
+ synthesizeKey("c", {accelKey: true});
+ },
+ () => {
+ ok(true, `Succeeded to copy "${aText.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/ /g, "\u00A0")}" to clipboard`);
+ let element = document.getElementById("toCopyPlaintext");
+ element.style.display = "none";
+ resolve();
+ },
+ () => {
+ SimpleTest.finish();
+ });
+ });
+}
+
+async function doTests() {
+ // nsIEditor::eNewlinesPasteIntact (0):
+ // only remove the leading and trailing newlines.
+ // nsIEditor::eNewlinesPasteToFirst (1) or any other value:
+ // remove the first newline and all characters following it.
+ // nsIEditor::eNewlinesReplaceWithSpaces (2, Firefox default):
+ // replace newlines with spaces.
+ // nsIEditor::eNewlinesStrip (3):
+ // remove newlines from the string.
+ // nsIEditor::eNewlinesReplaceWithCommas (4, Thunderbird default):
+ // replace newlines with commas.
+ // nsIEditor::eNewlinesStripSurroundingWhitespace (5):
+ // collapse newlines and surrounding whitespace characters and
+ // remove them from the string.
+
+ // value: setting or pasting text.
+ // expected: array of final values for each above pref value.
+ // setValue: expected result when HTMLInputElement.value is set to the value.
+ // pasteValue: expected result when pasting the value from clipboard.
+ //
+ // Note that HTMLInputElement strips both \r and \n. Therefore, each expected
+ // result is different from pasting the value.
+ const kTests = [
+ { value: "\nabc\ndef\n",
+ expected: [{ setValue: "abcdef", pasteValue: "abc\ndef" },
+ { setValue: "abcdef", pasteValue: "abc" },
+ { setValue: "abcdef", pasteValue: " abc def" },
+ { setValue: "abcdef", pasteValue: "abcdef" },
+ { setValue: "abcdef", pasteValue: "abc,def" },
+ { setValue: "abcdef", pasteValue: "abcdef" }],
+ },
+ { value: "\n abc \n def \n",
+ expected: [{ setValue: " abc def ", pasteValue: " abc \n def " },
+ { setValue: " abc def ", pasteValue: " abc " },
+ { setValue: " abc def ", pasteValue: " abc def " },
+ { setValue: " abc def ", pasteValue: " abc def " },
+ { setValue: " abc def ", pasteValue: " abc , def " },
+ { setValue: " abc def ", pasteValue: "abcdef" }],
+ },
+ { value: " abc \n def ",
+ expected: [{ setValue: " abc def ", pasteValue: " abc \n def " },
+ { setValue: " abc def ", pasteValue: " abc " },
+ { setValue: " abc def ", pasteValue: " abc def " },
+ { setValue: " abc def ", pasteValue: " abc def " },
+ { setValue: " abc def ", pasteValue: " abc , def " },
+ { setValue: " abc def ", pasteValue: " abcdef " }],
+ },
+ ];
+
+ let container = document.getElementById("container");
+ for (let i = 0; i <= 5; i++) {
+ await SpecialPowers.pushPrefEnv({"set": [["editor.singleLine.pasteNewlines", i]]});
+ container.innerHTML = `<input id="input${i}" type="text">`;
+ let input = document.getElementById(`input${i}`);
+ input.focus();
+ let editor = SpecialPowers.wrap(input).editor;
+ for (const kLineBreaker of ["\n", "\r", "\r\n"]) {
+ for (let kTest of kTests) {
+ let value = kTest.value.replace(/\n/g, kLineBreaker);
+ input.value = value;
+ is(editor.rootElement.firstChild.wholeText, kTest.expected[i].setValue,
+ `Setting value to "${value.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/ /g, "\u00A0")}" when pref is ${i}`);
+ input.value = "";
+
+ await copyPlaintext(value);
+ input.focus();
+ synthesizeKey("v", {accelKey: true});
+ is(editor.rootElement.firstChild.wholeText, kTest.expected[i].pasteValue,
+ `Pasting "${value.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/ /g, "\u00A0")}" when pref is ${i}`);
+ input.value = "";
+ }
+ }
+ }
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(doTests);
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_htmleditor_keyevent_handling.html b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html
new file mode 100644
index 0000000000..58666beb35
--- /dev/null
+++ b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html
@@ -0,0 +1,766 @@
+<html>
+<head>
+ <title>Test for key event handler of HTML editor</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <div id="htmlEditor" contenteditable="true"><br></div>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/* eslint-disable no-nested-ternary */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+var htmlEditor = document.getElementById("htmlEditor");
+
+const kIsMac = navigator.platform.includes("Mac");
+const kIsWin = navigator.platform.includes("Win");
+const kIsLinux = navigator.platform.includes("Linux") || navigator.platform.includes("SunOS");
+
+async function runTests() {
+ document.execCommand("stylewithcss", false, "true");
+ document.execCommand("defaultParagraphSeparator", false, "div");
+
+ var fm = SpecialPowers.Services.focus;
+
+ var capturingPhase = { fired: false, prevented: false };
+ var bubblingPhase = { fired: false, prevented: false };
+
+ var listener = {
+ handleEvent: function _hv(aEvent) {
+ is(aEvent.type, "keypress", "unexpected event is handled");
+ switch (aEvent.eventPhase) {
+ case aEvent.CAPTURING_PHASE:
+ capturingPhase.fired = true;
+ capturingPhase.prevented = aEvent.defaultPrevented;
+ break;
+ case aEvent.BUBBLING_PHASE:
+ bubblingPhase.fired = true;
+ bubblingPhase.prevented = aEvent.defaultPrevented;
+ aEvent.preventDefault(); // prevent the browser default behavior
+ break;
+ default:
+ ok(false, "event is handled in unexpected phase");
+ }
+ },
+ };
+
+ function check(aDescription,
+ aFiredOnCapture, aFiredOnBubbling, aPreventedOnBubbling) {
+ function getDesciption(aExpected) {
+ return aDescription + (aExpected ? " wasn't " : " was ");
+ }
+ is(capturingPhase.fired, aFiredOnCapture,
+ getDesciption(aFiredOnCapture) + "fired on capture phase");
+ is(bubblingPhase.fired, aFiredOnBubbling,
+ getDesciption(aFiredOnBubbling) + "fired on bubbling phase");
+
+ // If the event is fired on bubbling phase and it was already prevented
+ // on capture phase, it must be prevented on bubbling phase too.
+ if (capturingPhase.prevented) {
+ todo(false, aDescription +
+ " was consumed already, so, we cannot test the editor behavior actually");
+ aPreventedOnBubbling = true;
+ }
+
+ is(bubblingPhase.prevented, aPreventedOnBubbling,
+ getDesciption(aPreventedOnBubbling) + "prevented on bubbling phase");
+ }
+
+ SpecialPowers.addSystemEventListener(window, "keypress", listener, true);
+ SpecialPowers.addSystemEventListener(window, "keypress", listener, false);
+
+ // eslint-disable-next-line complexity
+ async function doTest(
+ aElement,
+ aDescription,
+ aIsReadonly,
+ aIsTabbable,
+ aIsPlaintext
+ ) {
+ function reset(aText) {
+ capturingPhase.fired = false;
+ capturingPhase.prevented = false;
+ bubblingPhase.fired = false;
+ bubblingPhase.prevented = false;
+ aElement.innerHTML = aText;
+ var sel = window.getSelection();
+ var range = document.createRange();
+ range.setStart(aElement, aElement.childNodes.length);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+
+ function resetForIndent(aText) {
+ capturingPhase.fired = false;
+ capturingPhase.prevented = false;
+ bubblingPhase.fired = false;
+ bubblingPhase.prevented = false;
+ aElement.innerHTML = aText;
+ var sel = window.getSelection();
+ var range = document.createRange();
+ var target = document.getElementById("target").firstChild;
+ range.setStart(target, target.length);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+
+ aDescription += ": ";
+
+ aElement.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement, aDescription + "failed to move focus");
+
+ // Backspace key:
+ // If native key bindings map the key combination to something, it's consumed.
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable, it consumes backspace and shift+backspace.
+ // Otherwise, editor doesn't consume the event.
+ reset("");
+ synthesizeKey("KEY_Backspace");
+ check(aDescription + "Backspace", true, true, true);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {shiftKey: true});
+ check(aDescription + "Shift+Backspace", true, true, true);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {ctrlKey: true});
+ check(aDescription + "Ctrl+Backspace", true, true, aIsReadonly || kIsLinux);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {altKey: true});
+ check(aDescription + "Alt+Backspace", true, true, aIsReadonly || kIsMac);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {metaKey: true});
+ check(aDescription + "Meta+Backspace", true, true, aIsReadonly || kIsMac);
+
+ // Delete key:
+ // If native key bindings map the key combination to something, it's consumed.
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable, delete is consumed.
+ // Otherwise, editor doesn't consume the event.
+ reset("");
+ synthesizeKey("KEY_Delete");
+ check(aDescription + "Delete", true, true, !aIsReadonly || kIsMac || kIsLinux);
+
+ reset("");
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ check(aDescription + "Shift+Delete", true, true, kIsMac || kIsLinux);
+
+ reset("");
+ synthesizeKey("KEY_Delete", {ctrlKey: true});
+ check(aDescription + "Ctrl+Delete", true, true, kIsLinux);
+
+ reset("");
+ synthesizeKey("KEY_Delete", {altKey: true});
+ check(aDescription + "Alt+Delete", true, true, kIsMac);
+
+ reset("");
+ synthesizeKey("KEY_Delete", {metaKey: true});
+ check(aDescription + "Meta+Delete", true, true, false);
+
+ // Return key:
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable and not single line editor, it consumes Return
+ // and Shift+Return.
+ // Otherwise, editor doesn't consume the event.
+ reset("a");
+ synthesizeKey("KEY_Enter");
+ check(aDescription + "Return",
+ true, true, !aIsReadonly);
+ is(aElement.innerHTML, aIsReadonly ? "a" : "<div>a</div><br>",
+ aDescription + "Return");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {shiftKey: true});
+ check(aDescription + "Shift+Return",
+ true, true, !aIsReadonly);
+ is(aElement.innerHTML, aIsReadonly ? "a" : "a<br><br>",
+ aDescription + "Shift+Return");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {ctrlKey: true});
+ check(aDescription + "Ctrl+Return", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Ctrl+Return");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {altKey: true});
+ check(aDescription + "Alt+Return", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Alt+Return");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {metaKey: true});
+ check(aDescription + "Meta+Return", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Meta+Return");
+
+ // Tab key:
+ // If editor is tabbable, editor doesn't consume all tab key events.
+ // Otherwise, editor consumes tab key event without any modifier keys.
+ reset("a");
+ synthesizeKey("KEY_Tab");
+ check(aDescription + "Tab",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ (() => {
+ if (aIsTabbable || aIsReadonly) {
+ return "a";
+ }
+ if (aIsPlaintext) {
+ return "a\t";
+ }
+ return SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible")
+ ? "a&nbsp; &nbsp;&nbsp;"
+ : "a&nbsp;&nbsp;&nbsp; <br>";
+ })(),
+ aDescription + "Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab)");
+
+ reset("a");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Shift+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ reset("a");
+ synthesizeKey("KEY_Tab", {ctrlKey: true});
+ check(aDescription + "Ctrl+Tab", false, false, false);
+ is(aElement.innerHTML, "a", aDescription + "Ctrl+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab)");
+
+ reset("a");
+ synthesizeKey("KEY_Tab", {altKey: true});
+ check(aDescription + "Alt+Tab", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Alt+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab)");
+
+ reset("a");
+ synthesizeKey("KEY_Tab", {metaKey: true});
+ check(aDescription + "Meta+Tab", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "Meta+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab)");
+
+ // Indent/Outdent tests:
+ // UL
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("KEY_Tab");
+ check(aDescription + "Tab on UL",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable ?
+ "<ul><li id=\"target\">ul list item</li></ul>" :
+ aIsPlaintext ? "<ul><li id=\"target\">ul list item\t</li></ul>" :
+ "<ul><ul><li id=\"target\">ul list item</li></ul></ul>",
+ aDescription + "Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab on UL)");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab after Tab on UL",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable || (!aIsPlaintext) ?
+ "<ul><li id=\"target\">ul list item</li></ul>" :
+ "<ul><li id=\"target\">ul list item\t</li></ul>",
+ aDescription + "Shift+Tab after Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab after Tab on UL)");
+
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab on UL",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable || aIsPlaintext ?
+ "<ul><li id=\"target\">ul list item</li></ul>" : "ul list item",
+ aDescription + "Shift+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab on UL)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("KEY_Tab", {ctrlKey: true});
+ check(aDescription + "Ctrl+Tab on UL", false, false, false);
+ is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>",
+ aDescription + "Ctrl+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab on UL)");
+
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("KEY_Tab", {altKey: true});
+ check(aDescription + "Alt+Tab on UL", true, true, false);
+ is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>",
+ aDescription + "Alt+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab on UL)");
+
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("KEY_Tab", {metaKey: true});
+ check(aDescription + "Meta+Tab on UL", true, true, false);
+ is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>",
+ aDescription + "Meta+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab on UL)");
+
+ // OL
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("KEY_Tab");
+ check(aDescription + "Tab on OL",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable ?
+ "<ol><li id=\"target\">ol list item</li></ol>" :
+ aIsPlaintext ? "<ol><li id=\"target\">ol list item\t</li></ol>" :
+ "<ol><ol><li id=\"target\">ol list item</li></ol></ol>",
+ aDescription + "Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab on OL)");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab after Tab on OL",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable || (!aIsPlaintext) ?
+ "<ol><li id=\"target\">ol list item</li></ol>" :
+ "<ol><li id=\"target\">ol list item\t</li></ol>",
+ aDescription + "Shift+Tab after Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab after Tab on OL)");
+
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab on OL",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsReadonly || aIsTabbable || aIsPlaintext ?
+ "<ol><li id=\"target\">ol list item</li></ol>" : "ol list item",
+ aDescription + "Shfit+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab on OL)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("KEY_Tab", {ctrlKey: true});
+ check(aDescription + "Ctrl+Tab on OL", false, false, false);
+ is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>",
+ aDescription + "Ctrl+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab on OL)");
+
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("KEY_Tab", {altKey: true});
+ check(aDescription + "Alt+Tab on OL", true, true, false);
+ is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>",
+ aDescription + "Alt+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab on OL)");
+
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("KEY_Tab", {metaKey: true});
+ check(aDescription + "Meta+Tab on OL", true, true, false);
+ is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>",
+ aDescription + "Meta+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab on OL)");
+
+ // TD
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("KEY_Tab");
+ check(aDescription + "Tab on TD",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsTabbable || aIsReadonly ?
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>" :
+ aIsPlaintext ? "<table><tbody><tr><td id=\"target\">td\t</td></tr></tbody></table>" :
+ "<table><tbody><tr><td id=\"target\">td</td></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>",
+ aDescription + "Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab on TD)");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab after Tab on TD",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsTabbable || aIsReadonly ?
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>" :
+ aIsPlaintext ? "<table><tbody><tr><td id=\"target\">td\t</td></tr></tbody></table>" :
+ "<table><tbody><tr><td id=\"target\">td</td></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>",
+ aDescription + "Shift+Tab after Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab after Tab on TD)");
+
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab on TD", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "Shift+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab on TD)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("KEY_Tab", {ctrlKey: true});
+ check(aDescription + "Ctrl+Tab on TD", false, false, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "Ctrl+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab on TD)");
+
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("KEY_Tab", {altKey: true});
+ check(aDescription + "Alt+Tab on TD", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "Alt+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab on TD)");
+
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("KEY_Tab", {metaKey: true});
+ check(aDescription + "Meta+Tab on TD", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "Meta+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab on TD)");
+
+ // TH
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("KEY_Tab");
+ check(aDescription + "Tab on TH",
+ true, true, !aIsTabbable && !aIsReadonly);
+ is(aElement.innerHTML,
+ aIsTabbable || aIsReadonly ?
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>" :
+ aIsPlaintext ? "<table><tbody><tr><th id=\"target\">th\t</th></tr></tbody></table>" :
+ "<table><tbody><tr><th id=\"target\">th</th></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>",
+ aDescription + "Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab on TH)");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab after Tab on TH",
+ true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext);
+ is(aElement.innerHTML,
+ aIsTabbable || aIsReadonly ?
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>" :
+ aIsPlaintext ? "<table><tbody><tr><th id=\"target\">th\t</th></tr></tbody></table>" :
+ "<table><tbody><tr><th id=\"target\">th</th></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>",
+ aDescription + "Shift+Tab after Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab after Tab on TH)");
+
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab on TH", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "Shift+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab on TH)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("KEY_Tab", {ctrlKey: true});
+ check(aDescription + "Ctrl+Tab on TH", false, false, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "Ctrl+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab on TH)");
+
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("KEY_Tab", {altKey: true});
+ check(aDescription + "Alt+Tab on TH", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "Alt+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab on TH)");
+
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("KEY_Tab", {metaKey: true});
+ check(aDescription + "Meta+Tab on TH", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "Meta+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab on TH)");
+
+ // Esc key:
+ // In all cases, esc key events are not consumed
+ reset("abc");
+ synthesizeKey("KEY_Escape");
+ check(aDescription + "Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {shiftKey: true});
+ check(aDescription + "Shift+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {ctrlKey: true});
+ check(aDescription + "Ctrl+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {altKey: true});
+ check(aDescription + "Alt+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {metaKey: true});
+ check(aDescription + "Meta+Esc", true, true, false);
+
+ // typical typing tests:
+ reset("");
+ sendString("M");
+ check(aDescription + "M", true, true, !aIsReadonly);
+ sendString("o");
+ check(aDescription + "o", true, true, !aIsReadonly);
+ sendString("z");
+ check(aDescription + "z", true, true, !aIsReadonly);
+ sendString("i");
+ check(aDescription + "i", true, true, !aIsReadonly);
+ sendString("l");
+ check(aDescription + "l", true, true, !aIsReadonly);
+ sendString("l");
+ check(aDescription + "l", true, true, !aIsReadonly);
+ sendString("a");
+ check(aDescription + "a", true, true, !aIsReadonly);
+ sendString(" ");
+ check(aDescription + "' '", true, true, !aIsReadonly);
+ is(aElement.innerHTML,
+ (() => {
+ if (aIsReadonly) {
+ return "";
+ }
+ if (aIsPlaintext) {
+ return "Mozilla ";
+ }
+ return SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible")
+ ? "Mozilla&nbsp;"
+ : "Mozilla <br>";
+ })(),
+ aDescription + "typed \"Mozilla \"");
+
+ // typing non-BMP character:
+ async function test_typing_surrogate_pair(
+ aTestPerSurrogateKeyPress,
+ aTestIllFormedUTF16KeyValue = false
+ ) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress],
+ ["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue],
+ ],
+ });
+ reset("");
+ let events = [];
+ function pushIntoEvents(aEvent) {
+ events.push(aEvent);
+ }
+ function getEventData(aKeyboardEventOrInputEvent) {
+ if (!aKeyboardEventOrInputEvent) {
+ return "{}";
+ }
+ switch (aKeyboardEventOrInputEvent.type) {
+ case "keydown":
+ case "keypress":
+ case "keyup":
+ return `{ type: "${aKeyboardEventOrInputEvent.type}", key="${
+ aKeyboardEventOrInputEvent.key
+ }", charCode=0x${
+ aKeyboardEventOrInputEvent.charCode.toString(16).toUpperCase()
+ } }`;
+ default:
+ return `{ type: "${aKeyboardEventOrInputEvent.type}", inputType="${
+ aKeyboardEventOrInputEvent.inputType
+ }", data="${aKeyboardEventOrInputEvent.data}" }`;
+ }
+ }
+ function getEventArrayData(aEvents) {
+ if (!aEvents.length) {
+ return "[]";
+ }
+ let result = "[\n";
+ for (const e of aEvents) {
+ result += ` ${getEventData(e)}\n`;
+ }
+ return result + "]";
+ }
+ aElement.addEventListener("keydown", pushIntoEvents);
+ aElement.addEventListener("keypress", pushIntoEvents);
+ aElement.addEventListener("keyup", pushIntoEvents);
+ aElement.addEventListener("beforeinput", pushIntoEvents);
+ aElement.addEventListener("input", pushIntoEvents);
+ synthesizeKey("\uD842\uDFB7");
+ aElement.removeEventListener("keydown", pushIntoEvents);
+ aElement.removeEventListener("keypress", pushIntoEvents);
+ aElement.removeEventListener("keyup", pushIntoEvents);
+ aElement.removeEventListener("beforeinput", pushIntoEvents);
+ aElement.removeEventListener("input", pushIntoEvents);
+ const settingDescription =
+ `aTestPerSurrogateKeyPress=${
+ aTestPerSurrogateKeyPress
+ }, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`;
+ const allowIllFormedUTF16 =
+ aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue;
+
+ check(`${aDescription}, ${settingDescription}a surrogate pair`, true, true, !aIsReadonly);
+ is(
+ aElement.textContent,
+ !aIsReadonly ? "\uD842\uDFB7" : "",
+ `${aDescription}, ${settingDescription}, The typed surrogate pair should've been inserted`
+ );
+ if (aIsReadonly) {
+ is(
+ getEventArrayData(events),
+ getEventArrayData(
+ aTestPerSurrogateKeyPress
+ ? (
+ allowIllFormedUTF16
+ ? [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842", charCode: 0xD842 },
+ { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ : [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 },
+ { type: "keypress", key: "", charCode: 0xDFB7 },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ )
+ : [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ ),
+ `${aDescription}, ${
+ settingDescription
+ }, Typing a surrogate pair in readonly editor should not cause input events`
+ );
+ } else {
+ is(
+ getEventArrayData(events),
+ getEventArrayData(
+ aTestPerSurrogateKeyPress
+ ? (
+ allowIllFormedUTF16
+ ? [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842", charCode: 0xD842 },
+ { type: "beforeinput", data: "\uD842", inputType: "insertText" },
+ { type: "input", data: "\uD842", inputType: "insertText" },
+ { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 },
+ { type: "beforeinput", data: "\uDFB7", inputType: "insertText" },
+ { type: "input", data: "\uDFB7", inputType: "insertText" },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ : [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 },
+ { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" },
+ { type: "input", data: "\uD842\uDFB7", inputType: "insertText" },
+ { type: "keypress", key: "", charCode: 0xDFB7 },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ )
+ : [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 },
+ { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" },
+ { type: "input", data: "\uD842\uDFB7", inputType: "insertText" },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ ),
+ `${aDescription}, ${
+ settingDescription
+ }, Typing a surrogate pair in editor should cause input events`
+ );
+ }
+ }
+ await test_typing_surrogate_pair(true, true);
+ await test_typing_surrogate_pair(true, false);
+ await test_typing_surrogate_pair(false);
+ }
+
+ await doTest(htmlEditor, "contenteditable=\"true\"", false, true, false);
+
+ const nsIEditor = SpecialPowers.Ci.nsIEditor;
+ var editor = SpecialPowers.wrap(window).docShell.editor;
+ var flags = editor.flags;
+ // readonly
+ editor.flags = flags | nsIEditor.eEditorReadonlyMask;
+ await doTest(htmlEditor, "readonly HTML editor", true, true, false);
+
+ // non-tabbable
+ editor.flags = flags & ~(nsIEditor.eEditorAllowInteraction);
+ await doTest(htmlEditor, "non-tabbable HTML editor", false, false, false);
+
+ // readonly and non-tabbable
+ editor.flags =
+ (flags | nsIEditor.eEditorReadonlyMask) &
+ ~(nsIEditor.eEditorAllowInteraction);
+ await doTest(htmlEditor, "readonly and non-tabbable HTML editor",
+ true, false, false);
+
+ // plaintext
+ editor.flags = flags | nsIEditor.eEditorPlaintextMask;
+ await doTest(htmlEditor, "HTML editor but plaintext mode", false, true, true);
+
+ // plaintext and non-tabbable
+ editor.flags = (flags | nsIEditor.eEditorPlaintextMask) &
+ ~(nsIEditor.eEditorAllowInteraction);
+ await doTest(htmlEditor, "non-tabbable HTML editor but plaintext mode",
+ false, false, true);
+
+
+ // readonly and plaintext
+ editor.flags = flags | nsIEditor.eEditorPlaintextMask |
+ nsIEditor.eEditorReadonlyMask;
+ await doTest(htmlEditor, "readonly HTML editor but plaintext mode",
+ true, true, true);
+
+ // readonly, plaintext and non-tabbable
+ editor.flags = (flags | nsIEditor.eEditorPlaintextMask |
+ nsIEditor.eEditorReadonlyMask) &
+ ~(nsIEditor.eEditorAllowInteraction);
+ await doTest(htmlEditor, "readonly and non-tabbable HTML editor but plaintext mode",
+ true, false, true);
+
+ SpecialPowers.removeSystemEventListener(window, "keypress", listener, true);
+ SpecialPowers.removeSystemEventListener(window, "keypress", listener, false);
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_htmleditor_tab_key_handling.html b/editor/libeditor/tests/test_htmleditor_tab_key_handling.html
new file mode 100644
index 0000000000..2d428611af
--- /dev/null
+++ b/editor/libeditor/tests/test_htmleditor_tab_key_handling.html
@@ -0,0 +1,112 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing indentation with `Tab` key</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ function initEditor(aEditingHostTag) {
+ const editor = document.createElement(aEditingHostTag);
+ editor.setAttribute("contenteditable", "true");
+ document.body.insertBefore(editor, document.body.firstChild);
+ editor.getBoundingClientRect();
+ const htmlEditor = SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
+ htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorAllowInteraction;
+ return editor;
+ }
+
+ (function test_tab_in_paragraph() {
+ const editor = initEditor("div");
+ editor.innerHTML = "<p>abc</p><p>def</p>";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.querySelector("p").firstChild, 1);
+ synthesizeKey("KEY_Tab");
+ is(
+ editor.innerHTML.replace(/&nbsp;/g, " "),
+ `<p>a bc</p><p>def</p>`,
+ "4 white-spaces should be inserted into the paragraph by Tab"
+ );
+
+ let blurred = false;
+ editor.addEventListener("blur", () => {
+ blurred = true;
+ }, {once: true});
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ is(
+ editor.innerHTML.replace(/&nbsp;/g, " "),
+ `<p>a bc</p><p>def</p>`,
+ "The spaces in the paragraph shouldn't be removed by Shift-Tab"
+ );
+ ok(blurred, "Shift-Tab should cause moving focus");
+ editor.remove();
+ })();
+
+ (function test_tab_in_body_text() {
+ const editor = initEditor("div");
+ editor.innerHTML = "abc";
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.firstChild, 1);
+ synthesizeKey("KEY_Tab");
+ is(
+ editor.innerHTML.replace(/&nbsp;/g, " "),
+ `a bc`,
+ "4 white-spaces should be inserted into the body text by Tab"
+ );
+ editor.remove();
+ })();
+
+ (function test_tab_in_li() {
+ const editor = initEditor("div");
+ for (const list of ["ol", "ul"]) {
+ editor.innerHTML = `abc<${list}><li>def</li><li>ghi</li><li>jkl</li></${list}>`;
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.querySelector("li + li").firstChild, 1);
+ synthesizeKey("KEY_Tab");
+ is(
+ editor.innerHTML,
+ `abc<${list}><li>def</li><${list}><li>ghi</li></${list}><li>jkl</li></${list}>`,
+ `The list item containing caret should be moved into new sub-<${list}> by Tab`
+ );
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ is(
+ editor.innerHTML,
+ `abc<${list}><li>def</li><li>ghi</li><li>jkl</li></${list}>`,
+ `The list item containing caret should be moved into parent <${list}> by Shift-Tab`
+ );
+ }
+ editor.remove();
+ })();
+
+ (function test_tab_in_nested_editing_host_in_li() {
+ const editor = initEditor("div");
+ editor.innerHTML = `abc<ul><li>def</li><li><span contenteditable="false">g<span contenteditable="true">h</span>i</span></li><li>jkl</li></ul>`;
+ editor.getBoundingClientRect();
+ editor.focus();
+ getSelection().collapse(editor.querySelector("span[contenteditable=true]").firstChild, 1);
+ synthesizeKey("KEY_Tab");
+ is(
+ editor.innerHTML,
+ `abc<ul><li>def</li><li><span contenteditable="false">g<span contenteditable="true">h</span>i</span></li><li>jkl</li></ul>`,
+ `The list item containing caret should be modified by Tab`
+ );
+ editor.remove();
+ })();
+
+ // TODO: Add table cell cases.
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_htmleditor_toggle_text_direction.html b/editor/libeditor/tests/test_htmleditor_toggle_text_direction.html
new file mode 100644
index 0000000000..9dfa80f113
--- /dev/null
+++ b/editor/libeditor/tests/test_htmleditor_toggle_text_direction.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Text direction switch target of HTMLEditor</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ await (async function test_in_contenteditable() {
+ document.body.innerHTML = "<div><div contenteditable>editable text</div></div>";
+ const editingHost = document.querySelector("div[contenteditable]");
+ editingHost.focus();
+ SpecialPowers.doCommand(window, "cmd_switchTextDirection");
+ is(
+ editingHost.getAttribute("dir"),
+ "rtl",
+ "test_in_contenteditable: dir attr of the editing host should be set"
+ );
+ is(
+ editingHost.parentElement.getAttribute("dir"),
+ null,
+ "test_in_contenteditable: dir attr of the parent div of the editing host should not be set"
+ );
+ is(
+ document.body.getAttribute("dir"),
+ null,
+ "test_in_contenteditable: dir attr of the <body> should not be set",
+ );
+ is(
+ document.documentElement.getAttribute("dir"),
+ null,
+ "test_in_contenteditable: dir attr of the <html> should not be set",
+ );
+ })();
+
+ await (async function test_in_designMode() {
+ document.body.innerHTML = "<div>abc</div>";
+ document.designMode = "on";
+ getSelection().collapse(document.querySelector("div").firstChild, 0);
+ SpecialPowers.doCommand(window, "cmd_switchTextDirection");
+ is(
+ document.querySelector("div").getAttribute("dir"),
+ null,
+ "test_in_designMode: dir attr of the <div> should not be set",
+ );
+ is(
+ document.body.getAttribute("dir"),
+ "rtl",
+ "test_in_designMode: dir attr of the <body> should be set",
+ );
+ is(
+ document.documentElement.getAttribute("dir"),
+ null,
+ "test_in_designMode: dir attr of the <html> should not be set",
+ );
+ document.designMode = "off";
+ document.body.removeAttribute("dir");
+ document.body.innerHTML = "";
+ document.documentElement.removeAttribute("dir");
+ })();
+
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_initial_selection_and_caret_of_designMode.html b/editor/libeditor/tests/test_initial_selection_and_caret_of_designMode.html
new file mode 100644
index 0000000000..5c08370321
--- /dev/null
+++ b/editor/libeditor/tests/test_initial_selection_and_caret_of_designMode.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<head>
+ <title>Test for initial selection and caret at turning on the designMode with user interaction</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<p>foo</p>
+<button onclick="document.designMode = 'on'">click</button>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ synthesizeMouseAtCenter(document.querySelector("button"), {});
+
+ is(
+ document.activeElement,
+ document.body,
+ "The <body> element should be active"
+ );
+ is(
+ getSelection().focusNode,
+ document.body.firstChild.firstChild,
+ "The focus node should be the text node in the first paragraph"
+ );
+ is(
+ getSelection().anchorNode,
+ document.body.firstChild.firstChild,
+ "The anchor node should be the text node in the first paragraph"
+ );
+ is(
+ getSelection().focusOffset,
+ 0,
+ "The focus offset should be 0"
+ );
+ is(
+ getSelection().anchorOffset,
+ 0,
+ "The anchor offset should be 0"
+ );
+
+ function getHTMLEditor() {
+ return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window);
+ }
+ ok(
+ getHTMLEditor().selectionController.getCaretEnabled(),
+ "The caret should be enabled"
+ );
+ ok(
+ getHTMLEditor().selectionController.caretVisible,
+ "The caret should be visible"
+ );
+
+ document.designMode = "off";
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_inlineTableEditing.html b/editor/libeditor/tests/test_inlineTableEditing.html
new file mode 100644
index 0000000000..411b5f6510
--- /dev/null
+++ b/editor/libeditor/tests/test_inlineTableEditing.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div contenteditable></div>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ const editableInnerHTML =
+`<table>
+ <tr><td>ABCDEFG</td><td>HIJKLMN</td></tr>
+ <tr><td>ABCDEFG</td><td>HIJKLMN</td></tr>
+ <tr><td>ABCDEFG</td><td>HIJKLMN</td></tr>
+</table>`;
+
+ document.execCommand("enableObjectResizing", false, "false");
+ document.execCommand("enableInlineTableEditing", false, "true");
+
+ function doTest(aDescription, aTable) {
+ synthesizeMouseAtCenter(aTable, {});
+ isnot(
+ aTable.getAttribute("_moz_resizing"),
+ "true",
+ `${aDescription}: _moz_resizing attribute shouldn't be true without object resizing`
+ );
+
+ const tr2 = aTable.querySelector("tr + tr");
+ synthesizeMouse(tr2, 0, tr2.clientHeight / 2, {});
+ ok(
+ !tr2.isConnected,
+ `${aDescription}: The second <tr> element should've been removed by a click`
+ );
+ }
+
+ const editingHost = document.querySelector("div[contenteditable]");
+ editingHost.innerHTML = editableInnerHTML;
+ doTest("Testing in Light DOM", editingHost.querySelector("table"));
+
+ editingHost.remove();
+
+ const shadowHost = document.createElement("div");
+ document.body.insertBefore(shadowHost, document.body.firstChild);
+ const shadowRoot = shadowHost.attachShadow({mode: "open"});
+ shadowRoot.appendChild(document.createElement("div"));
+ shadowRoot.firstChild.setAttribute("contenteditable", "");
+ shadowRoot.firstChild.innerHTML = editableInnerHTML;
+ doTest("Testing in Shadow DOM", shadowRoot.firstChild.querySelector("table"));
+
+ shadowHost.remove();
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_inline_style_cache.html b/editor/libeditor/tests/test_inline_style_cache.html
new file mode 100644
index 0000000000..7a4f0f9172
--- /dev/null
+++ b/editor/libeditor/tests/test_inline_style_cache.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Tests for inline style cache</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="editor" contenteditable></div>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var editor = document.getElementById("editor");
+ editor.focus();
+
+ document.execCommand("defaultParagraphSeparator", false, "div");
+
+ var selection = window.getSelection();
+
+ // #01-01 Typing something after setting some styles should insert some nodes to insert text.
+ editor.innerHTML = "beforeafter";
+ selection.collapse(editor.firstChild, "before".length);
+ document.execCommand("bold");
+ document.execCommand("italic");
+ document.execCommand("strikethrough");
+ sendString("test");
+
+ is(editor.innerHTML, "before<b><i><strike>test</strike></i></b>after",
+ "#01-01 At typing something after setting some styles, should cause inserting some nodes to apply the style");
+
+ // #01-02 Typing something after removing some characters after setting some styles should work as without removing some character.
+ editor.innerHTML = "beforeafter";
+ selection.collapse(editor.firstChild, "before".length);
+ document.execCommand("bold");
+ document.execCommand("italic");
+ document.execCommand("strikethrough");
+ synthesizeKey("KEY_Delete");
+ sendString("test");
+
+ todo_is(
+ editor.innerHTML,
+ "beforetestfter",
+ "#01-02-1 At typing something after Delete after setting style, the style should not be preserved for compatibility with the other browsers"
+ );
+ is(
+ editor.innerHTML,
+ "before<b><i><strike>test</strike></i></b>fter",
+ "#01-02-1 At typing something after Delete after setting style, the style should not be preserved, but it's okay to do it for backward compatibility"
+ );
+
+ editor.innerHTML = "beforeafter";
+ selection.collapse(editor.firstChild, "before".length);
+ document.execCommand("bold");
+ document.execCommand("italic");
+ document.execCommand("strikethrough");
+ synthesizeKey("KEY_Backspace");
+ sendString("test");
+
+ is(editor.innerHTML, "befor<b><i><strike>test</strike></i></b>after",
+ "#01-02-2 At typing something after Backspace after setting style, should cause inserting some nodes to apply the style");
+
+ // #03-01 Replacing in <b style="font-weight: normal;"> shouldn't cause new <b>.
+ editor.innerHTML = "<b style=\"font-weight: normal;\">beforeselectionafter</b>";
+ selection.collapse(editor.firstChild.firstChild, "before".length);
+ selection.extend(editor.firstChild.firstChild, "beforeselection".length);
+ sendString("test");
+
+ is(editor.innerHTML, "<b style=\"font-weight: normal;\">beforetestafter</b>",
+ "#03-01 Replacing text in styled inline elements should respect the styles");
+
+ // #03-02 Typing something after removing selected text in <b style="font-weight: normal;"> shouldn't cause new <b>.
+ editor.innerHTML = "<b style=\"font-weight: normal;\">beforeselectionafter</b>";
+ selection.collapse(editor.firstChild.firstChild, "before".length);
+ selection.extend(editor.firstChild.firstChild, "beforeselection".length);
+ synthesizeKey("KEY_Backspace");
+ sendString("test");
+
+ is(editor.innerHTML, "<b style=\"font-weight: normal;\">beforetestafter</b>",
+ "#03-02 Inserting text after removing text in styled inline elements should respect the styles");
+
+ // #03-03 Typing something after typing Enter at selected text in <b style="font-weight: normal;"> shouldn't cause new <b>.
+ editor.innerHTML = "<b style=\"font-weight: normal;\">beforeselectionafter</b>";
+ selection.collapse(editor.firstChild.firstChild, "before".length);
+ selection.extend(editor.firstChild.firstChild, "beforeselection".length);
+ synthesizeKey("KEY_Enter");
+ sendString("test");
+
+ is(editor.innerHTML, "<div><b style=\"font-weight: normal;\">before</b></div><div><b style=\"font-weight: normal;\">testafter</b></div>",
+ "#03-03-1 Inserting text after typing Enter at selected text in styled inline elements should respect the styles");
+
+ editor.innerHTML = "<p><b style=\"font-weight: normal;\">beforeselectionafter</b></p>";
+ selection.collapse(editor.firstChild.firstChild.firstChild, "before".length);
+ selection.extend(editor.firstChild.firstChild.firstChild, "beforeselection".length);
+ synthesizeKey("KEY_Enter");
+ sendString("test");
+
+ is(editor.innerHTML, "<p><b style=\"font-weight: normal;\">before</b></p><p><b style=\"font-weight: normal;\">testafter</b></p>",
+ "#03-03-2 Inserting text after typing Enter at selected text in styled inline elements should respect the styles");
+
+ // #04-01 Replacing in some styled inline elements shouldn't cause new same elements.
+ editor.innerHTML = "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforeselectionafter</b></i></strike>";
+ selection.collapse(editor.firstChild.firstChild.firstChild.firstChild, "before".length);
+ selection.extend(editor.firstChild.firstChild.firstChild.firstChild, "beforeselection".length);
+ sendString("test");
+
+ is(editor.innerHTML, "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforetestafter</b></i></strike>",
+ "#04-01 Replacing text in styled inline elements should respect the styles");
+
+ // #04-02 Typing something after removing selected text in some styled inline elements shouldn't cause new same elements.
+ editor.innerHTML = "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforeselectionafter</b>";
+ selection.collapse(editor.firstChild.firstChild.firstChild.firstChild, "before".length);
+ selection.extend(editor.firstChild.firstChild.firstChild.firstChild, "beforeselection".length);
+ synthesizeKey("KEY_Backspace");
+ sendString("test");
+
+ is(editor.innerHTML, "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforetestafter</b></i></strike>",
+ "#04-02 Inserting text after removing text in styled inline elements should respect the styles");
+
+ // #04-03 Typing something after typing Enter at selected text in some styled inline elements shouldn't cause new same elements.
+ editor.innerHTML = "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforeselectionafter</b>";
+ selection.collapse(editor.firstChild.firstChild.firstChild.firstChild, "before".length);
+ selection.extend(editor.firstChild.firstChild.firstChild.firstChild, "beforeselection".length);
+ synthesizeKey("KEY_Enter");
+ sendString("test");
+
+ is(editor.innerHTML, "<div><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">before</b></i></strike></div><div><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">testafter</b></i></strike></div>",
+ "#04-03-1 Inserting text after typing Enter at selected text in styled inline elements should respect the styles");
+
+ editor.innerHTML = "<p><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforeselectionafter</b></p>";
+ selection.collapse(editor.firstChild.firstChild.firstChild.firstChild.firstChild, "before".length);
+ selection.extend(editor.firstChild.firstChild.firstChild.firstChild.firstChild, "beforeselection".length);
+ synthesizeKey("KEY_Enter");
+ sendString("test");
+
+ is(editor.innerHTML, "<p><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">before</b></i></strike></p><p><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">testafter</b></i></strike></p>",
+ "#04-03-2 Inserting text after typing Enter at selected text in styled inline elements should respect the styles");
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_insertHTML_starting_with_multiple_comment_nodes.html b/editor/libeditor/tests/test_insertHTML_starting_with_multiple_comment_nodes.html
new file mode 100644
index 0000000000..c3b440659f
--- /dev/null
+++ b/editor/libeditor/tests/test_insertHTML_starting_with_multiple_comment_nodes.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Insert HTML containing comment nodes</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ const editor = document.querySelector("div[contenteditable]");
+ getSelection().collapse(editor.querySelector("li").firstChild, 1);
+ document.execCommand("insertHTML", false, "<!-- 1 --><!-- 2 -->b");
+ is(
+ editor.innerHTML,
+ "<ul><li>a<!-- 1 --><!-- 2 -->bc</li></ul>",
+ "The HTML fragment should be inserted as-is"
+ );
+ document.execCommand("undo");
+ is(
+ editor.innerHTML,
+ "<ul><li>ac</li></ul>",
+ "Undoing should work as expected"
+ );
+ document.execCommand("redo");
+ is(
+ editor.innerHTML,
+ "<ul><li>a<!-- 1 --><!-- 2 -->bc</li></ul>",
+ "Redoing should work as expected"
+ );
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body>
+<div contenteditable><ul><li>ac</li></ul></div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_insertParagraph_in_h2_and_li.html b/editor/libeditor/tests/test_insertParagraph_in_h2_and_li.html
new file mode 100644
index 0000000000..0af77108c6
--- /dev/null
+++ b/editor/libeditor/tests/test_insertParagraph_in_h2_and_li.html
@@ -0,0 +1,167 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=449243
+-->
+<head>
+ <title>Test for Bug 449243</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=449243">Mozilla Bug 449243</a>
+<p id="display"></p>
+<div id="content" contenteditable>
+ <h2>This is a title</h2>
+ <ul>
+ <li>this is a</li>
+ <li>bullet list</li>
+ </ul>
+ <ol>
+ <li>this is a</li>
+ <li>numbered list</li>
+ </ol>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 449243 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+const CARET_BEGIN = 0;
+const CARET_MIDDLE = 1;
+const CARET_END = 2;
+
+function split(element, caretPos, nbKeyPresses) {
+ // put the caret on the requested position
+ const offset = (() => {
+ switch (caretPos) {
+ case CARET_BEGIN:
+ return 0;
+ case CARET_MIDDLE:
+ return Math.floor(element.textContent.length / 2);
+ case CARET_END:
+ return element.textContent.length;
+ }
+ return 0;
+ })();
+ getSelection().collapse(element.firstChild, offset);
+
+ // simulates a [Return] keypress
+ for (let i = 0; i < nbKeyPresses; i++) {
+ synthesizeKey("KEY_Enter");
+ }
+}
+
+function undo(nbKeyPresses) {
+ for (let i = 0; i < nbKeyPresses; i++) {
+ document.execCommand("Undo");
+ }
+}
+
+function getNewElement(element) {
+ return element.nextElementSibling;
+}
+
+function runTests() {
+ const content = document.querySelector("[contenteditable]");
+ const header = content.querySelector("h2");
+ const ulItem = content.querySelector("ul > li:last-child");
+ const olItem = content.querySelector("ol > li:last-child");
+ content.focus();
+
+ // beginning of selection: split current node
+ split(header, CARET_BEGIN, 1);
+ is(
+ getNewElement(header)?.nodeName,
+ header.nodeName,
+ "Pressing [Return] at the beginning of a header " +
+ "should create another header."
+ );
+ split(ulItem, CARET_BEGIN, 2);
+ is(
+ getNewElement(ulItem)?.nodeName,
+ ulItem.nodeName,
+ "Pressing [Return] at the beginning of an unordered list item " +
+ "should create another list item."
+ );
+ split(olItem, CARET_BEGIN, 2);
+ is(
+ getNewElement(olItem)?.nodeName,
+ olItem.nodeName,
+ "Pressing [Return] at the beginning of an ordered list item " +
+ "should create another list item."
+ );
+ undo(3);
+
+ // middle of selection: split current node
+ split(header, CARET_MIDDLE, 1);
+ is(
+ getNewElement(header)?.nodeName,
+ header.nodeName,
+ "Pressing [Return] at the middle of a header " +
+ "should create another header."
+ );
+ split(ulItem, CARET_MIDDLE, 2);
+ is(
+ getNewElement(ulItem)?.nodeName,
+ ulItem.nodeName,
+ "Pressing [Return] at the middle of an unordered list item " +
+ "should create another list item."
+ );
+ split(olItem, CARET_MIDDLE, 2);
+ is(
+ getNewElement(olItem)?.nodeName,
+ olItem.nodeName,
+ "Pressing [Return] at the middle of an ordered list item " +
+ "should create another list item."
+ );
+ undo(3);
+
+ // end of selection: create a new div/paragraph
+ function testEndOfSelection(expected, defaultParagraphSeparator) {
+ split(header, CARET_END, 1);
+ is(
+ content.querySelector("h2+*")?.nodeName,
+ expected.toUpperCase(),
+ `Pressing [Return] at the end of a header should create a new <${
+ expected
+ }> (defaultParagraphSeparator: <${defaultParagraphSeparator}>)`
+ );
+ split(ulItem, CARET_END, 2);
+ is(
+ content.querySelector("ul+*")?.nodeName,
+ expected.toUpperCase(),
+ `Pressing [Return] twice at the end of an unordered list item should create a new <${
+ expected
+ }> (defaultParagraphSeparator: <${defaultParagraphSeparator}>)`
+ );
+ split(olItem, CARET_END, 2);
+ is(
+ content.querySelector("ol+*")?.nodeName,
+ expected.toUpperCase(),
+ `Pressing [Return] twice at the end of an ordered list item should create a new <${
+ expected
+ }> (defaultParagraphSeparator: <${defaultParagraphSeparator}>)`
+ );
+ undo(3);
+ }
+
+ document.execCommand("defaultParagraphSeparator", false, "div");
+ testEndOfSelection("div", "div");
+ document.execCommand("defaultParagraphSeparator", false, "p");
+ testEndOfSelection("p", "p");
+ document.execCommand("defaultParagraphSeparator", false, "br");
+ testEndOfSelection("p", "br");
+
+ // done
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_insertParagraph_in_inline_editing_host.html b/editor/libeditor/tests/test_insertParagraph_in_inline_editing_host.html
new file mode 100644
index 0000000000..ae276fd3ad
--- /dev/null
+++ b/editor/libeditor/tests/test_insertParagraph_in_inline_editing_host.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test "insertParagraph" command in inline editing host</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<span contenteditable>foobar</span>
+<hr>
+<p><span contenteditable>foobar</span></p>
+<hr>
+<div><span contenteditable>foobar</span></div>
+<hr>
+<div contenteditable><p contenteditable="false"><span contenteditable>foobar</span></p></div>
+
+<p id="display">
+</p>
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ var selection = document.getSelection();
+ var editors = document.querySelectorAll("span[contenteditable]");
+
+ var editor = editors.item(0);
+ editor.focus();
+ selection.collapse(editor.firstChild, 3);
+ document.execCommand("insertParagraph", false);
+ is(editor.innerHTML, "foo<br>bar",
+ "insertParagraph should insert <br> element (inline editing host is in <body>)");
+
+ editor = editors.item(1);
+ editor.focus();
+ selection.collapse(editor.firstChild, 3);
+ document.execCommand("insertParagraph", false);
+ is(editor.parentNode.innerHTML, "<span contenteditable=\"\">foo<br>bar</span>",
+ "insertParagraph should insert <br> element (inline editing host is in <p>)");
+
+ editor = editors.item(2);
+ editor.focus();
+ selection.collapse(editor.firstChild, 3);
+ document.execCommand("insertParagraph", false);
+ is(editor.parentNode.innerHTML, "<span contenteditable=\"\">foo<br>bar</span>",
+ "insertParagraph should insert <br> element (inline editing host is in <div>)");
+
+ editor = editors.item(3);
+ editor.focus();
+ selection.collapse(editor.firstChild, 3);
+ document.execCommand("insertParagraph", false);
+ is(editor.parentNode.parentNode.innerHTML, "<p contenteditable=\"false\"><span contenteditable=\"\">foo<br>bar</span></p>",
+ "insertParagraph should insert <br> element (inline editing host is in <p contenteditable=\"false\"> in <div contenteditable>)");
+
+ SimpleTest.finish();
+});
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_insertText_around_text_node_in_plaintext_mode.html b/editor/libeditor/tests/test_insertText_around_text_node_in_plaintext_mode.html
new file mode 100644
index 0000000000..1acf319456
--- /dev/null
+++ b/editor/libeditor/tests/test_insertText_around_text_node_in_plaintext_mode.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test insertText when caret is around a text node</title>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ function getHTMLEditor() {
+ let editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ if (!editingSession) {
+ return null;
+ }
+ let editor = editingSession.getEditorForWindow(window);
+ if (!editor) {
+ return null;
+ }
+ return editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor);
+ }
+
+ document.designMode = "on";
+ document.body.focus();
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
+ const editor = getHTMLEditor();
+ editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+
+ (function test_collapsed_before_first_text_child() {
+ document.body.innerHTML = "bc<br>";
+ getSelection().collapse(document.body, 0);
+ document.execCommand("insertText", false, "a");
+ is(
+ document.body.firstChild.data,
+ "abc",
+ "test_collapsed_before_first_text_child: Text should be inserted into start of the first text child"
+ );
+ })();
+
+ (function test_collapsed_before_first_text_child() {
+ document.body.innerHTML = "bc<br>";
+ getSelection().collapse(document.body, 0);
+ document.execCommand("insertText", false, "a");
+ is(
+ document.body.firstChild.data,
+ "abc",
+ "test_collapsed_before_first_text_child: Text should be inserted into start of the first text child"
+ );
+ })();
+
+ (function test_collapsed_after_last_text_child() {
+ document.body.innerHTML = "ab";
+ getSelection().collapse(document.body, 1);
+ document.execCommand("insertText", false, "c");
+ is(
+ document.body.firstChild.data,
+ "abc",
+ "test_collapsed_after_last_text_child: Text should be inserted into end of the last text child"
+ );
+ })();
+
+ (function test_collapsed_after_text_child() {
+ document.body.innerHTML = "ab<br>";
+ getSelection().collapse(document.body, 1);
+ document.execCommand("insertText", false, "c");
+ is(
+ document.body.firstChild.data,
+ "abc",
+ "test_collapsed_after_text_child: Text should be inserted into end of the previous text child"
+ );
+ })();
+
+ (function test_collapsed_at_text_child() {
+ document.body.innerHTML = "<img>bc";
+ getSelection().collapse(document.body, 1);
+ document.execCommand("insertText", false, "a");
+ is(
+ document.body.firstChild.nextSibling.data,
+ "abc",
+ "test_collapsed_at_text_child: Text should be inserted into start of the text child"
+ );
+ })();
+
+ document.designMode = "off";
+ SimpleTest.finish();
+});
+</script>
+<body></body>
+</html>
diff --git a/editor/libeditor/tests/test_join_split_node_direction_change_command.html b/editor/libeditor/tests/test_join_split_node_direction_change_command.html
new file mode 100644
index 0000000000..a68396b700
--- /dev/null
+++ b/editor/libeditor/tests/test_join_split_node_direction_change_command.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ let iframe = document.querySelector("iframe");
+ async function resetIframe() {
+ iframe?.remove();
+ iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ iframe.srcdoc = "<body></body>";
+ if (iframe.contentDocument?.readyState != "complete") {
+ await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true}));
+ }
+ iframe.contentWindow.focus();
+ }
+
+ await resetIframe();
+ iframe.contentDocument.body.innerHTML = "<div contenteditable><br></div>";
+ ok(
+ iframe.contentDocument.queryCommandSupported("enableCompatibleJoinSplitDirection"),
+ "command should be supported"
+ );
+ ok(
+ iframe.contentDocument.queryCommandEnabled("enableCompatibleJoinSplitDirection"),
+ "command should be enabled"
+ );
+ ok(
+ iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"),
+ "command state should be true"
+ );
+ is(
+ iframe.contentDocument.queryCommandValue("enableCompatibleJoinSplitDirection"),
+ "",
+ "command value should be empty string"
+ );
+ ok(
+ !iframe.contentDocument.execCommand("enableCompatibleJoinSplitDirection", false, "false"),
+ "command to disable it should return false"
+ );
+ ok(
+ iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"),
+ "command state should be true even after executing the command to disable it"
+ );
+
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body></body>
+</html>
diff --git a/editor/libeditor/tests/test_keypress_untrusted_event.html b/editor/libeditor/tests/test_keypress_untrusted_event.html
new file mode 100644
index 0000000000..e8151b53e6
--- /dev/null
+++ b/editor/libeditor/tests/test_keypress_untrusted_event.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=622245
+-->
+<head>
+ <title>Test for untrusted keypress events</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622245">Mozilla Bug 622245</a>
+<p id="display"></p>
+<div id="content">
+<input id="i"><br>
+<textarea id="t"></textarea><br>
+<div id="d" contenteditable style="min-height: 1em;"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 674770 **/
+SimpleTest.waitForExplicitFinish();
+
+var input = document.getElementById("i");
+var textarea = document.getElementById("t");
+var div = document.getElementById("d");
+
+addLoadEvent(function() {
+ input.focus();
+
+ SimpleTest.executeSoon(function() {
+ input.addEventListener("keypress",
+ function(aEvent) {
+ is(aEvent.target, input,
+ "The keypress event target isn't the input element");
+
+ SimpleTest.executeSoon(function() {
+ is(input.value, "",
+ "Did keypress event cause modifying the input element?");
+ textarea.focus();
+ SimpleTest.executeSoon(runTextareaTest);
+ });
+ }, {once: true});
+ var keypress = new KeyboardEvent("keypress", {
+ bubbles: true,
+ cancelable: true,
+ view: document.defaultView,
+ keyCode: 0,
+ charCode:"a".charCodeAt(0),
+ });
+ input.dispatchEvent(keypress);
+ });
+});
+
+function runTextareaTest() {
+ textarea.addEventListener("keypress",
+ function(aEvent) {
+ is(aEvent.target, textarea,
+ "The keypress event target isn't the textarea element");
+
+ SimpleTest.executeSoon(function() {
+ is(textarea.value, "",
+ "Did keypress event cause modifying the textarea element?");
+ div.focus();
+ SimpleTest.executeSoon(runContentediableTest);
+ });
+ }, {once: true});
+ var keypress = new KeyboardEvent("keypress", {
+ bubbles: true,
+ cancelable: true,
+ view: document.defaultView,
+ keyCode: 0,
+ charCode:"b".charCodeAt(0),
+ });
+ textarea.dispatchEvent(keypress);
+}
+
+function runContentediableTest() {
+ div.addEventListener("keypress",
+ function(aEvent) {
+ is(aEvent.target, div,
+ "The keypress event target isn't the div element");
+
+ SimpleTest.executeSoon(function() {
+ is(div.innerHTML, "",
+ "Did keypress event cause modifying the div element?");
+
+ SimpleTest.finish();
+ });
+ }, {once: true});
+ var keypress = new KeyboardEvent("keypress", {
+ bubbles: true,
+ cancelable: true,
+ view: document.defaultView,
+ keyCode: 0,
+ charCode:"c".charCodeAt(0),
+ });
+ div.dispatchEvent(keypress);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_label_contenteditable.html b/editor/libeditor/tests/test_label_contenteditable.html
new file mode 100644
index 0000000000..43bf9d4292
--- /dev/null
+++ b/editor/libeditor/tests/test_label_contenteditable.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<label style="display: block" contenteditable>
+ Foo
+</label>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let label = document.querySelector("label");
+ synthesizeMouseAtCenter(label, {});
+ is(document.activeElement, label, "Label should get focus");
+ synthesizeKey("x", {});
+ is(label.innerText.trim(), "Foox", "Should not select the whole label");
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_middle_click_paste.html b/editor/libeditor/tests/test_middle_click_paste.html
new file mode 100644
index 0000000000..eaa918c194
--- /dev/null
+++ b/editor/libeditor/tests/test_middle_click_paste.html
@@ -0,0 +1,680 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for paste with middle button click</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="container"></div>
+
+<textarea id="toCopyPlaintext" style="display: none;"></textarea>
+<iframe id="toCopyHTMLContent" srcdoc="<body></body>" style="display: none;"></iframe>
+
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+
+// TODO: This file should test complicated cases too.
+// E.g., pasting into existing content, e.g., pasting invalid child
+// element for the parent elements at insertion point.
+
+async function copyPlaintext(aText) {
+ return new Promise(resolve => {
+ SimpleTest.waitForClipboard(aText,
+ () => {
+ let element = document.getElementById("toCopyPlaintext");
+ element.style.display = "block";
+ element.focus();
+ element.value = aText;
+ synthesizeKey("a", {accelKey: true});
+ synthesizeKey("c", {accelKey: true});
+ },
+ () => {
+ ok(true, `Succeeded to copy "${aText}" to clipboard`);
+ let element = document.getElementById("toCopyPlaintext");
+ element.style.display = "none";
+ resolve();
+ },
+ () => {
+ ok(false, `Failed to copy "${aText}" to clipboard`);
+ SimpleTest.finish();
+ });
+ });
+}
+
+async function copyHTMLContent(aInnerHTML) {
+ let iframe = document.getElementById("toCopyHTMLContent");
+ iframe.style.display = "block";
+ iframe.contentDocument.body.scrollTop;
+ iframe.contentDocument.body.innerHTML = aInnerHTML;
+ iframe.contentWindow.focus();
+ iframe.contentWindow.getSelection().selectAllChildren(iframe.contentDocument.body);
+ return new Promise(resolve => {
+ SimpleTest.waitForClipboard(
+ () => { return true; },
+ () => {
+ synthesizeKey("c", {accelKey: true}, iframe.contentWindow);
+ },
+ () => {
+ ok(true, `Succeeded to copy "${aInnerHTML}" to clipboard as HTML`);
+ iframe.style.display = "none";
+ resolve();
+ },
+ () => {
+ ok(false, `Failed to copy "${aInnerHTML}" to clipboard`);
+ SimpleTest.finish();
+ },
+ "text/html");
+ });
+}
+
+function checkInputEvent(aEvent, aInputType, aData, aDataTransfer, aTargetRanges, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, aEvent.type === "beforeinput",
+ `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, aInputType,
+ `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`);
+ is(aEvent.data, aData,
+ `data of "${aEvent.type}" event should be ${aData} ${aDescription}`);
+ if (aDataTransfer === null) {
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ } else {
+ for (let dataTransfer of aDataTransfer) {
+ is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data,
+ `dataTransfer of "${aEvent.type}" should have "${dataTransfer.data}" whose type is "${dataTransfer.type}" ${aDescription}`);
+ }
+ }
+ let targetRanges = aEvent.getTargetRanges();
+ if (aTargetRanges.length === 0) {
+ is(targetRanges.length, 0,
+ `getTargetRange() of "${aEvent.type}" event should return empty array: ${aDescription}`);
+ } else {
+ is(targetRanges.length, aTargetRanges.length,
+ `getTargetRange() of "${aEvent.type}" event should return static range array: ${aDescription}`);
+ if (targetRanges.length == aTargetRanges.length) {
+ for (let i = 0; i < targetRanges.length; i++) {
+ is(targetRanges[i].startContainer, aTargetRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ is(targetRanges[i].startOffset, aTargetRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ is(targetRanges[i].endContainer, aTargetRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ is(targetRanges[i].endOffset, aTargetRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ }
+ }
+ }
+}
+
+async function doTextareaTests(aTextarea) {
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ aTextarea.addEventListener("beforeinput", onBeforeInput);
+ aTextarea.addEventListener("input", onInput);
+
+ await copyPlaintext("abc\ndef\nghi");
+ aTextarea.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
+ is(aTextarea.value,
+ "> abc\n> def\n> ghi\n\n",
+ "Pasted each line should start with \"> \"");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired #1');
+ checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\nghi", null, [], "#1");
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired #1');
+ checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\nghi", null, [], "#1");
+ aTextarea.value = "";
+
+ await copyPlaintext("> abc\n> def\n> ghi");
+ aTextarea.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
+ is(aTextarea.value,
+ ">> abc\n>> def\n>> ghi\n\n",
+ "Pasted each line should be start with \">> \" when already quoted one level");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired #2');
+ checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n> ghi", null, [], "#2");
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired #2');
+ checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n> ghi", null, [], "#2");
+ aTextarea.value = "";
+
+ await copyPlaintext("> abc\n> def\n\nghi");
+ aTextarea.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
+ is(aTextarea.value,
+ ">> abc\n>> def\n> \n> ghi\n\n",
+ "Pasted each line should be start with \">> \" when already quoted one level");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired #3');
+ checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n\nghi", null, [], "#3");
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired #3');
+ checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n\nghi", null, [], "#3");
+ aTextarea.value = "";
+
+ await copyPlaintext("abc\ndef\n\n");
+ aTextarea.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
+ is(aTextarea.value,
+ "> abc\n> def\n> \n",
+ "If pasted text ends with \"\\n\", only the last line should not started with \">\"");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired #4');
+ checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, [], "#4");
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired #4');
+ checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, [], "#4");
+ aTextarea.value = "";
+
+ await copyPlaintext("abc\ndef\n\n");
+ aTextarea.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true});
+ aTextarea.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
+ is(aTextarea.value, "",
+ 'Pasting as quote should have been canceled if "paste" event was canceled');
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired since "paste" event was canceled #5');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired since "paste" was canceled #5');
+ aTextarea.value = "";
+
+ await copyPlaintext("abc\ndef\n\n");
+ aTextarea.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true});
+ aTextarea.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
+ is(aTextarea.value, "",
+ 'Pasting as quote should have been canceled if "beforeinput" event was canceled');
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired #5');
+ checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, [], "#6");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired since "beforeinput" was canceled #6');
+ aTextarea.value = "";
+
+ let pasteEventCount = 0;
+ function pasteEventLogger(event) {
+ pasteEventCount++;
+ }
+ aTextarea.addEventListener("paste", pasteEventLogger);
+
+ await copyPlaintext("abc");
+ aTextarea.focus();
+ document.body.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true});
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1});
+ is(aTextarea.value, "abc",
+ "If 'click' event is consumed at capturing phase of the <body>, paste should not be canceled");
+ is(pasteEventCount, 1,
+ "If 'click' event is consumed at capturing phase of the <body>, 'paste' event should still be fired");
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when the "click" event is canceled');
+ checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'when the "click" event is canceled');
+ is(inputEvents.length, 1,
+ '"input" event should be fired when the "click" event is canceled');
+ checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, [], 'when the "click" event is canceled');
+ aTextarea.value = "";
+
+ await copyPlaintext("abc");
+ aTextarea.focus();
+ aTextarea.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1});
+ is(aTextarea.value, "abc",
+ "Even if 'mouseup' event is consumed, paste should be done");
+ is(pasteEventCount, 1,
+ "Even if 'mouseup' event is consumed, 'paste' event should be fired once");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired even if "mouseup" event is canceled');
+ checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'even if "mouseup" event is canceled');
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired even if "mouseup" event is canceled');
+ checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, [], 'even if "mouseup" event is canceled');
+ aTextarea.value = "";
+
+ await copyPlaintext("abc");
+ aTextarea.focus();
+ aTextarea.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1});
+ is(aTextarea.value, "abc",
+ "If 'click' event handler is added to the <textarea>, paste should not be canceled");
+ is(pasteEventCount, 1,
+ "If 'click' event handler is added to the <textarea>, 'paste' event should be fired once");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired even if "click" event is canceled in bubbling phase');
+ checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'even if "click" event is canceled in bubbling phase');
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired even if "click" event is canceled in bubbling phase');
+ checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, [], 'even if "click" event is canceled in bubbling phase');
+ aTextarea.value = "";
+
+ await copyPlaintext("abc");
+ aTextarea.focus();
+ aTextarea.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1});
+ is(aTextarea.value, "",
+ "If 'auxclick' event is consumed, paste should be canceled");
+ is(pasteEventCount, 0,
+ "If 'auxclick' event is consumed, 'paste' event should not be fired once");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired if "auxclick" event is canceled');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired if "auxclick" event is canceled');
+ aTextarea.value = "";
+
+ await copyPlaintext("abc");
+ aTextarea.focus();
+ aTextarea.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1});
+ is(aTextarea.value, "",
+ "If 'paste' event is consumed, paste should be canceled");
+ is(pasteEventCount, 1,
+ 'One "paste" event should be fired for making it possible to consume');
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired if "paste" event is canceled');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired if "paste" event is canceled');
+ aTextarea.value = "";
+
+ await copyPlaintext("abc");
+ aTextarea.focus();
+ aTextarea.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aTextarea, {button: 1});
+ is(aTextarea.value, "",
+ "If 'beforeinput' event is consumed, paste should be canceled");
+ is(pasteEventCount, 1,
+ 'One "paste" event should be fired before "beforeinput" event is consumed');
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired for making it possible to consume');
+ checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'when "beforeinput" is canceled in bubbling phase');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired if "paste" event is canceled');
+ aTextarea.value = "";
+
+ aTextarea.removeEventListener("paste", pasteEventLogger);
+ aTextarea.removeEventListener("beforeinput", onBeforeInput);
+ aTextarea.removeEventListener("input", onInput);
+}
+
+async function doContenteditableTests(aEditableDiv) {
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ let selectionRanges = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ let selection = document.getSelection();
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ aEditableDiv.addEventListener("beforeinput", onBeforeInput);
+ aEditableDiv.addEventListener("input", onInput);
+
+ await copyPlaintext("abc\ndef\nghi");
+ aEditableDiv.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true});
+ is(aEditableDiv.innerHTML,
+ "<blockquote type=\"cite\">abc<br>def<br>ghi</blockquote>",
+ "Pasted plaintext should be in <blockquote> element and each linebreaker should be <br> element");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired on the editing host');
+ checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", null,
+ [{type: "text/plain", data: "abc\ndef\nghi"}], selectionRanges, "(contenteditable)");
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired on the editing host');
+ checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", null,
+ [{type: "text/plain", data: "abc\ndef\nghi"}], [], "(contenteditable)");
+ aEditableDiv.innerHTML = "";
+
+ let pasteEventCount = 0;
+ function pasteEventLogger(event) {
+ pasteEventCount++;
+ }
+ aEditableDiv.addEventListener("paste", pasteEventLogger);
+
+ await copyPlaintext("abc");
+ aEditableDiv.focus();
+ window.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true});
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aEditableDiv, {button: 1});
+ is(aEditableDiv.innerHTML, "abc",
+ "If 'click' event is consumed at capturing phase of the window, paste should not be canceled");
+ is(pasteEventCount, 1,
+ "If 'click' event is consumed at capturing phase of the window, 'paste' event should be fired once");
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should still be fired when the "click" event is canceled (contenteditable)');
+ checkInputEvent(beforeInputEvents[0], "insertFromPaste", null,
+ [{type: "text/plain", data: "abc"}], selectionRanges, 'when the "click" event is canceled (contenteditable)');
+ is(inputEvents.length, 1,
+ '"input" event should still be fired when the "click" event is canceled (contenteditable)');
+ checkInputEvent(inputEvents[0], "insertFromPaste", null,
+ [{type: "text/plain", data: "abc"}], [], 'when the "click" event is canceled (contenteditable)');
+ aEditableDiv.innerHTML = "";
+
+ await copyPlaintext("abc");
+ aEditableDiv.focus();
+ aEditableDiv.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aEditableDiv, {button: 1});
+ is(aEditableDiv.innerHTML, "abc",
+ "Even if 'mouseup' event is consumed, paste should be done");
+ is(pasteEventCount, 1,
+ "Even if 'mouseup' event is consumed, 'paste' event should be fired once");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired even if "mouseup" event is canceled (contenteditable)');
+ checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], selectionRanges,
+ 'even if "mouseup" event is canceled (contenteditable)');
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired even if "mouseup" event is canceled (contenteditable)');
+ checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], [],
+ 'even if "mouseup" event is canceled (contenteditable)');
+ aEditableDiv.innerHTML = "";
+
+ await copyPlaintext("abc");
+ aEditableDiv.focus();
+ aEditableDiv.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aEditableDiv, {button: 1});
+ is(aEditableDiv.innerHTML, "abc",
+ "Even if 'click' event handler is added to the editing host, paste should not be canceled");
+ is(pasteEventCount, 1,
+ "Even if 'click' event handler is added to the editing host, 'paste' event should be fired");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)');
+ checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], selectionRanges,
+ 'even if "click" event is canceled in bubbling phase (contenteditable)');
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)');
+ checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], [],
+ 'even if "click" event is canceled in bubbling phase (contenteditable)');
+ aEditableDiv.innerHTML = "";
+
+ await copyPlaintext("abc");
+ aEditableDiv.focus();
+ aEditableDiv.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aEditableDiv, {button: 1});
+ is(aEditableDiv.innerHTML, "",
+ "If 'auxclick' event is consumed, paste should be canceled");
+ is(pasteEventCount, 0,
+ "If 'auxclick' event is consumed, 'paste' event should not be fired");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired if "auxclick" event is canceled (contenteditable)');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired if "auxclick" event is canceled (contenteditable)');
+ aEditableDiv.innerHTML = "";
+
+ await copyPlaintext("abc");
+ aEditableDiv.focus();
+ aEditableDiv.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aEditableDiv, {button: 1});
+ is(aEditableDiv.innerHTML, "",
+ "If 'paste' event is consumed, paste should be canceled");
+ is(pasteEventCount, 1,
+ 'One "paste" event should be fired for making it possible to consume');
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired if "paste" event is canceled (contenteditable)');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired if "paste" event is canceled (contenteditable)');
+ aEditableDiv.innerHTML = "";
+
+ await copyPlaintext("abc");
+ aEditableDiv.focus();
+ aEditableDiv.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true});
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aEditableDiv, {button: 1});
+ is(aEditableDiv.innerHTML, "",
+ "If 'paste' event is consumed, paste should be canceled");
+ is(pasteEventCount, 1,
+ 'One "paste" event should be fired before "beforeinput" event');
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired for making it possible to consume (contenteditable)');
+ checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], selectionRanges,
+ 'when "beforeinput" will be canceled (contenteditable)');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired if "beforeinput" event is canceled (contenteditable)');
+ aEditableDiv.innerHTML = "";
+
+ // If clipboard event is disabled, InputEvent.dataTransfer should have only empty string.
+ await SpecialPowers.pushPrefEnv({"set": [["dom.event.clipboardevents.enabled", false]]});
+ await copyPlaintext("abc");
+ aEditableDiv.focus();
+ pasteEventCount = 0;
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aEditableDiv, {button: 1});
+ is(aEditableDiv.innerHTML, "abc",
+ "Even if clipboard event is disabled, paste should be done");
+ is(pasteEventCount, 0,
+ "If clipboard event is disabled, 'paste' event shouldn't be fired once");
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired even if clipboard event is disabled (contenteditable)');
+ checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: ""}], selectionRanges,
+ "when clipboard event is disabled (contenteditable)");
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired even if clipboard event is disabled (contenteditable)');
+ checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: ""}], [],
+ "when clipboard event is disabled (contenteditable)");
+ await SpecialPowers.pushPrefEnv({"set": [["dom.event.clipboardevents.enabled", true]]});
+ aEditableDiv.innerHTML = "";
+
+ aEditableDiv.removeEventListener("paste", pasteEventLogger);
+
+ // Oddly, copyHTMLContent fails randomly only on Linux. Let's skip this.
+ if (navigator.platform.startsWith("Linux")) {
+ aEditableDiv.removeEventListener("input", onInput);
+ return;
+ }
+
+ await copyHTMLContent("<p>abc</p><p>def</p><p>ghi</p>");
+ aEditableDiv.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true});
+ if (!navigator.appVersion.includes("Android")) {
+ is(aEditableDiv.innerHTML,
+ "<blockquote type=\"cite\"><p>abc</p><p>def</p><p>ghi</p></blockquote>",
+ "Pasted HTML content should be set to the <blockquote>");
+ } else {
+ // Oddly, on Android, we use <br> elements for pasting <p> elements.
+ is(aEditableDiv.innerHTML,
+ "<blockquote type=\"cite\">abc<br><br>def<br><br>ghi</blockquote>",
+ "Pasted HTML content should be set to the <blockquote>");
+ }
+ // On windows, HTML clipboard includes extra data.
+ // The values are from widget/windows/nsDataObj.cpp.
+ const kHTMLPrefix = (navigator.platform.includes("Win")) ? kTextHtmlPrefixClipboardDataWindows : "";
+ const kHTMLPostfix = (navigator.platform.includes("Win")) ? kTextHtmlSuffixClipboardDataWindows : "";
+ is(beforeInputEvents.length, 1,
+ 'One "beforeinput" event should be fired when pasting HTML');
+ checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", null,
+ [{type: "text/html",
+ data: `${kHTMLPrefix}<p>abc</p><p>def</p><p>ghi</p>${kHTMLPostfix}`}],
+ selectionRanges,
+ "when pasting HTML");
+ is(inputEvents.length, 1,
+ 'One "input" event should be fired when pasting HTML');
+ checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", null,
+ [{type: "text/html",
+ data: `${kHTMLPrefix}<p>abc</p><p>def</p><p>ghi</p>${kHTMLPostfix}`}],
+ [],
+ "when pasting HTML");
+ aEditableDiv.innerHTML = "";
+
+ aEditableDiv.removeEventListener("beforeinput", onBeforeInput);
+ aEditableDiv.removeEventListener("input", onInput);
+}
+
+async function doNestedEditorTests(aEditableDiv) {
+ await copyPlaintext("CLIPBOARD TEXT");
+ aEditableDiv.innerHTML = '<p id="p">foo</p><textarea id="textarea"></textarea>';
+ aEditableDiv.focus();
+ let textarea = document.getElementById("textarea");
+ let pasteTarget = null;
+ function onPaste(aEvent) {
+ pasteTarget = aEvent.target;
+ }
+ document.addEventListener("paste", onPaste);
+
+ synthesizeMouseAtCenter(textarea, {button: 1});
+ is(pasteTarget.getAttribute("id"), "textarea",
+ "Target of 'paste' event should be the clicked <textarea>");
+ is(textarea.value, "CLIPBOARD TEXT",
+ "Clicking in <textarea> in an editable <div> should paste the clipboard text into the <textarea>");
+ is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea"></textarea>',
+ "Pasting in the <textarea> shouldn't be handled by the HTMLEditor");
+
+ textarea.value = "";
+ textarea.readOnly = true;
+ pasteTarget = null;
+ synthesizeMouseAtCenter(textarea, {button: 1});
+ is(pasteTarget, textarea,
+ "Target of 'paste' event should be the clicked <textarea> even if it's read-only");
+ is(textarea.value, "",
+ "Clicking in read-only <textarea> in an editable <div> should not paste the clipboard text into the read-only <textarea>");
+ // HTMLEditor thinks that read-only <textarea> is not modifiable.
+ // Therefore, HTMLEditor does not paste the text.
+ is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea" readonly=""></textarea>',
+ "Clicking in read-only <textarea> shouldn't cause pasting the clipboard text into its parent HTMLEditor");
+
+ textarea.value = "";
+ textarea.readOnly = false;
+ textarea.disabled = true;
+ pasteTarget = null;
+ synthesizeMouseAtCenter(textarea, {button: 1});
+ // Although, this compares with <textarea>, I'm not sure it's proper event
+ // target because of disabled <textarea>.
+ todo_is(pasteTarget, textarea,
+ "Target of 'paste' event should be the clicked <textarea> even if it's disabled");
+ is(textarea.value, "",
+ "Clicking in disabled <textarea> in an editable <div> should not paste the clipboard text into the disabled <textarea>");
+ // HTMLEditor thinks that disabled <textarea> is not modifiable.
+ // Therefore, HTMLEditor does not paste the text.
+ is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea" disabled=""></textarea>',
+ "Clicking in disabled <textarea> shouldn't cause pasting the clipboard text into its parent HTMLEditor");
+
+ document.removeEventListener("paste", onPaste);
+ aEditableDiv.innerHTML = "";
+}
+
+async function doAfterRemoveOfClickedElementTest(aEditableDiv) {
+ await copyPlaintext("CLIPBOARD TEXT");
+ aEditableDiv.innerHTML = '<p id="p">foo<span id="span">bar</span></p>';
+ aEditableDiv.focus();
+ let span = document.getElementById("span");
+ let pasteTarget = null;
+ document.addEventListener("paste", (aEvent) => { pasteTarget = aEvent.target; }, {once: true});
+ document.addEventListener("auxclick", (aEvent) => {
+ is(aEvent.target.getAttribute("id"), "span",
+ "Target of auxclick event should be the <span> element");
+ span.parentElement.removeChild(span);
+ }, {once: true});
+ synthesizeMouseAtCenter(span, {button: 1});
+ is(pasteTarget.getAttribute("id"), "p",
+ "Target of 'paste' event should be the <p> element since <span> has gone");
+ // XXX Currently, pasted to start of the <p> because EventStateManager
+ // do not recompute event target frame.
+ todo_is(aEditableDiv.innerHTML, '<p id="p">fooCLIPBOARD TEXT</p>',
+ "Clipbpard text should looks like replacing the <span> element");
+ aEditableDiv.innerHTML = "";
+}
+
+async function doNotStartAutoscrollInContentEditable(aEditableDiv) {
+ await SpecialPowers.pushPrefEnv({"set": [["general.autoScroll", true]]});
+ await copyPlaintext("CLIPBOARD TEXT");
+ aEditableDiv.innerHTML = '<p id="p">foo<span id="span">bar</span></p>';
+ aEditableDiv.focus();
+ let span = document.getElementById("span");
+ synthesizeMouseAtCenter(span, {button: 1});
+ ok(aEditableDiv.innerHTML.includes("CLIPBOARD TEXT"),
+ "Clipbpard text should be inserted");
+ aEditableDiv.innerHTML = "";
+}
+
+async function doTests() {
+ await SpecialPowers.pushPrefEnv({"set": [["middlemouse.paste", true],
+ ["middlemouse.contentLoadURL", false],
+ ["dom.event.clipboardevents.enabled", true]]});
+ let container = document.getElementById("container");
+ container.innerHTML = "<textarea id=\"editor\"></textarea>";
+ await doTextareaTests(document.getElementById("editor"));
+ container.innerHTML = "<div id=\"editor\" contenteditable style=\"min-height: 1em;\"></div>";
+ await doContenteditableTests(document.getElementById("editor"));
+ await doNestedEditorTests(document.getElementById("editor"));
+ await doAfterRemoveOfClickedElementTest(document.getElementById("editor"));
+ // NOTE: The following test sets `general.autoScroll` to true.
+ await doNotStartAutoscrollInContentEditable(document.getElementById("editor"));
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(doTests);
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_native_key_bindings_in_shadow.html b/editor/libeditor/tests/test_native_key_bindings_in_shadow.html
new file mode 100644
index 0000000000..d29fb5f13f
--- /dev/null
+++ b/editor/libeditor/tests/test_native_key_bindings_in_shadow.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Check whether native key bindings work in shadow DOM in editor</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script>
+"use strict";
+
+const kIsMac = navigator.platform.includes("Mac");
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ const shadowHost = document.querySelector("div");
+ const shadowRoot = shadowHost.attachShadow({mode: "open"});
+ shadowRoot.innerHTML = "<div contenteditable>abc def</div>";
+ const editingHost = shadowRoot.querySelector("div[contenteditable]");
+ editingHost.focus();
+ getSelection().collapse(editingHost.firstChild, "abc ".length);
+ if (kIsMac) {
+ synthesizeKey("KEY_ArrowLeft", {metaKey: true});
+ } else {
+ synthesizeKey("KEY_Home");
+ }
+ synthesizeKey("X", {shiftKey: true});
+ is(
+ editingHost.textContent,
+ "Xabc def",
+ "X should've insert start of the editing host after typing \"Home\""
+ );
+ if (kIsMac) {
+ synthesizeKey("KEY_ArrowRight", {metaKey: true});
+ } else {
+ synthesizeKey("KEY_End");
+ }
+ synthesizeKey("Y", {shiftKey: true});
+ is(
+ editingHost.textContent,
+ "Xabc defY",
+ "Y should've been inserted end of the editing host after typing \"End\""
+ );
+
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body><div></div></body>
+</html>
diff --git a/editor/libeditor/tests/test_nested_editor.html b/editor/libeditor/tests/test_nested_editor.html
new file mode 100644
index 0000000000..aeb0c115c1
--- /dev/null
+++ b/editor/libeditor/tests/test_nested_editor.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title> Test for nested contenteditable elements </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+ <template id="focus-iframe-contenteditable-in-div">
+ <div contenteditable>
+ <iframe srcdoc="<div id='focusme' contenteditable></div>"></iframe>
+ </div>
+ </template>
+
+ <template id="focus-contenteditable-parent-along-with-iframe">
+ <div id='focusme' contenteditable></div>
+ <iframe srcdoc="<div contenteditable></div>"></iframe>
+ </template>
+
+ <template id="focus-iframe-textarea-in-div">
+ <div contenteditable>
+ <iframe srcdoc="<textarea id='focusme'></textarea>"></iframe>
+ </div>
+ </template>
+
+ <template id="focus-textarea-parent-along-with-iframe">
+ <textarea id='focusme' contenteditable></textarea>
+ <iframe srcdoc="<div contenteditable></div>"></iframe>
+ </template>
+<script>
+"use strict";
+
+async function runTest() {
+ function findFocusme() {
+ return new Promise(r => {
+ let focusInParent = document.getElementById("focusme");
+ if (focusInParent) {
+ r(focusInParent);
+ return;
+ }
+ document.querySelector("iframe").addEventListener("load", function() {
+ return r(document.querySelector("iframe").contentDocument.getElementById("focusme"));
+ });
+ });
+ }
+
+ const focusme = await findFocusme();
+
+ focusme.focus();
+ synthesizeKey("abc");
+
+ if (focusme.nodeName === "TEXTAREA") {
+ is(focusme.value, "abc");
+ } else {
+ is(focusme.innerHTML, "abc");
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(async () => {
+ for (const template of document.querySelectorAll("template")) {
+ const content = template.content.cloneNode(true);
+ document.body.appendChild(content);
+
+ await runTest();
+
+ document.body.innerHTML = "";
+ }
+
+ SimpleTest.finish();
+});
+
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_new_plaintext_mail_with_plaintext_signature.html b/editor/libeditor/tests/test_new_plaintext_mail_with_plaintext_signature.html
new file mode 100644
index 0000000000..327954aedb
--- /dev/null
+++ b/editor/libeditor/tests/test_new_plaintext_mail_with_plaintext_signature.html
@@ -0,0 +1,94 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title></title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const iframe = document.querySelector("iframe");
+ const doc = iframe.contentDocument;
+ const win = iframe.contentWindow;
+ doc.designMode = "on";
+ // Ensure focus
+ await new Promise(resolve => {
+ doc.addEventListener("focus", resolve, {once: true});
+ iframe.focus();
+ });
+
+ function getHTMLEditor() {
+ const editingSession = SpecialPowers.wrap(win).docShell.editingSession;
+ if (!editingSession) {
+ return null;
+ }
+ const editor = editingSession.getEditorForWindow(win);
+ if (!editor) {
+ return null;
+ }
+ return editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor);
+ }
+
+ const nsIEditor = SpecialPowers.Ci.nsIEditor;
+ const htmlEditor = getHTMLEditor();
+ // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mozilla/editor/composer/nsEditingSession.cpp#300-302
+ htmlEditor.flags |=
+ nsIEditor.eEditorPlaintextMask
+ | nsIEditor.eEditorMailMask
+ | nsIEditor.eEditorEnableWrapHackMask;
+
+ // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mail/components/compose/content/MsgComposeCommands.js#10984,10986-10987,10989
+ doc.execCommand("defaultParagraphSeparator", false, "br");
+
+ // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#700
+ win.getSelection().collapse(doc.body, doc.body.childNodes.length);
+ is(
+ doc.body.innerHTML,
+ "<br>",
+ "Initially, the <body> should have a <br>"
+ );
+
+ // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#705
+ htmlEditor.insertLineBreak();
+ is(
+ doc.body.innerHTML,
+ "<br><br>",
+ "Insert line break in the <body> should insert 2 <br> elements"
+ );
+ is(
+ win.getSelection().focusNode,
+ doc.body,
+ "Selection container should be the <body> after the call of insertLineBreak()"
+ );
+ is(
+ win.getSelection().focusOffset,
+ 1,
+ "Selection should be collapsed after the first <br> after the call of insertLineBreak()"
+ );
+
+ // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#706
+ // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#371-372
+ const wrapperDiv = htmlEditor.createElementWithDefaults("div");
+ // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#390-391,394,401-402,404
+ wrapperDiv.appendChild(doc.createTextNode("-- "));
+ wrapperDiv.appendChild(htmlEditor.createElementWithDefaults("br"));
+ // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#390-391,394,401-402,404
+ wrapperDiv.appendChild(doc.createTextNode("Plaintext signature"));
+ wrapperDiv.appendChild(htmlEditor.createElementWithDefaults("br"));
+ // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#414
+ htmlEditor.insertElementAtSelection(wrapperDiv, true);
+
+ is(
+ doc.body.innerHTML.trim(),
+ "<br><div>-- <br>Plaintext signature<br></div><br>",
+ "The signature block should follow a <br>"
+ );
+
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body><iframe srcdoc='<body style="font-family: -moz-fixed; white-space: pre-wrap; width: 72ch;"></body>'></iframe></body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html b/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html
new file mode 100644
index 0000000000..531a8c6015
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html
@@ -0,0 +1,322 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsIEditorMailSupport.insertAsCitedQuotation()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div contenteditable></div>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ function testInputEvents() {
+ const inReadonlyMode = getEditor().flags & SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ const editorDescription = `(readonly=${!!inReadonlyMode})`;
+
+ const editor = document.querySelector("div[contenteditable]");
+ const selection = getSelection();
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ let selectionRanges = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editor.addEventListener("beforeinput", onBeforeInput);
+ editor.addEventListener("input", onInput);
+
+ editor.innerHTML = "";
+ editor.focus();
+ selection.collapse(editor, 0);
+
+ function checkInputEvent(aEvent, aInputType, aData, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ // If it were cancelable whose inputType is empty string, web apps would
+ // block any Firefox specific modification whose inputType are not declared
+ // by the spec.
+ let expectedCancelable = aEvent.type === "beforeinput" && aInputType !== "";
+ is(aEvent.cancelable, expectedCancelable,
+ `"${aEvent.type}" event should ${expectedCancelable ? "be" : "be never"} cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, aInputType,
+ `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`);
+ is(aEvent.data, aData,
+ `data of "${aEvent.type}" event should be ${aData} ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ is(targetRanges.length, selectionRanges.length,
+ `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`);
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0,
+ `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`);
+ }
+ }
+
+ // Tests when the editor is in plaintext mode.
+
+ getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ getEditorMailSupport().insertAsCitedQuotation("this is quoted text\nAnd here is second line.", "this is cited text", false);
+
+ ok(
+ selection.isCollapsed,
+ `Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}`
+ );
+ is(
+ selection.focusNode,
+ editor,
+ `focus node of Selection should be a child of the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}`
+ );
+ is(
+ selection.focusOffset,
+ 1,
+ `focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}`
+ );
+ is(
+ editor.innerHTML,
+ '<span style="white-space: pre-wrap;">&gt; this is quoted text<br>&gt; And here is second line.<br><br></span>',
+ `The quoted text should be inserted as plaintext into the plaintext editor ${editorDescription}`
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ `One "beforeinput" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}`
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "insertText", "this is quoted text\nAnd here is second line.",
+ `after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}`
+ );
+ is(
+ inputEvents.length,
+ 1,
+ `One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}`
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "insertText", "this is quoted text\nAnd here is second line.",
+ `after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}`
+ );
+
+ // Tests when the editor is in HTML editor mode.
+ getEditor().flags &= ~SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+
+ editor.innerHTML = "";
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>", "this is cited text", false);
+
+ ok(
+ selection.isCollapsed,
+ `Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}`
+ );
+ is(
+ selection.focusNode,
+ editor,
+ `focus node of Selection should be a child of the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}`
+ );
+ is(
+ selection.focusOffset,
+ 1,
+ `focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}`
+ );
+ is(
+ editor.innerHTML,
+ '<blockquote type="cite" cite="this is cited text">this is quoted text&lt;br&gt;</blockquote>',
+ `The quoted text should be inserted as plaintext into the HTML editor ${editorDescription}`
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ `One "beforeinput" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}`
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "",
+ null,
+ `after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}`
+ );
+ is(
+ inputEvents.length,
+ 1,
+ `One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}`
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "",
+ null,
+ `after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}`
+ );
+
+ editor.innerHTML = "";
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>And here is second line.", "this is cited text", true);
+
+ ok(
+ selection.isCollapsed,
+ `Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}`
+ );
+ is(
+ selection.focusNode,
+ editor,
+ `focus node of Selection should be a child of the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}`
+ );
+ is(
+ selection.focusOffset,
+ 1,
+ `focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}`
+ );
+ is(
+ editor.innerHTML,
+ '<blockquote type="cite" cite="this is cited text">this is quoted text<br>And here is second line.</blockquote>',
+ `The quoted text should be inserted as HTML source into the HTML editor ${editorDescription}`
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ `One "beforeinput" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}`
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "",
+ null,
+ `after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}`
+ );
+ is(
+ inputEvents.length,
+ 1,
+ `One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}`
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "",
+ null,
+ `after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}`
+ );
+
+ editor.removeEventListener("beforeinput", onBeforeInput);
+ editor.removeEventListener("input", onInput);
+ }
+
+ function testStyleOfPlaintextMode() {
+ const inReadonlyMode = getEditor().flags & SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ const editorDescription = `(readonly=${!!inReadonlyMode})`;
+
+ getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+
+ (function testInDiv() {
+ const editor = document.querySelector("div[contenteditable]");
+ editor.innerHTML = "";
+ editor.focus();
+ getEditorMailSupport().insertAsCitedQuotation(
+ "this is quoted text.",
+ "this is cited text",
+ false
+ );
+ is(
+ editor.firstChild.tagName,
+ "SPAN",
+ `testStyleOfPlaintextMode: testInDiv: insertAsCitedQuotation should insert a <span> element ${editorDescription}`
+ );
+ const computedSpanStyle = getComputedStyle(editor.firstChild);
+ is(
+ computedSpanStyle.display,
+ "inline",
+ `testStyleOfPlaintextMode: testInDiv: The inserted <span> element should be "display: inline;" ${editorDescription}`
+ );
+ is(
+ computedSpanStyle.whiteSpace,
+ "pre-wrap",
+ `testStyleOfPlaintextMode: testInDiv: The inserted <span> element should be "white-space: pre-wrap;" ${editorDescription}`
+ );
+ })();
+
+ try {
+ document.body.contentEditable = true;
+ (function testInBody() {
+ getSelection().collapse(document.body, 0);
+ getEditorMailSupport().insertAsCitedQuotation(
+ "this is quoted text.",
+ "this is cited text",
+ false
+ );
+ is(
+ document.body.firstChild.tagName,
+ "SPAN",
+ `testStyleOfPlaintextMode: testInBody: insertAsCitedQuotation should insert a <span> element in plaintext mode and in a <body> element ${editorDescription}`
+ );
+ const computedSpanStyle = getComputedStyle(document.body.firstChild);
+ is(
+ computedSpanStyle.display,
+ "block",
+ `testStyleOfPlaintextMode: testInBody: The inserted <span> element should be "display: block;" in plaintext mode and in a <body> element ${editorDescription}`
+ );
+ is(
+ computedSpanStyle.whiteSpace,
+ "pre-wrap",
+ `testStyleOfPlaintextMode: testInBody: The inserted <span> element should be "white-space: pre-wrap;" in plaintext mode and in a <body> element ${editorDescription}`
+ );
+ document.body.firstChild.remove();
+ })();
+ } finally {
+ document.body.contentEditable = false;
+ }
+ }
+
+ testInputEvents();
+ testStyleOfPlaintextMode();
+
+ // Even if the HTMLEditor is readonly, XPCOM API should keep working.
+ getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ testInputEvents();
+ testStyleOfPlaintextMode();
+
+ SimpleTest.finish();
+});
+
+function getEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window);
+}
+
+function getEditorMailSupport() {
+ return getEditor().QueryInterface(SpecialPowers.Ci.nsIEditorMailSupport);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditorMailSupport_insertTextWithQuotations.html b/editor/libeditor/tests/test_nsIEditorMailSupport_insertTextWithQuotations.html
new file mode 100644
index 0000000000..921e137fa8
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditorMailSupport_insertTextWithQuotations.html
@@ -0,0 +1,104 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsIEditorMailSupport.insertTextWithQuotations()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div contenteditable></div>
+<iframe srcdoc="<body contenteditable></body>"></iframe>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const iframe = document.querySelector("iframe");
+ await new Promise(resolve => {
+ if (iframe.contentDocument?.readyState == "complete") {
+ resolve();
+ return;
+ }
+ iframe.addEventListener("load", resolve, {once: true});
+ });
+
+ function testInDiv() {
+ const inPlaintextMode = getEditor(window).flags & SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+ const inReadonlyMode = getEditor(window).flags & SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ const editorDescription = `(readonly=${!!inReadonlyMode}, plaintext=${!!inPlaintextMode})`;
+ const editor = document.querySelector("div[contenteditable]");
+ editor.innerHTML = "";
+ editor.focus();
+ getEditorMailSupport(window).insertTextWithQuotations(
+ "This is Text\n\n> This is a quote."
+ );
+ is(
+ editor.innerHTML,
+ 'This is Text<br><br><span style="white-space: pre-wrap;">&gt; This is a quote.</span><br>',
+ `The <div contenteditable> should have the expected innerHTML ${editorDescription}`
+ );
+ is(
+ editor.querySelector("span")?.getAttribute("_moz_quote"),
+ "true",
+ `The <span> element in the <div contenteditable> should have _moz_quote="true" ${editorDescription}`
+ );
+ }
+
+ function testInBody() {
+ const inPlaintextMode = getEditor(iframe.contentWindow).flags & SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+ const inReadonlyMode = getEditor(iframe.contentWindow).flags & SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ const editorDescription = `(readonly=${!!inReadonlyMode}, plaintext=${!!inPlaintextMode})`;
+ const editor = iframe.contentDocument.body;
+ editor.innerHTML = "";
+ iframe.contentWindow.getSelection().collapse(document.body, 0);
+ getEditorMailSupport(iframe.contentWindow).insertTextWithQuotations(
+ "This is Text\n\n> This is a quote."
+ );
+ is(
+ editor.innerHTML,
+ 'This is Text<br><br><span style="white-space: pre-wrap; display: block; width: 98vw;">&gt; This is a quote.</span><br>',
+ `The <body> should have the expected innerHTML ${editorDescription}`
+ );
+ is(
+ editor.querySelector("span")?.getAttribute("_moz_quote"),
+ "true",
+ `The <span> element in the <body> should have _moz_quote="true" ${editorDescription}`
+ );
+ }
+
+ for (const testReadOnly of [false, true]) {
+ // Even if the HTMLEditor is readonly, XPCOM API should keep working.
+ if (testReadOnly) {
+ getEditor(window).flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ getEditor(iframe.contentWindow).flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ } else {
+ getEditor(window).flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ getEditor(iframe.contentWindow).flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ }
+
+ getEditor(window).flags &= ~SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+ getEditor(iframe.contentWindow).flags &= ~SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+ testInDiv();
+ testInBody();
+
+ getEditor(window).flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+ getEditor(iframe.contentWindow).flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+ testInDiv();
+ testInBody();
+ }
+
+ SimpleTest.finish();
+});
+
+function getEditor(aWindow) {
+ const editingSession = SpecialPowers.wrap(aWindow).docShell.editingSession;
+ return editingSession.getEditorForWindow(aWindow);
+}
+
+function getEditorMailSupport(aWindow) {
+ return getEditor(aWindow).QueryInterface(SpecialPowers.Ci.nsIEditorMailSupport);
+}
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_beginningOfDocument.html b/editor/libeditor/tests/test_nsIEditor_beginningOfDocument.html
new file mode 100644
index 0000000000..119f35dbdd
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_beginningOfDocument.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests of nsIEditor#beginningOfDocument()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(() => {
+ const originalBody = document.body.innerHTML;
+
+ (function test_with_text_editor() {
+ for (const test of [
+ {
+ tag: "input",
+ innerHTML: "<input>",
+ },
+ {
+ tag: "textarea",
+ innerHTML: "<textarea></textarea>",
+ },
+ ]) {
+ document.body.innerHTML = test.innerHTML;
+ const textControl = document.body.querySelector(test.tag);
+ const editor = SpecialPowers.wrap(textControl).editor;
+ editor.beginningOfDocument();
+ is(textControl.selectionStart, 0,
+ `nsIEditor.beginningOfDocument() should set selectionStart of empty <${test.tag}> to 0`);
+ is(textControl.selectionEnd, 0,
+ `nsIEditor.beginningOfDocument() should set selectionEnd of empty <${test.tag}> to 0`);
+ textControl.value = "abc";
+ textControl.selectionStart = 2;
+ textControl.selectionEnd = 3;
+ editor.beginningOfDocument();
+ is(textControl.selectionStart, 0,
+ `nsIEditor.beginningOfDocument() should set selectionStart of non-empty <${test.tag}> to 0`);
+ is(textControl.selectionEnd, 0,
+ `nsIEditor.beginningOfDocument() should set selectionEnd of non-empty <${test.tag}> to 0`);
+ }
+ })();
+
+ function getHTMLEditor() {
+ const editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ if (!editingSession) {
+ return null;
+ }
+ return editingSession.getEditorForWindow(window);
+ }
+
+ (function test_with_contenteditable() {
+ document.body.innerHTML = "<div contenteditable><p>abc</p></div>";
+ getSelection().removeAllRanges();
+ getHTMLEditor().beginningOfDocument();
+ is(getSelection().rangeCount, 0,
+ "selection shouldn't be changed when there is no selection");
+ getSelection().setBaseAndExtent(document.body.querySelector("p").firstChild, 1,
+ document.body.querySelector("p").firstChild, 3);
+ getHTMLEditor().beginningOfDocument();
+ is(getSelection().isCollapsed, true,
+ "selection should be collapsed after calling nsIEditor.beginningOfDocument() with contenteditable having one paragraph");
+ is(getSelection().rangeCount, 1,
+ "selection should has only one range after calling nsIEditor.beginningOfDocument() with contenteditable having one paragraph");
+ is(getSelection().focusNode, document.body.querySelector("p").firstChild,
+ "selection should be collapsed into the text node after calling nsIEditor.beggingOfDocument() with content editable having one paragraph");
+ is(getSelection().focusOffset, 0,
+ "selection should be collapsed to start of the text node after calling nsIEditor.beggingOfDocument() with content editable having one paragraph");
+
+ document.body.innerHTML = "<div contenteditable><p contenteditable=\"false\">abc</p><p>def</p></div>";
+ getSelection().setBaseAndExtent(document.body.querySelector("p + p").firstChild, 1,
+ document.body.querySelector("p + p").firstChild, 3);
+ getHTMLEditor().beginningOfDocument();
+ is(getSelection().isCollapsed, true,
+ "selection should be collapsed after calling nsIEditor.beginningOfDocument() with contenteditable having non-editable paragraph first");
+ is(getSelection().rangeCount, 1,
+ "selection should has only one range after calling nsIEditor.beginningOfDocument() with contenteditable having non-editable paragraph first");
+ is(getSelection().focusNode, document.body.querySelector("div[contenteditable]"),
+ "selection should be collapsed to start of the editing host after calling nsIEditor.beggingOfDocument() with content editable having non-editable paragraph first");
+ is(getSelection().focusOffset, 0,
+ "selection should be collapsed to start of the editing host after calling nsIEditor.beggingOfDocument() with content editable having non-editable paragraph first");
+
+ document.body.innerHTML = "<div contenteditable>\n<p contenteditable=\"false\">abc</p>\n<p>def</p>\n</div>";
+ getSelection().setBaseAndExtent(document.body.querySelector("p + p").firstChild, 1,
+ document.body.querySelector("p + p").firstChild, 3);
+ getHTMLEditor().beginningOfDocument();
+ is(getSelection().isCollapsed, true,
+ "selection should be collapsed after calling nsIEditor.beginningOfDocument() with contenteditable having non-editable paragraph first and some invisible line breaks");
+ is(getSelection().rangeCount, 1,
+ "selection should has only one range after calling nsIEditor.beginningOfDocument() with contenteditable having non-editable paragraph first and some invisible line breaks");
+ is(getSelection().focusNode, document.body.querySelector("div[contenteditable]"),
+ "selection should be collapsed to start of the editing host after calling nsIEditor.beggingOfDocument() with content editable having non-editable paragraph first and some invisible line breaks");
+ is(getSelection().focusOffset, 0,
+ "selection should be collapsed to start of the editing host after calling nsIEditor.beggingOfDocument() with content editable having non-editable paragraph first and some invisible line breaks");
+
+ document.body.innerHTML = "<div contenteditable><p>abc</p></div>def<div contenteditable><p>ghi</p></div>";
+ getSelection().setBaseAndExtent(document.body.querySelector("div + div > p").firstChild, 1,
+ document.body.querySelector("div + div > p").firstChild, 3);
+ getHTMLEditor().beginningOfDocument();
+ is(getSelection().isCollapsed, true,
+ "selection should be collapsed after calling nsIEditor.beginningOfDocument() with the 2nd contenteditable");
+ is(getSelection().rangeCount, 1,
+ "selection should has only one range after calling nsIEditor.beginningOfDocument() with the 2nd contenteditable");
+ is(getSelection().focusNode, document.body.querySelector("div + div > p").firstChild,
+ "selection should be collapsed to start of the first text node in the second editing host after calling nsIEditor.beggingOfDocument() with the 2nd contenteditable");
+ is(getSelection().focusOffset, 0,
+ "selection should be collapsed to start of the first text node in the second editing host after calling nsIEditor.beggingOfDocument() with the 2nd contenteditable");
+ })();
+
+ document.body.innerHTML = originalBody;
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_canUndo_canRedo.html b/editor/libeditor/tests/test_nsIEditor_canUndo_canRedo.html
new file mode 100644
index 0000000000..fb0993daab
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_canUndo_canRedo.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Test for nsIEditor.canUndo and nsIEditor.canRedo</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content"><input><textarea></textarea><div contenteditable></div></div>
+<pre id="test">
+<script>
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ function isTextEditor(aElement) {
+ return aElement.tagName.toLowerCase() == "input" ||
+ aElement.tagName.toLowerCase() == "textarea";
+ }
+ function getEditor(aElement) {
+ if (isTextEditor(aElement)) {
+ return SpecialPowers.wrap(aElement).editor;
+ }
+ return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window);
+ }
+ function setValue(aElement, aValue) {
+ if (isTextEditor(aElement)) {
+ aElement.value = aValue;
+ return;
+ }
+ aElement.innerHTML = aValue;
+ }
+ function getValue(aElement) {
+ if (isTextEditor(aElement)) {
+ return aElement.value;
+ }
+ return aElement.innerHTML.replace(/<br>/g, "");
+ }
+ for (const selector of ["input", "textarea", "div[contenteditable]"]) {
+ const editableElement = document.querySelector(selector);
+ editableElement.focus();
+ const editor = getEditor(editableElement);
+ setValue(editableElement, "");
+ is(
+ editor.canUndo,
+ false,
+ `Editor for ${selector} shouldn't have undo transaction at start`
+ );
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} shouldn't have redo transaction at start`
+ );
+
+ synthesizeKey("b");
+ is(
+ getValue(editableElement),
+ "b",
+ `Editor for ${selector} should've handled inserting "b"`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `Editor for ${selector} should have undo transaction after inserting "b"`
+ );
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} shouldn't have redo transaction after inserting "b"`
+ );
+
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("a");
+ is(
+ getValue(editableElement),
+ "ab",
+ `Editor for ${selector} should've handled inserting "a" before "b"`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `Editor for ${selector} should have undo transaction after inserting text again`
+ );
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} should have redo transaction after inserting text again`
+ );
+
+ document.execCommand("undo");
+ is(
+ getValue(editableElement),
+ "b",
+ `Editor for ${selector} should've undone inserting "a"`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `Editor for ${selector} should have undo transaction for inserting "b" after undoing inserting "a"`
+ );
+ is(
+ editor.canRedo,
+ true,
+ `Editor for ${selector} should have redo transaction for inserting "b" after undoing inserting "a"`
+ );
+
+ document.execCommand("undo");
+ is(
+ getValue(editableElement),
+ "",
+ `Editor for ${selector} should've undone inserting "b"`
+ );
+ is(
+ editor.canUndo,
+ false,
+ `Editor for ${selector} shouldn't have undo transaction after undoing all things`
+ );
+ is(
+ editor.canRedo,
+ true,
+ `Editor for ${selector} should have redo transaction after undoing all things`
+ );
+
+ document.execCommand("redo");
+ is(
+ getValue(editableElement),
+ "b",
+ `Editor for ${selector} should've redone inserting "b"`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `Editor for ${selector} should have undo transaction after redoing inserted "a"`
+ );
+ is(
+ editor.canRedo,
+ true,
+ `Editor for ${selector} should have redo transaction after redoing inserted "a"`
+ );
+
+ document.execCommand("redo");
+ is(
+ getValue(editableElement),
+ "ab",
+ `Editor for ${selector} should've redone inserting "b"`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `Editor for ${selector} should have undo transaction after redoing all things`
+ );
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} shouldn't have redo transaction for after redoing all things`
+ );
+
+ document.execCommand("undo");
+ synthesizeKey("c");
+ is(
+ getValue(editableElement),
+ "cb",
+ `Editor for ${selector} should've redone inserting "b"`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `Editor for ${selector} should have undo transaction after inserting another undoing once`
+ );
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} shouldn't have redo transaction after inserting another undoing once`
+ );
+ }
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_clearUndoRedo.html b/editor/libeditor/tests/test_nsIEditor_clearUndoRedo.html
new file mode 100644
index 0000000000..695826c0d8
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_clearUndoRedo.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Test for nsIEditor.clearUndoRedo()</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content"><input><textarea></textarea><div contenteditable></div></div>
+<pre id="test">
+<script>
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ function isTextEditor(aElement) {
+ return aElement.tagName.toLowerCase() == "input" ||
+ aElement.tagName.toLowerCase() == "textarea";
+ }
+ function getEditor(aElement) {
+ if (isTextEditor(aElement)) {
+ return SpecialPowers.wrap(aElement).editor;
+ }
+ return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window);
+ }
+ function setValue(aElement, aValue) {
+ if (isTextEditor(aElement)) {
+ aElement.value = aValue;
+ return;
+ }
+ aElement.innerHTML = aValue;
+ }
+ function getValue(aElement) {
+ if (isTextEditor(aElement)) {
+ return aElement.value;
+ }
+ return aElement.innerHTML.replace(/<br>/g, "");
+ }
+ for (const selector of ["input", "textarea", "div[contenteditable]"]) {
+ const editableElement = document.querySelector(selector);
+ editableElement.focus();
+ const editor = getEditor(editableElement);
+ (function test_clearing_undo_history() {
+ setValue(editableElement, "");
+ is(
+ editor.canUndo,
+ false,
+ `Editor for ${selector} shouldn't have undo transaction at start`
+ );
+ synthesizeKey("a");
+ is(
+ getValue(editableElement),
+ "a",
+ `Editor for ${selector} should've handled typing "a"`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `Editor for ${selector} should have undo transaction for the inserted text`
+ );
+ editor.clearUndoRedo();
+ is(
+ editor.canUndo,
+ false,
+ `Editor for ${selector} shouldn't have undo transaction after calling nsIEditor.clearUndoRedo()`
+ );
+ document.execCommand("undo");
+ is(
+ getValue(editableElement),
+ "a",
+ `Editor for ${selector} should do noting for document.execCommand("undo")`
+ );
+ })();
+
+ (function test_clearing_redo_history() {
+ setValue(editableElement, "");
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} shouldn't have redo transaction at start`
+ );
+ synthesizeKey("b");
+ is(
+ getValue(editableElement),
+ "b",
+ `Editor for ${selector} should've handled typing "b"`
+ );
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} shouldn't have redo transaction after inserting text`
+ );
+ document.execCommand("undo");
+ is(
+ getValue(editableElement),
+ "",
+ `Editor for ${selector} should've handled the typing "b" after undoing`
+ );
+ is(
+ editor.canRedo,
+ true,
+ `Editor for ${selector} should have redo transaction of inserting text`
+ );
+ editor.clearUndoRedo();
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} shouldn't have redo transaction after calling nsIEditor.clearUndoRedo()`
+ );
+ document.execCommand("redo");
+ is(
+ getValue(editableElement),
+ "",
+ `Editor for ${selector} should do noting for document.execCommand("redo")`
+ );
+ })();
+ }
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_deleteNode.html b/editor/libeditor/tests/test_nsIEditor_deleteNode.html
new file mode 100644
index 0000000000..eacdac1c1b
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_deleteNode.html
@@ -0,0 +1,242 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>nsIEditor.insertNode</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+function stringifyInputEvent(aEvent) {
+ if (!aEvent) {
+ return "null";
+ }
+ return `${aEvent.type}: { inputType=${aEvent.inputType} }`;
+}
+
+function getRangeDescription(range) {
+ function getNodeDescription(node) {
+ if (!node) {
+ return "null";
+ }
+ switch (node.nodeType) {
+ case Node.TEXT_NODE:
+ return `${node.nodeName} "${node.data}"`;
+ case Node.ELEMENT_NODE:
+ return `<${node.nodeName.toLowerCase()}>`;
+ default:
+ return `${node.nodeName}`;
+ }
+ }
+ if (range === null) {
+ return "null";
+ }
+ if (range === undefined) {
+ return "undefined";
+ }
+ return range.startContainer == range.endContainer &&
+ range.startOffset == range.endOffset
+ ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
+ : `(${getNodeDescription(range.startContainer)}, ${
+ range.startOffset
+ }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ const editingHost = document.querySelector("div[contenteditable]");
+ const editor =
+ SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
+
+ editingHost.focus();
+
+ let events = [];
+ editingHost.addEventListener("input", event => events.push(event));
+
+ (function test_delete_node_before_selection() {
+ editingHost.innerHTML = "<span>abc</span><span>def</span>";
+ getSelection().collapse(editingHost.querySelector("span + span").firstChild, 0);
+ editor.deleteNode(editingHost.querySelector("span"));
+ is(
+ editingHost.innerHTML,
+ "<span>def</span>",
+ "test_delete_node_before_selection: deleteNode() should delete the node"
+ );
+ is(
+ events.length,
+ 1,
+ "test_delete_node_before_selection: Only one input event should be fired when deleteNode() deletes a node"
+ );
+ is(
+ stringifyInputEvent(events[0]),
+ stringifyInputEvent({ type: "input", inputType: "" }),
+ "test_delete_node_before_selection: input event should be fired when deleting a node"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost.firstChild.firstChild,
+ startOffset: 0,
+ endContainer: editingHost.firstChild.firstChild,
+ endOffset: 0,
+ }),
+ "test_delete_node_before_selection: selection shouldn't be updated"
+ );
+ })();
+
+ (function test_delete_node_after_selection() {
+ events = [];
+ editingHost.innerHTML = "<span>abc</span><span>def</span>";
+ getSelection().collapse(editingHost.querySelector("span").firstChild, 0);
+ editor.deleteNode(editingHost.querySelector("span + span"));
+ is(
+ editingHost.innerHTML,
+ "<span>abc</span>",
+ "test_delete_node_after_selection: deleteNode() should delete the node"
+ );
+ is(
+ events.length,
+ 1,
+ "test_delete_node_after_selection: Only one input event should be fired when deleteNode() deletes a node"
+ );
+ is(
+ stringifyInputEvent(events[0]),
+ stringifyInputEvent({ type: "input", inputType: "" }),
+ "test_delete_node_after_selection: input event should be fired when deleting a node"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost.firstChild.firstChild,
+ startOffset: 0,
+ endContainer: editingHost.firstChild.firstChild,
+ endOffset: 0,
+ }),
+ "test_delete_node_after_selection: selection shouldn't be updated"
+ );
+ })();
+
+ (function test_delete_node_containing_selection() {
+ events = [];
+ editingHost.innerHTML = "<span>abc</span><span>def</span>";
+ getSelection().collapse(editingHost.querySelector("span").firstChild, 0);
+ editor.deleteNode(editingHost.querySelector("span"));
+ is(
+ editingHost.innerHTML,
+ "<span>def</span>",
+ "test_delete_node_containing_selection: deleteNode() should delete the node"
+ );
+ is(
+ events.length,
+ 1,
+ "test_delete_node_containing_selection: Only one input event should be fired when deleteNode() deletes a node"
+ );
+ is(
+ stringifyInputEvent(events[0]),
+ stringifyInputEvent({ type: "input", inputType: "" }),
+ "test_delete_node_containing_selection: input event should be fired when deleting a node"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost,
+ startOffset: 0,
+ endContainer: editingHost,
+ endOffset: 0,
+ }),
+ "test_delete_node_containing_selection: selection should be updated whether node was"
+ );
+ })();
+
+ (function test_delete_node_containing_selection_with_preserving_selection() {
+ events = [];
+ editingHost.innerHTML = "<span>abc</span><span>def</span>";
+ getSelection().collapse(editingHost.querySelector("span").firstChild, 0);
+ editor.deleteNode(editingHost.querySelector("span"), true);
+ is(
+ editingHost.innerHTML,
+ "<span>def</span>",
+ "test_delete_node_containing_selection_with_preserving_selection: deleteNode() should delete the node"
+ );
+ is(
+ events.length,
+ 1,
+ "test_delete_node_containing_selection_with_preserving_selection: Only one input event should be fired when deleteNode() deletes a node"
+ );
+ is(
+ stringifyInputEvent(events[0]),
+ stringifyInputEvent({ type: "input", inputType: "" }),
+ "test_delete_node_containing_selection_with_preserving_selection: input event should be fired when deleting a node"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost,
+ startOffset: 0,
+ endContainer: editingHost,
+ endOffset: 0,
+ }),
+ "test_delete_node_containing_selection_with_preserving_selection: selection should be updated whether node was"
+ );
+ })();
+
+ (function test_not_preserve_selection_nested_by_beforeinput() {
+ editingHost.innerHTML = "<span>abc</span><span>ghi</span>";
+ const span = document.createElement("span");
+ span.textContent = "def";
+ getSelection().collapse(editingHost, 0);
+ editingHost.addEventListener("beforeinput", () => {
+ editor.insertNode(span, editingHost, 1);
+ }, {once: true});
+ editor.deleteNode(editingHost.querySelector("span + span"), true);
+ is(
+ editingHost.innerHTML,
+ "<span>abc</span><span>def</span>",
+ "test_not_preserve_selection_nested_by_beforeinput: both insertNode() and deleteNode() should work"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost,
+ startOffset: 2,
+ endContainer: editingHost,
+ endOffset: 2,
+ }),
+ "test_not_preserve_selection_nested_by_beforeinput: only insertNode() called in beforeinput listener should update selection"
+ );
+ })();
+
+ (function test_not_preserve_selection_nested_by_input() {
+ editingHost.innerHTML = "<span>abc</span><span>ghi</span>";
+ const span = document.createElement("span");
+ span.textContent = "def";
+ getSelection().collapse(editingHost, 0);
+ editingHost.addEventListener("input", () => {
+ editor.insertNode(span, editingHost, 1);
+ }, {once: true});
+ editor.deleteNode(editingHost.querySelector("span + span"), true);
+ is(
+ editingHost.innerHTML,
+ "<span>abc</span><span>def</span>",
+ "test_not_preserve_selection_nested_by_input: both insertNode() and deleteNode() should work"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost,
+ startOffset: 2,
+ endContainer: editingHost,
+ endOffset: 2,
+ }),
+ "test_not_preserve_selection_nested_by_input: only insertNode() called in input listener should update selection"
+ );
+ })();
+
+ SimpleTest.finish();
+});
+
+</script>
+</head>
+<body><div contenteditable><br></div></body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_documentCharacterSet.html b/editor/libeditor/tests/test_nsIEditor_documentCharacterSet.html
new file mode 100644
index 0000000000..18f948fdd9
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_documentCharacterSet.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html,charset=utf-8">
+ <title>Tests of nsIEditor#documentCharacterSet</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(() => {
+ const originalBody = document.body.innerHTML;
+
+ (function test_with_text_editor() {
+ for (const test of [
+ {
+ tag: "input",
+ innerHTML: "<input>",
+ },
+ {
+ tag: "textarea",
+ innerHTML: "<textarea></textarea>",
+ },
+ ]) {
+ document.body.innerHTML = test.innerHTML;
+ const textControl = document.body.querySelector(test.tag);
+ const editor = SpecialPowers.wrap(textControl).editor;
+ try {
+ editor.documentCharacterSet;
+ ok(false, `TextEditor::GetDocumentCharacterSet() for <${test.tag}> should throw an exception`);
+ } catch (e) {
+ ok(true, `TextEditor::GetDocumentCharacterSet() for <${test.tag}> should throw an exception`);
+ }
+ try {
+ editor.documentCharacterSet = "windows-1252";
+ ok(false, `TextEditor::SetDocumentCharacterSet() for <${test.tag}> should throw an exception`);
+ } catch (e) {
+ ok(true, `TextEditor::SetDocumentCharacterSet() for <${test.tag}> should throw an exception`);
+ }
+ }
+ })();
+
+ function getHTMLEditor(win = window) {
+ const editingSession = SpecialPowers.wrap(win).docShell.editingSession;
+ if (!editingSession) {
+ return null;
+ }
+ return editingSession.getEditorForWindow(win);
+ }
+
+ (function test_with_contenteditable() {
+ document.body.innerHTML = "<div contenteditable><p>abc</p></div>";
+ const editor = getHTMLEditor();
+ is(editor.documentCharacterSet, "UTF-8",
+ "HTMLEditor::GetDocumentCharacterSet() should return \"UTF-8\"");
+ editor.documentCharacterSet = "windows-1252";
+ is(document.querySelector("meta[http-equiv]").getAttribute("content"), "text/html,charset=windows-1252",
+ "HTMLEditor::SetDocumentCharacterSet() should add <meta> element whose \"http-equiv\" attribute has \"windows-1252\"");
+ is(editor.documentCharacterSet, "windows-1252",
+ "HTMLEditor::GetDocumentCharacterSet() should return \"windows-1252\" after setting the value");
+ editor.documentCharacterSet = "utf-8";
+ is(document.querySelector("meta[http-equiv]").getAttribute("content"), "text/html,charset=utf-8",
+ "HTMLEditor::SetDocumentCharacterSet() should add <meta> element whose \"http-equiv\" attribute has \"utf-8\"");
+ is(editor.documentCharacterSet, "UTF-8",
+ "HTMLEditor::GetDocumentCharacterSet() should return \"UTF-8\" after setting the value");
+ })();
+
+ (function test_with_designMode() {
+ while (document.querySelector("meta")) {
+ document.querySelector("meta").remove();
+ }
+ document.body.innerHTML = "<iframe></iframe>";
+ const editdoc = document.querySelector("iframe").contentDocument;
+ editdoc.designMode = "on";
+ const editor = getHTMLEditor(document.querySelector("iframe").contentWindow);
+
+ editor.documentCharacterSet = "us-ascii";
+ const metaWithHttpEquiv = editdoc.getElementsByTagName("meta")[0];
+ is(metaWithHttpEquiv.getAttribute("http-equiv"), "Content-Type",
+ "meta element should have http-equiv");
+ is(metaWithHttpEquiv.getAttribute("content"), "text/html;charset=us-ascii",
+ "charset should be set as us-ascii");
+
+ const dummyMeta = editdoc.createElement("meta");
+ dummyMeta.setAttribute("name", "keywords");
+ dummyMeta.setAttribute("content", "test");
+ metaWithHttpEquiv.parentNode.insertBefore(dummyMeta, metaWithHttpEquiv);
+
+ editor.documentCharacterSet = "utf-8";
+
+ is(dummyMeta, editdoc.getElementsByTagName("meta")[0],
+ "<meta> element shouldn't be touched");
+ isnot(dummyMeta.getAttribute("http-equiv"), "Content-Type",
+ "first meta element shouldn't have http-equiv");
+
+ is(metaWithHttpEquiv, editdoc.getElementsByTagName("meta")[1],
+ "The second <meta> element should be reused");
+ is(metaWithHttpEquiv.getAttribute("http-equiv"), "Content-Type",
+ "second meta element should have http-equiv");
+ is(metaWithHttpEquiv.getAttribute("content"), "text/html;charset=utf-8",
+ "charset should be set as utf-8");
+ })();
+
+ document.body.innerHTML = originalBody;
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_documentIsEmpty.html b/editor/libeditor/tests/test_nsIEditor_documentIsEmpty.html
new file mode 100644
index 0000000000..49c1db78a9
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_documentIsEmpty.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests of nsIEditor#documentIsEmpty</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(() => {
+ const originalBody = document.body.innerHTML;
+
+ (function test_with_text_editor() {
+ for (const test of [
+ {
+ tag: "input",
+ innerHTML: '<input><input value="abc"><input placeholder="abc">',
+ },
+ {
+ tag: "textarea",
+ innerHTML: '<textarea></textarea><textarea>abc</textarea><textarea placeholder="abc"></textarea>',
+ },
+ ]) {
+ document.body.innerHTML = test.innerHTML;
+ let textControl = document.body.querySelector(test.tag);
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true,
+ `nsIEditor.documentIsEmpty should be true if value of <${test.tag}> is empty by default`);
+ textControl.focus();
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true,
+ `nsIEditor.documentIsEmpty should be true if value of <${test.tag}> is empty by default after getting focus`);
+ textControl.value = "abc";
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false,
+ `nsIEditor.documentIsEmpty should be false if <${test.tag}>.value is set to non-empty string`);
+ textControl.value = "";
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true,
+ `nsIEditor.documentIsEmpty should be true if <${test.tag}>.value is set to empty string`);
+
+ textControl = textControl.nextSibling;
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false,
+ `nsIEditor.documentIsEmpty should be false if value of <${test.tag}> is non-empty by default`);
+ textControl.focus();
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false,
+ `nsIEditor.documentIsEmpty should be false if value of <${test.tag}> is non-empty by default after getting focus`);
+ textControl.value = "def";
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false,
+ `nsIEditor.documentIsEmpty should be false if <${test.tag}>.value is set to different non-empty string`);
+ textControl.value = "";
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true,
+ `nsIEditor.documentIsEmpty should be true if <${test.tag}>.value is set to empty string from non-empty string`);
+
+ textControl = textControl.nextSibling;
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true,
+ `nsIEditor.documentIsEmpty should be true if value of <${test.tag}> is empty by default (placeholder isn't empty)`);
+ textControl.focus();
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true,
+ `nsIEditor.documentIsEmpty should be true if value of <${test.tag}> is empty by default after getting focus (placeholder isn't empty)`);
+ textControl.value = "abc";
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false,
+ `nsIEditor.documentIsEmpty should be false if <${test.tag}>.value is set to non-empty string (placeholder isn't empty)`);
+ textControl.value = "";
+ is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true,
+ `nsIEditor.documentIsEmpty should be true if <${test.tag}>.value is set to empty string (placeholder isn't empty)`);
+ }
+ })();
+
+ function getHTMLEditor() {
+ const editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ if (!editingSession) {
+ return null;
+ }
+ return editingSession.getEditorForWindow(window);
+ }
+
+ (function test_with_contenteditable() {
+ document.body.innerHTML = "<div contenteditable></div>";
+ try {
+ getHTMLEditor().documentIsEmpty;
+ todo(false, "nsIEditor.documentIsEmpty should throw an exception when no editing host has focus");
+ } catch (e) {
+ ok(true, "nsIEditor.documentIsEmpty should throw an exception when no editing host has focus");
+ }
+ document.querySelector("div[contenteditable]").focus();
+ todo_is(getHTMLEditor().documentIsEmpty, true,
+ "nsIEditor.documentIsEmpty should be true when editing host does not have contents");
+
+ document.body.innerHTML = "<div contenteditable><br></div>";
+ document.querySelector("div[contenteditable]").focus();
+ is(getHTMLEditor().documentIsEmpty, false,
+ "nsIEditor.documentIsEmpty should be false when editing host has only a <br> element");
+
+ document.body.innerHTML = "<div contenteditable><p><br></p></div>";
+ document.querySelector("div[contenteditable]").focus();
+ is(getHTMLEditor().documentIsEmpty, false,
+ "nsIEditor.documentIsEmpty should be false when editing host has only an empty paragraph");
+
+ document.body.innerHTML = "<div contenteditable><p>abc</p></div>";
+ document.querySelector("div[contenteditable]").focus();
+ is(getHTMLEditor().documentIsEmpty, false,
+ "nsIEditor.documentIsEmpty should be false when editing host has text in a paragraph");
+
+ document.body.innerHTML = "<div contenteditable>abc</div>";
+ document.querySelector("div[contenteditable]").focus();
+ is(getHTMLEditor().documentIsEmpty, false,
+ "nsIEditor.documentIsEmpty should be false when editing host has text directly");
+
+ document.execCommand("selectall");
+ document.execCommand("delete");
+ todo_is(getHTMLEditor().documentIsEmpty, true,
+ "nsIEditor.documentIsEmpty should be true when all contents in editing host are deleted");
+ })();
+
+ document.designMode = "on";
+ (function test_with_designMode() {
+ document.body.innerHTML = "";
+ is(getHTMLEditor().documentIsEmpty, true,
+ "nsIEditor.documentIsEmpty should be true when <body> is empty in designMode");
+ document.body.focus();
+ is(getHTMLEditor().documentIsEmpty, true,
+ "nsIEditor.documentIsEmpty should be true when <body> is empty in designMode (after setting focus explicitly)");
+
+ document.body.innerHTML = "<div><br></div>";
+ is(getHTMLEditor().documentIsEmpty, false,
+ "nsIEditor.documentIsEmpty should be false when <body> has only an empty paragraph in designMode");
+
+ document.body.innerHTML = "<div>abc</div>";
+ is(getHTMLEditor().documentIsEmpty, false,
+ "nsIEditor.documentIsEmpty should be false when <body> has text in a paragraph in designMode");
+
+ document.body.innerHTML = "abc";
+ is(getHTMLEditor().documentIsEmpty, false,
+ "nsIEditor.documentIsEmpty should be false when <body> has text directly in designMode");
+
+ document.execCommand("selectall");
+ document.execCommand("delete");
+ todo_is(getHTMLEditor().documentIsEmpty, true,
+ "nsIEditor.documentIsEmpty should be true when all contents in designMode are deleted");
+ })();
+ document.designMode = "off";
+
+ document.body.innerHTML = originalBody;
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_insertLineBreak.html b/editor/libeditor/tests/test_nsIEditor_insertLineBreak.html
new file mode 100644
index 0000000000..02a847b640
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_insertLineBreak.html
@@ -0,0 +1,416 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsIEditor.insertLineBreak()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<input value="abcdef">
+<textarea>abcdef</textarea>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(0, 2); // In a11y module
+SimpleTest.waitForFocus(() => {
+ let input = document.getElementsByTagName("input")[0];
+ let textarea = document.getElementsByTagName("textarea")[0];
+ let contenteditable = document.getElementById("content");
+ let selection = window.getSelection();
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(event) {
+ beforeInputEvents.push(event);
+ }
+ function onInput(event) {
+ inputEvents.push(event);
+ }
+
+ function checkInputEvent(aEvent, aInputType, aTargetRanges, aDescription) {
+ ok(aEvent != null, `aEvent is null (${aDescription})`);
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface (${aDescription})`);
+ is(aEvent.cancelable, aEvent.type === "beforeinput",
+ `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable (${aDescription})`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble (${aDescription})`);
+ is(aEvent.inputType, aInputType,
+ `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`);
+ is(aEvent.data, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aTargetRanges.length === 0) {
+ is(targetRanges.length, 0,
+ `getTargetRange() of "${aEvent.type}" event should return empty array: ${aDescription}`);
+ } else {
+ is(targetRanges.length, aTargetRanges.length,
+ `getTargetRange() of "${aEvent.type}" event should return static range array: ${aDescription}`);
+ if (targetRanges.length == aTargetRanges.length) {
+ for (let i = 0; i < targetRanges.length; i++) {
+ is(targetRanges[i].startContainer, aTargetRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ is(targetRanges[i].startOffset, aTargetRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ is(targetRanges[i].endContainer, aTargetRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ is(targetRanges[i].endOffset, aTargetRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ }
+ }
+ }
+ }
+
+ input.focus();
+ input.selectionStart = input.selectionEnd = 3;
+ beforeInputEvents = [];
+ inputEvents = [];
+ input.addEventListener("beforeinput", onBeforeInput);
+ input.addEventListener("input", onInput);
+ try {
+ getPlaintextEditor(input).insertLineBreak();
+ } catch (e) {
+ ok(true, e.message);
+ }
+ input.removeEventListener("beforeinput", onBeforeInput);
+ input.removeEventListener("input", onInput);
+ is(input.value, "abcdef", "nsIEditor.insertLineBreak() should do nothing on single line editor");
+ is(beforeInputEvents.length, 1, 'nsIEditor.insertLineBreak() should cause a "beforeinput" event on single line editor');
+ checkInputEvent(beforeInputEvents[0], "insertLineBreak", [], "on single line editor");
+ is(inputEvents.length, 0, 'nsIEditor.insertLineBreak() should not cause "input" event on single line editor');
+
+ textarea.focus();
+ textarea.selectionStart = textarea.selectionEnd = 3;
+ beforeInputEvents = [];
+ inputEvents = [];
+ textarea.addEventListener("beforeinput", onBeforeInput);
+ textarea.addEventListener("input", onInput);
+ getPlaintextEditor(textarea).insertLineBreak();
+ textarea.removeEventListener("beforeinput", onBeforeInput);
+ textarea.removeEventListener("input", onInput);
+ is(textarea.value, "abc\ndef", "nsIEditor.insertLineBreak() should insert \\n into multi-line editor");
+ is(beforeInputEvents.length, 1, 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on multi-line editor');
+ checkInputEvent(beforeInputEvents[0], "insertLineBreak", [], "on multi-line editor");
+ is(inputEvents.length, 1, 'nsIEditor.insertLineBreak() should cause "input" event once on multi-line editor');
+ checkInputEvent(inputEvents[0], "insertLineBreak", [], "on multi-line editor");
+
+ // Note that despite of the name, insertLineBreak() should insert paragraph separator in HTMLEditor.
+
+ document.execCommand("defaultParagraphSeparator", false, "br");
+
+ contenteditable.innerHTML = "abcdef";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ let selectionContainer = contenteditable.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "abc<br>def",
+ 'nsIEditor.insertLineBreak() should insert <br> element into text node when defaultParagraphSeparator is "br"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has only text node when defaultParagraphSeparator is "br"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'on HTMLEditor (when defaultParagraphSeparator is "br")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has only text node when defaultParagraphSeparator is "br"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'on HTMLEditor (when defaultParagraphSeparator is "br")');
+
+ contenteditable.innerHTML = "<p>abcdef</p>";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
+ 'nsIEditor.insertLineBreak() should add <p> element after <p> element even when defaultParagraphSeparator is "br"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <p> element when defaultParagraphSeparator is "br"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "br")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <p> element when defaultParagraphSeparator is "br"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "br")');
+
+ contenteditable.innerHTML = "<div>abcdef</div>";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<div>abc<br>def</div>",
+ 'nsIEditor.insertLineBreak() should insert <br> element into <div> element when defaultParagraphSeparator is "br"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <div> element when defaultParagraphSeparator is "br"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "br")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <div> element when defaultParagraphSeparator is "br"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "br")');
+
+ contenteditable.innerHTML = "<pre>abcdef</pre>";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<pre>abc<br>def</pre>",
+ 'nsIEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is "br"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "br"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "br")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "br"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "br")');
+
+ document.execCommand("defaultParagraphSeparator", false, "p");
+
+ contenteditable.innerHTML = "abcdef";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
+ 'nsIEditor.insertLineBreak() should create <p> elements when there is only text node and defaultParagraphSeparator is "p"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has only text node when defaultParagraphSeparator is "p"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'on HTMLEditor (when defaultParagraphSeparator is "p")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has only text node when defaultParagraphSeparator is "p"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'on HTMLEditor (when defaultParagraphSeparator is "p")');
+
+ contenteditable.innerHTML = "<p>abcdef</p>";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
+ 'nsIEditor.insertLineBreak() should add <p> element after <p> element when defaultParagraphSeparator is "p"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <p> element when defaultParagraphSeparator is "p"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "p")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <p> element when defaultParagraphSeparator is "p"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "p")');
+
+ contenteditable.innerHTML = "<div>abcdef</div>";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<div>abc</div><div>def</div>",
+ 'nsIEditor.insertLineBreak() should add <div> element after <div> element even when defaultParagraphSeparator is "p"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <div> element when defaultParagraphSeparator is "p"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "p")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <div> element when defaultParagraphSeparator is "p"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "p")');
+
+ contenteditable.innerHTML = "<pre>abcdef</pre>";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<pre>abc<br>def</pre>",
+ 'nsIEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is "p"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "p"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "p")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "p"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "p")');
+
+ document.execCommand("defaultParagraphSeparator", false, "div");
+
+ contenteditable.innerHTML = "abcdef";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<div>abc</div><div>def</div>",
+ 'nsIEditor.insertLineBreak() should create <div> elements when there is only text node and defaultParagraphSeparator is "div"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has only text node when defaultParagraphSeparator is "div"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'on HTMLEditor (when defaultParagraphSeparator is "div")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has only text node when defaultParagraphSeparator is "div"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'on HTMLEditor (when defaultParagraphSeparator is "div")');
+
+ contenteditable.innerHTML = "<p>abcdef</p>";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
+ 'nsIEditor.insertLineBreak() should add <p> element after <p> element even when defaultParagraphSeparator is "div"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <p> element when defaultParagraphSeparator is "div"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "div")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <p> element when defaultParagraphSeparator is "div"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "div")');
+
+ contenteditable.innerHTML = "<div>abcdef</div>";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<div>abc</div><div>def</div>",
+ 'nsIEditor.insertLineBreak() should add <div> element after <div> element when defaultParagraphSeparator is "div"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <div> element when defaultParagraphSeparator is "div"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "div")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <div> element when defaultParagraphSeparator is "div"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "div")');
+
+ contenteditable.innerHTML = "<pre>abcdef</pre>";
+ contenteditable.focus();
+ contenteditable.scrollTop;
+ selectionContainer = contenteditable.firstChild.firstChild;
+ selection.collapse(selectionContainer, 3);
+ beforeInputEvents = [];
+ inputEvents = [];
+ contenteditable.addEventListener("beforeinput", onBeforeInput);
+ contenteditable.addEventListener("input", onInput);
+ getPlaintextEditor(contenteditable).insertLineBreak();
+ contenteditable.removeEventListener("beforeinput", onBeforeInput);
+ contenteditable.removeEventListener("input", onInput);
+ is(contenteditable.innerHTML, "<pre>abc<br>def</pre>",
+ 'nsIEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is "div"');
+ is(beforeInputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "div"');
+ checkInputEvent(beforeInputEvents[0], "insertParagraph",
+ [{startContainer: selectionContainer, startOffset: 3,
+ endContainer: selectionContainer, endOffset: 3}],
+ 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "div")');
+ is(inputEvents.length, 1,
+ 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "div"');
+ checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "div")');
+
+ SimpleTest.finish();
+});
+
+function getPlaintextEditor(aEditorElement) {
+ let editor = aEditorElement ? SpecialPowers.wrap(aEditorElement).editor : null;
+ if (!editor) {
+ editor = SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
+ }
+ return editor;
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_insertNode.html b/editor/libeditor/tests/test_nsIEditor_insertNode.html
new file mode 100644
index 0000000000..47aa1e2c0a
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_insertNode.html
@@ -0,0 +1,214 @@
+<!doctype>
+<html>
+<head>
+<meta charset="utf-8">
+<title>nsIEditor.insertNode</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+function stringifyInputEvent(aEvent) {
+ if (!aEvent) {
+ return "null";
+ }
+ return `${aEvent.type}: { inputType=${aEvent.inputType} }`;
+}
+
+function getRangeDescription(range) {
+ function getNodeDescription(node) {
+ if (!node) {
+ return "null";
+ }
+ switch (node.nodeType) {
+ case Node.TEXT_NODE:
+ return `${node.nodeName} "${node.data}"`;
+ case Node.ELEMENT_NODE:
+ return `<${node.nodeName.toLowerCase()}>`;
+ default:
+ return `${node.nodeName}`;
+ }
+ }
+ if (range === null) {
+ return "null";
+ }
+ if (range === undefined) {
+ return "undefined";
+ }
+ return range.startContainer == range.endContainer &&
+ range.startOffset == range.endOffset
+ ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
+ : `(${getNodeDescription(range.startContainer)}, ${
+ range.startOffset
+ }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ const editingHost = document.querySelector("div[contenteditable]");
+ const editor =
+ SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
+
+ editingHost.focus();
+
+ let events = [];
+ editingHost.addEventListener("input", event => events.push(event));
+
+ (function test_insert_text_to_start() {
+ editor.insertNode(document.createTextNode("abc"), editingHost, 0);
+ is(
+ editingHost.innerHTML,
+ "abc<br>",
+ "test_insert_text_to_start: insertNode() should insert new text node at start of the container"
+ );
+ is(
+ events.length,
+ 1,
+ "test_insert_text_to_start: Only one input event should be fired when insertNode() inserts a text node"
+ );
+ is(
+ stringifyInputEvent(events[0]),
+ stringifyInputEvent({ type: "input", inputType: "" }),
+ "test_insert_text_to_start: input event should be fired when inserting a node"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost,
+ startOffset: 1,
+ endContainer: editingHost,
+ endOffset: 1,
+ }),
+ "test_insert_text_to_start: insertNode() should collapse selection after the inserted text node"
+ );
+ })();
+
+ (function test_insert_span_to_big_index() {
+ events = [];
+ editingHost.innerHTML = "abc";
+ const span = document.createElement("span");
+ span.textContent = "def";
+ editor.insertNode(span, editingHost, 1000);
+ is(
+ editingHost.innerHTML,
+ "abc<span>def</span>",
+ "test_insert_span_to_big_index: insertNode() with big index should insert new node at end of the container"
+ );
+ is(
+ events.length,
+ 1,
+ "test_insert_span_to_big_index: Only one input event should be fired when insertNode() inserts a node"
+ );
+ is(
+ stringifyInputEvent(events[0]),
+ stringifyInputEvent({ type: "input", inputType: "" }),
+ "test_insert_span_to_big_index: input event should be fired when inserting a node"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost,
+ startOffset: 2,
+ endContainer: editingHost,
+ endOffset: 2,
+ }),
+ "test_insert_span_to_big_index: insertNode() should collapse selection after the inserted node"
+ );
+ })();
+
+ (function test_preserve_selection() {
+ events = [];
+ editingHost.innerHTML = "abc";
+ const span = document.createElement("span");
+ span.textContent = "def";
+ getSelection().collapse(editingHost, 0);
+ editor.insertNode(span, editingHost, 1, true);
+ is(
+ editingHost.innerHTML,
+ "abc<span>def</span>",
+ "test_preserve_selection: insertNode() should insert new node at end of the container"
+ );
+ is(
+ events.length,
+ 1,
+ "test_preserve_selection: Only one input event should be fired when insertNode() inserts a node"
+ );
+ is(
+ stringifyInputEvent(events[0]),
+ stringifyInputEvent({ type: "input", inputType: "" }),
+ "test_preserve_selection: input event should be fired when inserting a node"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost,
+ startOffset: 0,
+ endContainer: editingHost,
+ endOffset: 0,
+ }),
+ "test_preserve_selection: insertNode() should not collapse selection after the inserted node"
+ );
+ })();
+
+ (function test_not_preserve_selection_nested_by_beforeinput() {
+ editingHost.innerHTML = "abc";
+ const span1 = document.createElement("span");
+ span1.textContent = "def";
+ const span2 = document.createElement("span");
+ span2.textContent = "ghi";
+ getSelection().collapse(editingHost, 0);
+ editingHost.addEventListener("beforeinput", () => {
+ editor.insertNode(span1, editingHost, 1);
+ }, {once: true});
+ editor.insertNode(span2, editingHost, 2, true);
+ is(
+ editingHost.innerHTML,
+ "abc<span>def</span><span>ghi</span>",
+ "test_not_preserve_selection_nested_by_beforeinput: both insertNode() should work"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost,
+ startOffset: 2,
+ endContainer: editingHost,
+ endOffset: 2,
+ }),
+ "test_not_preserve_selection_nested_by_beforeinput: only insertNode() called in beforeinput listener should update selection"
+ );
+ })();
+
+ (function test_not_preserve_selection_nested_by_input() {
+ editingHost.innerHTML = "abc";
+ const span1 = document.createElement("span");
+ span1.textContent = "def";
+ const span2 = document.createElement("span");
+ span2.textContent = "ghi";
+ getSelection().collapse(editingHost, 0);
+ editingHost.addEventListener("input", () => {
+ editor.insertNode(span2, editingHost, 2);
+ }, {once: true});
+ editor.insertNode(span1, editingHost, 1, true);
+ is(
+ editingHost.innerHTML,
+ "abc<span>def</span><span>ghi</span>",
+ "test_not_preserve_selection_nested_by_input: both insertNode() should work"
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ getRangeDescription({
+ startContainer: editingHost,
+ startOffset: 3,
+ endContainer: editingHost,
+ endOffset: 3,
+ }),
+ "test_not_preserve_selection_nested_by_input: only insertNode() called in input listener should update selection"
+ );
+ })();
+
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body><div contenteditable><br></div></body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_isSelectionEditable.html b/editor/libeditor/tests/test_nsIEditor_isSelectionEditable.html
new file mode 100644
index 0000000000..92c4ba1aee
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_isSelectionEditable.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Test for nsIEditor.isSelectionEditable</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<input>
+<textarea></textarea>
+<script>
+for (let tag of ["input", "textarea"]) {
+ let node = document.querySelector(tag);
+ ok(SpecialPowers.wrap(node).editor.isSelectionEditable, "Empty editor selection should be editable");
+ node.value = "abcd";
+ ok(SpecialPowers.wrap(node).editor.isSelectionEditable, "Non-empty editor selection should be editable");
+ node.value = "";
+ ok(SpecialPowers.wrap(node).editor.isSelectionEditable, "Empty editor selection should be editable after setting value");
+}
+</script>
diff --git a/editor/libeditor/tests/test_nsIEditor_outputToString.html b/editor/libeditor/tests/test_nsIEditor_outputToString.html
new file mode 100644
index 0000000000..9d2e83245b
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_outputToString.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests of nsIEditor#outputToString()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(() => {
+ const originalBody = document.body.innerHTML;
+ const Ci = SpecialPowers.Ci;
+
+ /**
+ * TODO: Add "text/html" cases and other `nsIDocumentEncoder.*` options.
+ */
+ (function test_with_text_editor() {
+ for (const test of [
+ {
+ tag: "input",
+ innerHTML: "<input>",
+ },
+ {
+ tag: "textarea",
+ innerHTML: "<textarea></textarea>",
+ },
+ ]) {
+ document.body.innerHTML = test.innerHTML;
+ const textControl = document.body.querySelector(test.tag);
+ const editor = SpecialPowers.wrap(textControl).editor;
+ is(
+ editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw),
+ "",
+ `outputToString("text/plain", OutputRaw) for <${test.tag}> should return empty string (before focused)`
+ );
+ textControl.focus();
+ is(
+ editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw),
+ "",
+ `outputToString("text/plain", OutputRaw) for <${test.tag}> should return empty string (after focused)`
+ );
+ textControl.value = "abc";
+ is(
+ editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw),
+ "abc",
+ `outputToString("text/plain", OutputRaw) for <${test.tag}> whose value is "abc" should return the value as-is`
+ );
+ if (editor.flags & Ci.nsIEditor.eEditorSingleLineMask) {
+ continue;
+ }
+ textControl.value = "abc\ndef";
+ is(
+ editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""),
+ "abc\ndef",
+ `outputToString("text/plain", OutputRaw) for <${test.tag}> whose value is "abc\ndef" should return the value as-is`
+ );
+ textControl.value = "abc\ndef\n";
+ is(
+ editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""),
+ "abc\ndef\n",
+ `outputToString("text/plain", OutputRaw) for <${test.tag}> whose value is "abc\ndef\n" should return the value as-is`
+ );
+ textControl.value = "abc\ndef\n\n";
+ is(
+ editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""),
+ "abc\ndef\n\n",
+ `outputToString("text/plain", OutputRaw) for <${test.tag}> whose value is "abc\ndef\n\n" should return the value as-is`
+ );
+ }
+ })();
+
+ function getHTMLEditor() {
+ const editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ if (!editingSession) {
+ return null;
+ }
+ return editingSession.getEditorForWindow(window);
+ }
+
+ (function test_with_contenteditable() {
+ document.body.setAttribute("contenteditable", "");
+ document.body.blur();
+ document.body.innerHTML = "";
+ is(
+ getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""),
+ "",
+ `outputToString("text/plain", OutputRaw) for empty <body contenteditable> should return empty string (before focused)`
+ );
+ document.body.focus();
+ is(
+ getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""),
+ "", // Ignore the padding <br> element for empty editor.
+ `outputToString("text/plain", OutputRaw) for empty <body contenteditable> should return empty string (after focused)`
+ );
+ const sourceHasParagraphsAndDivs = "<p>abc</p><p>def<br></p><div>ghi</div><div>jkl<br>mno<br></div>";
+ document.body.innerHTML = sourceHasParagraphsAndDivs;
+ // XXX Oddly, an ASCII white-space is inserted at the head of the result.
+ todo_is(
+ getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""),
+ sourceHasParagraphsAndDivs.replace(/<br>/gi, "\n").replace(/<[^>]+>/g, ""),
+ `outputToString("text/plain", OutputRaw) for <body contenteditable> should return the expected string`
+ );
+
+ document.body.removeAttribute("contenteditable");
+ document.body.innerHTML = "<div contenteditable></div>";
+ is(
+ getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""),
+ "",
+ `outputToString("text/plain", OutputRaw) for empty <div contenteditable> should return empty string (before focused)`
+ );
+ document.body.querySelector("div[contenteditable]").focus();
+ is(
+ getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""),
+ "", // Ignore the padding <br> element for empty editor.
+ `outputToString("text/plain", OutputRaw) for empty <div contenteditable> should return empty string (after focused)`
+ );
+ document.body.querySelector("div[contenteditable]").innerHTML = sourceHasParagraphsAndDivs;
+ is(
+ getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""),
+ sourceHasParagraphsAndDivs.replace(/<br>/gi, "\n").replace(/<[^>]+>/g, ""),
+ `outputToString("text/plain", OutputRaw) for <div contenteditable> should return the expected string`
+ );
+ })();
+
+ document.body.innerHTML = originalBody;
+ SimpleTest.finish();
+ });
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_undoAll.html b/editor/libeditor/tests/test_nsIEditor_undoAll.html
new file mode 100644
index 0000000000..45e82e884b
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_undoAll.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Test for nsIEditor.undoAll()</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content"><input><textarea></textarea><div contenteditable></div></div>
+<pre id="test">
+<script>
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ function isTextEditor(aElement) {
+ return aElement.tagName.toLowerCase() == "input" ||
+ aElement.tagName.toLowerCase() == "textarea";
+ }
+ function getEditor(aElement) {
+ if (isTextEditor(aElement)) {
+ return SpecialPowers.wrap(aElement).editor;
+ }
+ return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window);
+ }
+ function setValue(aElement, aValue) {
+ if (isTextEditor(aElement)) {
+ aElement.value = aValue;
+ return;
+ }
+ aElement.innerHTML = aValue;
+ }
+ function getValue(aElement) {
+ if (isTextEditor(aElement)) {
+ return aElement.value;
+ }
+ return aElement.innerHTML.replace(/<br>/g, "");
+ }
+ for (const selector of ["input", "textarea", "div[contenteditable]"]) {
+ const editableElement = document.querySelector(selector);
+ editableElement.focus();
+ const editor = getEditor(editableElement);
+ setValue(editableElement, "");
+ is(
+ editor.canUndo,
+ false,
+ `Editor for ${selector} shouldn't have undo transaction at start`
+ );
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} shouldn't have redo transaction at start`
+ );
+
+ synthesizeKey("b");
+ is(
+ getValue(editableElement),
+ "b",
+ `Editor for ${selector} should've handled inserting "b"`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `Editor for ${selector} should have undo transaction after inserting "b"`
+ );
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} shouldn't have redo transaction after inserting "b"`
+ );
+
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("a");
+ is(
+ getValue(editableElement),
+ "ab",
+ `Editor for ${selector} should've handled inserting "a" before "b"`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `Editor for ${selector} should have undo transaction after inserting text again`
+ );
+ is(
+ editor.canRedo,
+ false,
+ `Editor for ${selector} should have redo transaction after inserting text again`
+ );
+
+ editor.undoAll();
+ is(
+ getValue(editableElement),
+ "",
+ `Editor for ${selector} should've undone everything`
+ );
+ is(
+ editor.canUndo,
+ false,
+ `Editor for ${selector} shouldn't have undo transactions after undoAll() called`
+ );
+ is(
+ editor.canRedo,
+ true,
+ `Editor for ${selector} should have redo transaction after undoAll() called`
+ );
+
+ }
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIEditor_undoRedoEnabled.html b/editor/libeditor/tests/test_nsIEditor_undoRedoEnabled.html
new file mode 100644
index 0000000000..1a0841ea1e
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIEditor_undoRedoEnabled.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Test for nsIEditor.undoRedoEnabled</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content"><input><textarea></textarea><div contenteditable></div></div>
+<pre id="test">
+<script>
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ function isTextEditor(aElement) {
+ return aElement.tagName.toLowerCase() == "input" ||
+ aElement.tagName.toLowerCase() == "textarea";
+ }
+ function getEditor(aElement) {
+ if (isTextEditor(aElement)) {
+ return SpecialPowers.wrap(aElement).editor;
+ }
+ return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window);
+ }
+ function setValue(aElement, aValue) {
+ if (isTextEditor(aElement)) {
+ aElement.value = aValue;
+ return;
+ }
+ aElement.innerHTML = aValue;
+ }
+ function getValue(aElement) {
+ if (isTextEditor(aElement)) {
+ return aElement.value;
+ }
+ return aElement.innerHTML.replace(/<br>/g, "");
+ }
+ for (const selector of ["input", "textarea", "div[contenteditable]"]) {
+ const editableElement = document.querySelector(selector);
+ editableElement.focus();
+ const editor = getEditor(editableElement);
+ setValue(editableElement, "");
+ is(
+ editor.undoRedoEnabled,
+ true,
+ `undo/redo in editor for ${selector} should be enabled by default`
+ );
+ editor.enableUndo(false);
+ is(
+ editor.undoRedoEnabled,
+ false,
+ `undo/redo in editor for ${selector} should be disable after calling enableUndo(false)`
+ );
+ synthesizeKey("a");
+ is(
+ getValue(editableElement),
+ "a",
+ `inserting text should be handled by editor for ${selector} even if undo/redo is disabled`
+ );
+ is(
+ editor.canUndo,
+ false,
+ `undo transaction shouldn't be created by editor for ${selector} when undo/redo is disabled`
+ );
+ editor.enableUndo(true);
+ is(
+ editor.undoRedoEnabled,
+ true,
+ `undo/redo in editor for ${selector} should be enabled after calling enableUndo(true)`
+ );
+ synthesizeKey("b");
+ is(
+ getValue(editableElement),
+ "ab",
+ `inserting text should be handled by editor for ${selector} after enabling undo/redo`
+ );
+ is(
+ editor.canUndo,
+ true,
+ `undo transaction should be created by editor for ${selector} when undo/redo is enabled again`
+ );
+ }
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_getElementOrParentByTagName.html b/editor/libeditor/tests/test_nsIHTMLEditor_getElementOrParentByTagName.html
new file mode 100644
index 0000000000..c78ec6763d
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIHTMLEditor_getElementOrParentByTagName.html
@@ -0,0 +1,449 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsIHTMLEditor.getElementOrParentByTagName()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<section>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+</section>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ let section = document.querySelector("section");
+ let editor = document.querySelector("[contenteditable]");
+ let selection = window.getSelection();
+ let element;
+
+ // Make sure that each test can run without previous tests for making each test
+ // debuggable with commenting out the unrelated tests.
+
+ try {
+ editor.focus();
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("", null));
+ ok(false, "nsIHTMLEditor.getElementOrParentByTagName(\"\", null) should throw an exception");
+ } catch {
+ ok(true, "nsIHTMLEditor.getElementOrParentByTagName(\"\", null) should throw an exception");
+ }
+
+ try {
+ editor.focus();
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName(null, null));
+ ok(false, "nsIHTMLEditor.getElementOrParentByTagName(null, null) should throw an exception");
+ } catch {
+ ok(true, "nsIHTMLEditor.getElementOrParentByTagName(null, null) should throw an exception");
+ }
+
+ try {
+ editor.focus();
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName(undefined, null));
+ ok(false, "nsIHTMLEditor.getElementOrParentByTagName(undefined, null) should throw an exception");
+ } catch {
+ ok(true, "nsIHTMLEditor.getElementOrParentByTagName(undefined, null) should throw an exception");
+ }
+
+ editor.focus();
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("undefinedtagname", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"undefinedtagname\", null) should return null");
+
+ editor.blur();
+ selection.collapse(document.getElementById("display"), 0);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("section", null));
+ is(element, section,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"section\", null) should return the <section> when selection is in the it (HTML editor does not have focus)");
+
+ editor.focus();
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("section", null));
+ is(element, section,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"section\", null) should return the <section> when selection is in the it (HTML editor has focus)");
+
+ editor.focus();
+ selection.removeAllRanges();
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("section", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"section\", null) should return null when there is no selection");
+
+ editor.blur();
+ selection.collapse(document.getElementById("display"), 0);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("body", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"body\", null) should return null when it reaches the <body>");
+
+ editor.blur();
+ selection.collapse(document.getElementById("display"), 0);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("div", editor));
+ is(element, editor,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"div\", editor) should return editor even when selection is outside of it");
+
+ editor.innerHTML = "<p>first</p><p>second</p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild.firstChild, 0, editor.firstChild.nextSibling.firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("p", null));
+ is(element, editor.firstChild,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"p\", null) should return first <p> element when selection anchor is in it");
+
+ editor.innerHTML = "<table><tr><td>cell</td></tr></table>";
+ editor.focus();
+ selection.collapse(editor.querySelector("td").firstChild, 2);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("td", null));
+ is(element, editor.querySelector("td"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"td\", null) should return the <td> when selection is collapsed in it");
+
+ editor.innerHTML = "<table><tr><td>cell</td></tr></table>";
+ editor.focus();
+ selection.collapse(editor.querySelector("td").firstChild, 2);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("th", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"th\", null) should return null when selection is collapsed in <td>");
+
+ editor.innerHTML = "<table><tr><td>cell</td></tr></table>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("td", null));
+ is(element, editor.querySelector("td"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"td\", null) should return the <td> when it's selected");
+
+ editor.innerHTML = "<table><tr><th>cell</th></tr></table>";
+ editor.focus();
+ selection.collapse(editor.querySelector("th").firstChild, 2);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("td", null));
+ is(element, editor.querySelector("th"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"td\", null) should return the <th> when selection is collapsed in it");
+
+ editor.innerHTML = "<table><tr><th>cell</th></tr></table>";
+ editor.focus();
+ selection.collapse(editor.querySelector("th").firstChild, 2);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("th", null));
+ is(element, editor.querySelector("th"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"th\", null) should return the <th> when selection is collapsed in it");
+
+ editor.innerHTML = "<table><tr><th>cell</th></tr></table>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("td", null));
+ is(element, editor.querySelector("th"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"td\", null) should return the <th> when it's selected");
+
+ editor.innerHTML = "<table><tr><th>cell</th></tr></table>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("th", null));
+ is(element, editor.querySelector("th"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"th\", null) should return the <th> when it's selected");
+
+ editor.innerHTML = "<ul><li>listitem</li></ul>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ul", null));
+ is(element, editor.querySelector("ul"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"ul\", null) should return the <ul> when selection is collapsed in its <li>");
+
+ editor.innerHTML = "<ul><li>listitem</li></ul>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null));
+ is(element, editor.querySelector("ul"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <ul> when selection is collapsed in its <li>");
+
+ editor.innerHTML = "<ul><li>listitem</li></ul>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ol", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return null when selection is collapsed in <ul>");
+
+ editor.innerHTML = "<ol><li>listitem</li></ol>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ol", null));
+ is(element, editor.querySelector("ol"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return the <ol> when selection is collapsed in its <li>");
+
+ editor.innerHTML = "<ol><li>listitem</li></ol>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null));
+ is(element, editor.querySelector("ol"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <ol> when selection is collapsed in its <li>");
+
+ editor.innerHTML = "<ol><li>listitem</li></ol>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ul", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return null when selection is collapsed in <ol>");
+
+ editor.innerHTML = "<dl><dt>listitem</dt></dl>";
+ editor.focus();
+ selection.collapse(editor.querySelector("dt").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("dl", null));
+ is(element, editor.querySelector("dl"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"dl\", null) should return the <dl> when selection is collapsed in its <dt>");
+
+ editor.innerHTML = "<dl><dt>listitem</dt></dl>";
+ editor.focus();
+ selection.collapse(editor.querySelector("dt").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null));
+ is(element, editor.querySelector("dl"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <dl> when selection is collapsed in its <dt>");
+
+ editor.innerHTML = "<dl><dd>listitem</dd></dl>";
+ editor.focus();
+ selection.collapse(editor.querySelector("dd").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("dl", null));
+ is(element, editor.querySelector("dl"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"dl\", null) should return the <dl> when selection is collapsed in its <dd>");
+
+ editor.innerHTML = "<dl><dd>listitem</dd></dl>";
+ editor.focus();
+ selection.collapse(editor.querySelector("dd").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null));
+ is(element, editor.querySelector("dl"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <dl> when selection is collapsed in its <dd>");
+
+ editor.innerHTML = "<ul><ol><li>listitem</li></ol></ul>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null));
+ is(element, editor.querySelector("ol"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <ol> (sublist) when selection is collapsed in its <li>");
+
+ editor.innerHTML = "<ul><ol><li>listitem</li></ol></ul>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ol", null));
+ is(element, editor.querySelector("ol"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return the <ol> (sublist) when selection is collapsed in its <li>");
+
+ editor.innerHTML = "<ul><ol><li>listitem</li></ol></ul>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ul", null));
+ is(element, editor.querySelector("ul"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"ul\", null) should return the <ul> when selection is collapsed in its sublist's <li>");
+
+ editor.innerHTML = "<ol><ul><li>listitem</li></ul></ol>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null));
+ is(element, editor.querySelector("ul"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <ul> (sublist) when selection is collapsed in its <li>");
+
+ editor.innerHTML = "<ol><ul><li>listitem</li></ul></ol>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ul", null));
+ is(element, editor.querySelector("ul"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"ul\", null) should return the <ul> (sublist) when selection is collapsed in its <li>");
+
+ editor.innerHTML = "<ol><ul><li>listitem</li></ul></ol>";
+ editor.focus();
+ selection.collapse(editor.querySelector("li").firstChild, 4);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ol", null));
+ is(element, editor.querySelector("ol"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return the <ol> when selection is collapsed in its sublist's <li>");
+
+ editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a href=\"about:config\"> when selection is collapsed in it");
+
+ editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a href=\"about:config\"> when it's selected");
+
+ editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return the <a href=\"about:config\"> when selection is collapsed in it");
+
+ editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return the <a href=\"about:config\"> when it's selected");
+
+ editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when selection is collapsed in the <a href=\"about:config\">");
+
+ editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when the <a href=\"about:config\"> is selected");
+
+ editor.innerHTML = "<p><a href=\"\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a href=\"\"> when selection is collapsed in it");
+
+ editor.innerHTML = "<p><a href=\"\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a href=\"\"> when it's selected");
+
+ editor.innerHTML = "<p><a href=\"\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return the <a href=\"\"> when selection is collapsed in it");
+
+ editor.innerHTML = "<p><a href=\"\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return the <a href=\"\"> when it's selected");
+
+ editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a name=\"foo\"> when selection is collapsed in it");
+
+ editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a name=\"foo\"> when it's selected");
+
+ editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return null when selection is collapsed in the <a name=\"foo\">");
+
+ editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return null when the <a name=\"foo\"> is selected");
+
+ editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return the <a name=\"foo\"> when selection is collapsed in it");
+
+ editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return the <a name=\"foo\"> when it's selected");
+
+ editor.innerHTML = "<p><a name=\"\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a name=\"\"> when selection is collapsed in it");
+
+ editor.innerHTML = "<p><a name=\"\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a name=\"\"> when it's selected");
+
+ editor.innerHTML = "<p><a name=\"\">anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when selection is collapsed in the <a name=\"\">");
+
+ editor.innerHTML = "<p><a name=\"\">anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when the <a name=\"\"> is selected");
+
+ editor.innerHTML = "<p><a>anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a> when selection is collapsed in it");
+
+ editor.innerHTML = "<p><a>anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return null when selection is collapsed in the <a>");
+
+ editor.innerHTML = "<p><a>anchor</a></p>";
+ editor.focus();
+ selection.collapse(editor.querySelector("a").firstChild, 3);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when selection is collapsed in the <a>");
+
+ editor.innerHTML = "<p><a>anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null));
+ is(element, editor.querySelector("a"),
+ "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a> when it's selected");
+
+ editor.innerHTML = "<p><a>anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return null when the <a> is selected");
+
+ editor.innerHTML = "<p><a>anchor</a></p>";
+ editor.focus();
+ selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1);
+ element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null));
+ is(element, null,
+ "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return the <a> is selected");
+
+ SimpleTest.finish();
+});
+
+function getHTMLEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_getParagraphState.html b/editor/libeditor/tests/test_nsIHTMLEditor_getParagraphState.html
new file mode 100644
index 0000000000..0297464c3f
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIHTMLEditor_getParagraphState.html
@@ -0,0 +1,156 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsIHTMLEditor.getParagraphState()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+ let selection = window.getSelection();
+ let tag, range, mixed = {};
+
+ editor.focus();
+ editor.blur();
+ selection.removeAllRanges();
+
+ try {
+ tag = getHTMLEditor().getParagraphState(mixed);
+ ok(false, "nsIHTMLEditor.getParagraphState() should throw exception when there is no selection range");
+ } catch (e) {
+ ok(true, "nsIHTMLEditor.getParagraphState() should throw exception when there is no selection range");
+ }
+
+ range = document.createRange();
+ range.setStart(document, 0);
+ selection.addRange(range);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, "x", "nsIHTMLEditor.getParagraphState() should return \"x\" when selection range starts from document node");
+ is(mixed.value, false, "nsIHTMLEditor.getParagraphState() should return false for mixed state when selection range starts from document node");
+
+ editor.focus();
+ selection.collapse(editor, 0);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, "x", "nsIHTMLEditor.getParagraphState() should return \"x\" when the editing host is empty");
+ is(mixed.value, false, "nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host is empty");
+
+ editor.innerHTML = "foo";
+ selection.collapse(editor.firstChild, 0);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, "", "nsIHTMLEditor.getParagraphState() should return \"\" when the editing host has only text node");
+ is(mixed.value, false, "nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has only text node");
+
+ for (let test of [
+ {tag: "p",
+ expected: {tag: "p", tagIfEmpty: "p"}},
+ {tag: "pre",
+ expected: {tag: "pre", tagIfEmpty: "pre"}},
+ {tag: "h1",
+ expected: {tag: "h1", tagIfEmpty: "h1"}},
+ {tag: "h2",
+ expected: {tag: "h2", tagIfEmpty: "h2"}},
+ {tag: "h3",
+ expected: {tag: "h3", tagIfEmpty: "h3"}},
+ {tag: "h4",
+ expected: {tag: "h4", tagIfEmpty: "h4"}},
+ {tag: "h5",
+ expected: {tag: "h5", tagIfEmpty: "h5"}},
+ {tag: "h6",
+ expected: {tag: "h6", tagIfEmpty: "h6"}},
+ {tag: "address",
+ expected: {tag: "address", tagIfEmpty: "address"}},
+ {tag: "span",
+ expected: {tag: "", tagIfEmpty: ""}},
+ {tag: "b",
+ expected: {tag: "", tagIfEmpty: ""}},
+ {tag: "i",
+ expected: {tag: "", tagIfEmpty: ""}},
+ {tag: "em",
+ expected: {tag: "", tagIfEmpty: ""}},
+ {tag: "div",
+ expected: {tag: "", tagIfEmpty: "x"}},
+ {tag: "section",
+ expected: {tag: "", tagIfEmpty: "x"}},
+ {tag: "article",
+ expected: {tag: "", tagIfEmpty: "x"}},
+ {tag: "header",
+ expected: {tag: "", tagIfEmpty: "x"}},
+ {tag: "main",
+ expected: {tag: "", tagIfEmpty: "x"}},
+ {tag: "footer",
+ expected: {tag: "", tagIfEmpty: "x"}},
+ {tag: "aside",
+ expected: {tag: "", tagIfEmpty: "x"}},
+ {tag: "blockquote",
+ expected: {tag: "", tagIfEmpty: "x"}},
+ {tag: "form",
+ expected: {tag: "", tagIfEmpty: "x"}},
+ ]) {
+ editor.innerHTML = `<${test.tag}></${test.tag}>`;
+ selection.collapse(editor.firstChild, 0);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, test.expected.tagIfEmpty, `nsIHTMLEditor.getParagraphState() should return "${test.expected.tagIfEmpty}" when the editing host has an empty <${test.tag}>`);
+ is(mixed.value, false, `nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has an empty <${test.tag}>`);
+
+ editor.innerHTML = `<${test.tag}>foo</${test.tag}>`;
+ selection.collapse(editor.firstChild.firstChild, 0);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, test.expected.tag, `nsIHTMLEditor.getParagraphState() should return "${test.expected.tag}" when the editing host has a <${test.tag}> which has a text node`);
+ is(mixed.value, false, `nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has a <${test.tag}> which has a text node`);
+
+ editor.innerHTML = `<${test.tag}><span>foo</span></${test.tag}>`;
+ selection.collapse(editor.firstChild.firstChild.firstChild, 0);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, test.expected.tag, `nsIHTMLEditor.getParagraphState() should return "${test.expected.tag}" when the editing host has a <${test.tag}> which has a <span>`);
+ is(mixed.value, false, `nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has a <${test.tag}> which has a <span>`);
+
+ editor.innerHTML = `<${test.tag}>foo</${test.tag}>`;
+ selection.collapse(editor.firstChild, 1);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, test.expected.tag, `nsIHTMLEditor.getParagraphState() should return "${test.expected.tag}" when the editing host has a <${test.tag}> which has a text node (selection collapsed at end of the element)`);
+ is(mixed.value, false, `nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has a <${test.tag}> which has a text node (selection collapsed at end of the element)`);
+ }
+
+ editor.innerHTML = "<main><h1>header1</h1><section><h2>header2</h2><article><h3>header3</h3><p>paragraph</p><pre>preformat</pre></article></section></main>";
+
+ selection.setBaseAndExtent(document.querySelector("[contenteditable] h1").firstChild, 0,
+ document.querySelector("[contenteditable] h2").firstChild, 0);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, "h2", "nsIHTMLEditor.getParagraphState() should return \"h1\" when between <h1> and <h2> is selected");
+ is(mixed.value, true, "nsIHTMLEditor.getParagraphState() should return true for mixed state when between <h1> and <h2> is selected");
+
+ selection.setBaseAndExtent(document.querySelector("[contenteditable] h1").firstChild, 0,
+ document.querySelector("[contenteditable] h3").firstChild, 0);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, "h3", "nsIHTMLEditor.getParagraphState() should return \"h3\" when between <h1> and <h3> is selected (whole of <h2> is also selected)");
+ is(mixed.value, true, "nsIHTMLEditor.getParagraphState() should return true for mixed state when between <h1> and <h3> is selected (whole of <h2> is also selected)");
+
+ selection.setBaseAndExtent(document.querySelector("[contenteditable] p").firstChild, 0,
+ document.querySelector("[contenteditable] pre").firstChild, 0);
+ tag = getHTMLEditor().getParagraphState(mixed);
+ is(tag, "pre", "nsIHTMLEditor.getParagraphState() should return \"pre\" when between <p> and <pre> is selected");
+ is(mixed.value, true, "nsIHTMLEditor.getParagraphState() should return true for mixed state when between <p> and <pre> is selected");
+
+ SimpleTest.finish();
+});
+
+function getHTMLEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_getSelectedElement.html b/editor/libeditor/tests/test_nsIHTMLEditor_getSelectedElement.html
new file mode 100644
index 0000000000..009591c252
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIHTMLEditor_getSelectedElement.html
@@ -0,0 +1,816 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsIHTMLEditor.getSelectedElement()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<img src="green.png"><!-- necessary to load this image before start testing -->
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ let editor = document.getElementById("content");
+ let selection = window.getSelection();
+
+ // nsIHTMLEditor.getSelectedElement() is probably designed to retrieve
+ // a[href]:not([href=""]), a[name]:not([name=""]), or void element like
+ // <img> element. When user selects usual inline elements with dragging,
+ // double-clicking, Gecko sets start and/or end to point in text nodes
+ // as far as possible. Therefore, this API users don't expect that this
+ // returns usual inline elements like <b> element.
+
+ // So, we need to check user's operation works fine.
+ for (let eatSpaceToNextWord of [true, false]) {
+ await SpecialPowers.pushPrefEnv({"set": [["layout.word_select.eat_space_to_next_word", eatSpaceToNextWord]]});
+
+ editor.innerHTML = "<p>This <b>is</b> an <i>example </i>text.<br></p>" +
+ "<p>and an image <img src=\"green.png\"> is here.</p>" +
+ "<p>An anchor with href attr <a href=\"about:blank\">is</a> here.</p>";
+ editor.focus();
+ editor.scrollTop; // flush layout.
+
+ let b = editor.firstChild.firstChild.nextSibling;
+ let i = b.nextSibling.nextSibling;
+ let img = editor.firstChild.nextSibling.firstChild.nextSibling;
+ let href = editor.firstChild.nextSibling.nextSibling.firstChild.nextSibling;
+
+ // double clicking usual inline element shouldn't cause "selecting" the element.
+ synthesizeMouseAtCenter(b, {clickCount: 1});
+ synthesizeMouseAtCenter(b, {clickCount: 2});
+ is(selection.getRangeAt(0).startContainer, b.previousSibling,
+ `#0-1 Double-clicking in <b> element should set start of selection to end of previous text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).startOffset, b.previousSibling.length,
+ `#0-1 Double-clicking in <b> element should set start of selection to end of previous text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).endContainer, b.nextSibling,
+ `#0-1 Double-clicking in <b> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).endOffset, eatSpaceToNextWord ? 1 : 0,
+ `#0-1 Double-clicking in <b> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ `#0-1 nsIHTMLEditor::getSelectedElement(\"\") should return null after double-clicking in <b> element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ synthesizeMouseAtCenter(i, {clickCount: 1});
+ synthesizeMouseAtCenter(i, {clickCount: 2});
+ is(selection.getRangeAt(0).startContainer, i.previousSibling,
+ `#0-2 Double-clicking in <i> element should set start of selection to end of previous text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).startOffset, i.previousSibling.length,
+ `#0-2 Double-clicking in <i> element should set start of selection to end of previous text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ if (eatSpaceToNextWord) {
+ is(selection.getRangeAt(0).endContainer, i.nextSibling,
+ "#0-2 Double-clicking in <i> element should set end of selection to start of next text node (eat_space_to_next_word: true)");
+ is(selection.getRangeAt(0).endOffset, 0,
+ "#0-2 Double-clicking in <i> element should set end of selection to start of next text node (eat_space_to_next_word: true)");
+ } else {
+ is(selection.getRangeAt(0).endContainer, i.firstChild,
+ "#0-2 Double-clicking in <i> element should set end of selection to end of the word in <i> (eat_space_to_next_word: false)");
+ is(selection.getRangeAt(0).endOffset, "example".length,
+ "#0-2 Double-clicking in <i> element should set end of selection to end of the word in <i> (eat_space_to_next_word: false)");
+ }
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ `#0-2 nsIHTMLEditor::getSelectedElement(\"\") should return null after double-clicking in <b> element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ // Both clicking and double-clicking on <img> element should "select" it.
+ synthesizeMouseAtCenter(img, {clickCount: 1});
+ is(selection.getRangeAt(0).startContainer, img.parentElement,
+ `#0-3 Clicking in <img> element should set start of selection to the <img> element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).startOffset, 1,
+ `#0-3 Clicking in <img> element should set start of selection to the <img> element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).endContainer, img.parentElement,
+ `#0-3 Clicking in <img> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).endOffset, 2,
+ `#0-3 Clicking in <img> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ img,
+ `#0-3 nsIHTMLEditor::getSelectedElement(\"\") should return the <img> element after clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ synthesizeMouseAtCenter(img, {clickCount: 1});
+ synthesizeMouseAtCenter(img, {clickCount: 2});
+ is(selection.getRangeAt(0).startContainer, img.parentElement,
+ `#0-4 Double-clicking in <img> element should set start of selection to the <img> element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).startOffset, 1,
+ `#0-4 Double-clicking in <img> element should set start of selection to the <img> element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).endContainer, img.parentElement,
+ `#0-4 Double-clicking in <img> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).endOffset, 2,
+ `#0-4 Double-clicking in <img> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ img,
+ `#0-4 nsIHTMLEditor::getSelectedElement(\"\") should return the <img> element after double-clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ // Puts caret into the <a href> element.
+ synthesizeMouseAtCenter(href, {clickCount: 1});
+ is(selection.getRangeAt(0).startContainer, href.firstChild,
+ `#0-5 Clicking in <a href> element should set start of selection to the text node in it (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.isCollapsed, true,
+ `#0-5 Clicking in <a href> element should cause collapsing Selection (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ `#0-5 nsIHTMLEditor::getSelectedElement(\"\") should return null after clicking in the <a href> element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ href,
+ `#0-5 nsIHTMLEditor::getSelectedElement(\"href\") should return the <a href> element after clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ // Selects the <a href> element with a triple-click.
+ synthesizeMouseAtCenter(href, {clickCount: 1});
+ synthesizeMouseAtCenter(href, {clickCount: 2});
+ synthesizeMouseAtCenter(href, {clickCount: 3});
+ is(selection.getRangeAt(0).startContainer, href.parentElement,
+ `#0-6 Triple-clicking in <a href> element should set start of selection to the element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).startOffset, 1,
+ `#0-6 Triple-clicking in <a href> element should set start of selection to the element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).endContainer, href.parentElement,
+ `#0-6 Triple-clicking in <a href> element should set end of selection to start of next <br> element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(selection.getRangeAt(0).endOffset, 2,
+ `#0-6 Triple-clicking in <a href> element should set end of selection to start of next <br> element (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ href,
+ `#0-6 nsIHTMLEditor::getSelectedElement(\"\") should return the <a href> element after double-clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ href,
+ `#0-6 nsIHTMLEditor::getSelectedElement(\"href\") should return the <a href> element after double-clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`);
+ }
+
+ editor.innerHTML = "<p>p1<b>b1</b><i>i1</i></p>";
+ editor.focus();
+
+ // <p>[]p1...
+ let range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild.firstChild, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#1-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when selection is collapsed in a text node");
+
+ // <p>[p1]<b>...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#1-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when selection ends in a text node");
+
+ // <p>[p1<b>]b1</b>...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#1-3 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends at start of text node in <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#1-3 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection ends at start of text node in <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ editor.firstChild.nextSibling,
+ "#1-3 nsIHTMLEditor::getSelectedElement(\"i\") should return the <b> element when Selection ends at start of text node in <b> element");
+
+ // <p>[p1<b>b1]</b>...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#1-4 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends at end of text node in a text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#1-4 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection ends at end of text node in a text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ editor.firstChild.nextSibling,
+ "#1-4 nsIHTMLEditor::getSelectedElement(\"i\") should return the <b> element when Selection ends at end of text node in <b> element");
+
+ // <p>[p1}<b>b1...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild.firstChild.nextSibling, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#1-5 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends at text node and there are no elements");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#1-5 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection ends at text node and there are no elements");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#1-5 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection ends at text node and there are no elements");
+
+ // <p>p1<b>{b1}</b>...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling, 0);
+ range.setEnd(editor.firstChild.firstChild.nextSibling, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#1-6 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection only selects a text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#1-6 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection only selects a text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#1-6 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection only selects a text node");
+
+ // <p>[p1<b>b1</b>}<i>...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#1-7 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends the <b> element but starts from the previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#1-7 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection ends the <b> element but starts from the previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#1-7 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection ends the <b> element but starts from the previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#1-7 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection ends the <b> element but starts from the previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#1-7 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection ends the <b> element but starts from the previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#1-7 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection ends the <b> element but starts from the previous text node");
+
+ // <p>[p1<b>b1</b><i>i1</i>}...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild, 3);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#1-8 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection includes 2 elements and starts from previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#1-8 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection includes 2 elements and starts from previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#1-8 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection includes 2 elements and starts from previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#1-8 nsIHTMLEditor::getSelectedElement(\"href\") should null when Selection includes 2 elements and starts from previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#1-8 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection includes 2 elements and starts from previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#1-8 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection includes 2 elements and starts from previous text node");
+
+ // <p>p1{<b>b1</b>}<i>i1</i>...
+ // Note that this won't happen with user operation since Gecko sets
+ // start and end of Selection to points in text nodes as far as possible.
+ range = document.createRange();
+ range.setStart(editor.firstChild, 1);
+ range.setEnd(editor.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ editor.firstChild.firstChild.nextSibling,
+ "#1-9 nsIHTMLEditor::getSelectedElement(\"\") should return <b> element when only it is selected and matched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ editor.firstChild.firstChild.nextSibling,
+ "#1-9 nsIHTMLEditor::getSelectedElement(\"b\") should return <b> element when only it is selected and matched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#1-9 nsIHTMLEditor::getSelectedElement(\"i\") should return null when only a <b> element is selected but it is unmatched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#1-9 nsIHTMLEditor::getSelectedElement(\"href\") should return null when only a <b> element is selected but it is unmatched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#1-9 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when only a <b> element is selected but it is unmatched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#1-9 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return when only a <b> element is selected but it is unmatched");
+
+ // <p>p1<b>b1</b>{<i>i1</i>}</p>...
+ range = document.createRange();
+ range.setStart(editor.firstChild, 2);
+ range.setEnd(editor.firstChild, 3);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ editor.firstChild.firstChild.nextSibling.nextSibling,
+ "#1-10 nsIHTMLEditor::getSelectedElement(\"\") should return <i> element when only it is selected and matched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#1-10 nsIHTMLEditor::getSelectedElement(\"b\") should return null when only an <i> element is selected but it is unmatched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ editor.firstChild.firstChild.nextSibling.nextSibling,
+ "#1-10 nsIHTMLEditor::getSelectedElement(\"i\") should return <i> element when only it is selected and matched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#1-10 nsIHTMLEditor::getSelectedElement(\"href\") should return null when only an <i> element is selected but it is unmatched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#1-10 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when only an <i> element is selected but it is unmatched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#1-10 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when only an <i> element is selected but it is unmatched");
+
+ // <p>{p1}<b>b1</b><i>...
+ range = document.createRange();
+ range.setStart(editor.firstChild, 0);
+ range.setEnd(editor.firstChild, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#1-11 nsIHTMLEditor::getSelectedElement(\"\") should return null when only a text node is selected");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#1-11 nsIHTMLEditor::getSelectedElement(\"b\") should return null when only a text node is selected");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#1-11 nsIHTMLEditor::getSelectedElement(\"i\") should return null when only a text node is selected");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#1-11 nsIHTMLEditor::getSelectedElement(\"href\") should return null when only a text node is selected");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#1-11 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when only a text node is selected");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#1-11 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when only a text node is selected");
+
+ editor.innerHTML = "<p>p1<b>b1</b><b>b2</b><b>b3</b></p>";
+ editor.focus();
+
+ // <p>p1<b>b[1</b><b>b2</b><b>b]3</b>...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#2-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across 3 <b> elements");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#2-1 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across 3 <b> elements");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#2-1 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across 3 <b> elements");
+
+ // <p>p[1<b>b1</b><b>b2</b><b>b]3</b>...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#2-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across 3 <b> elements and previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#2-2 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across 3 <b> elements and previous text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#2-2 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across 3 <b> elements and previous text node");
+
+ editor.innerHTML = "<p>p1<b>b1<b>b2<b>b3</b></b></b>p2</p>";
+ editor.focus();
+
+ // <p>p1<b>b[1<b>b1-2<b>b]1-3</b>...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling.firstChild.nextSibling.firstChild, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#3-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across 3 <b> elements which are nested");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#3-1 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across 3 <b> elements which are nested");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#3-1 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across 3 <b> elements which are nested");
+
+ // <p>p[1<b>b1<b>b1-2<b>b]1-3</b>...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling.firstChild.nextSibling.firstChild, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#3-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across a text node and 3 <b> elements which are nested");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#3-2 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across a text node and 3 <b> elements which are nested");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#3-2 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across a text node and 3 <b> elements which are nested");
+
+ // <p>p1<b>b1<b>b1-2<b>b[1-3</b></b></b>p]2...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling.firstChild.nextSibling.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#3-3 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across 3 <b> elements which are nested and following text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")),
+ null,
+ "#3-3 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across 3 <b> elements which are nested and following text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")),
+ null,
+ "#3-3 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across 3 <b> elements which are nested and following text node");
+
+ editor.innerHTML = "<p><b>b1</b><a href=\"about:blank\">a1</a><a href=\"about:blank\">a2</a><a name=\"foo\">a3</a><b>b2</b></p>";
+ editor.focus();
+
+ // <p><b>b1</b>{<a href="...">a1</a>}<a href="...">a2...
+ // Note that this won't happen with user operation since Gecko sets
+ // start and end of Selection to points in text nodes as far as possible.
+ range = document.createRange();
+ range.setStart(editor.firstChild, 1);
+ range.setEnd(editor.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ editor.firstChild.firstChild.nextSibling,
+ "#4-1 nsIHTMLEditor::getSelectedElement(\"\") should return the first <a> element when only it is selected and matched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ editor.firstChild.firstChild.nextSibling,
+ "#4-1 nsIHTMLEditor::getSelectedElement(\"href\") should return the first <a> element when only it is selected and matched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#4-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when the first <a> element is selected but it is unmatched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#4-1 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when the first <a> element is selected but it is unmatched");
+
+ // <p><b>b1</b><a href="...">a[]1</a><a href="...">a2...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#4-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is collapsed");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ editor.firstChild.firstChild.nextSibling,
+ "#4-2 nsIHTMLEditor::getSelectedElement(\"href\") should return the first <a> element when Selection is collapsed in the element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#4-2 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection is collapsed");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#4-2 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection is collapsed");
+
+ // <p><b>b1</b><a href="...">a[1]</a><a href="...">a2...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#4-3 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is in a text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ editor.firstChild.firstChild.nextSibling,
+ "#4-3 nsIHTMLEditor::getSelectedElement(\"href\") should return the first <a> element when Selection is in the element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#4-3 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection is in a text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#4-3 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection is in a text node");
+
+ // <p><b>b1</b><a href="...">a[1</a><a href="...">a]2...
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling.firstChild, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#4-4 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection crosses 2 <a> elements");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#4-4 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection crosses 2 <a> elements");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#4-4 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection crosses 2 <a> elements");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#4-4 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection crosses 2 <a> elements");
+
+ // <p><b>b1</b><a href="...">a1</a><a href="...">a2</a>{<a name="...">a3</a>}<b>b2</b></p>
+ // Note that this won't happen with user operation since Gecko sets
+ // start and end of Selection to points in text nodes as far as possible.
+ range = document.createRange();
+ range.setStart(editor.firstChild, 3);
+ range.setEnd(editor.firstChild, 4);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling,
+ "#4-5 nsIHTMLEditor::getSelectedElement(\"\") should return the third <a> element when only it is selected and matched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#4-5 nsIHTMLEditor::getSelectedElement(\"href\") should return null when the third <a> element is selected but it is unmatched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling,
+ "#4-5 nsIHTMLEditor::getSelectedElement(\"anchor\") should return the third <a> element when only it is selected and matched");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#4-5 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when the third <a> element is selected but it is unmatched");
+
+ // <p><b>b1</b><a href="...">a1</a><a href="...">a2</a><a name="...">a[]3</a><b>b2</b></p>
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#4-6 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is collapsed in a text node even in named anchor element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#4-6 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection is collapsed in a text node even in named anchor element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#4-6 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection is collapsed in a text node even in named anchor element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")),
+ null,
+ "#4-6 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection is collapsed in a text node even in named anchor element");
+
+ editor.innerHTML = "<p>p1<b>b1</b>p1</p>";
+ editor.focus();
+
+ // <p>p1[<b>b1</b>]p1</p>
+ // This is usual case that user selects <b> element with dragging or double-clicking.
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 2);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#5-1 nsIHTMLEditor::getSelectedElement(\"\") should return null even when Selection starts from end of preceding text node and ends at start or following text node of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#5-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null even when Selection starts from end of preceding text node and ends at start or following text node of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#5-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null even when Selection starts from end of preceding text node and ends at start or following text node of <b> element");
+
+ // <p>p[1<b>b1</b>]p1</p>
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 1);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#5-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends at start of following text node of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#5-2 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection ends at start of following text node of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#5-2 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection ends at start of following text node of <b> element");
+
+ // <p>p1[<b>b1</b>p]1</p>
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 2);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#5-3 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from end of following text node of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#5-3 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from at end of following text node of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#5-3 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from end of following text node of <b> element");
+
+ // <p>p1<b>[b1</b>]p1</p>
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 0);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#5-4 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from start of text node in <b> element and ends at start of following text node of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#5-4 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from start of text node in <b> element and ends at start of following text node of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#5-4 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from start of text node in <b> element and ends at start of following text node of <b> element");
+
+ // <p>p1[<b>b1]</b>p1</p>
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 2);
+ range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#5-5 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from end of preceding text node of <b> element and ends at end of text node in <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#5-5 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from end of preceding text node of <b> element and ends at end of text node in <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#5-5 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from end of preceding text node of <b> element and ends at end of text node in <b> element");
+
+ // <p>p1<b>b[1</b>}p1</p>
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1);
+ range.setEnd(editor.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ editor.firstChild.firstChild.nextSibling,
+ "#5-6 nsIHTMLEditor::getSelectedElement(\"\") should the <b> element when Selection starts from in the <b> element's text node and end of preceding text node of <b> element and ends at end of the <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#5-6 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from in the <b> element's text node and end of preceding text node of <b> element and ends at end of the <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#5-6 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from in the <b> element's text node and end of preceding text node of <b> element and ends at end of the <b> element");
+
+ editor.innerHTML = "<p>p1<b>b1</b></p>";
+ editor.focus();
+
+ // <p>p1[<b>b1</b>}</p>
+ // This is usual case of double-clicking in the <b> element.
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 2);
+ range.setEnd(editor.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#6-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from previous text node of <b> element and end at end of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#6-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from previous text node of <b> element and end at end of <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#6-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from previous text node of <b> element and end at end of <b> element");
+
+ editor.innerHTML = "<p><b>b1</b>p1</p>";
+ editor.focus();
+
+ // <p><b>[b1</b>]p1</p>
+ // This is usual case of double-clicking in the <b> element.
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild.firstChild.nextSibling, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#7-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from a text node in <b> element and ends at start of following text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#7-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from a text node in <b> element and ends at start of following text node");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#7-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from a text node in <b> element and ends at start of following text node");
+
+ editor.innerHTML = "<p>p1<b>b1</b><br></p>";
+ editor.focus();
+
+ // <p>p1[<b>b1</b>}<br></p>
+ // This is usual case of double-clicking in the <b> element.
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild, 2);
+ range.setEnd(editor.firstChild, 2);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#8-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from previous text node of <b> element and ends before the following <br> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#8-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from previous text node of <b> element and ends before the following <br> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#8-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from previous text node of <b> element and ends before the following <br> element");
+
+
+ editor.innerHTML = "<p><b>b1</b><br></p>";
+ editor.focus();
+
+ // <p><b>[b1</b>}<br></p>
+ // This is usual case of double-clicking in the <b> element.
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#9-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#9-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#9-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element");
+
+ editor.innerHTML = "<p><b>b1</b><b><br></b><b>b2</b></p>";
+ editor.focus();
+
+ // <p><b>[b1</b><b>}<br></b><b>b2</b></p>
+ // This is usual case of double-clicking in the first <b> element.
+ range = document.createRange();
+ range.setStart(editor.firstChild.firstChild.firstChild, 0);
+ range.setEnd(editor.firstChild.firstChild.nextSibling, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")),
+ null,
+ "#10-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element in another <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")),
+ null,
+ "#10-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element in another <b> element");
+ is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")),
+ null,
+ "#10-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element in another <b> element");
+
+ SimpleTest.finish();
+});
+
+function getHTMLEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_insertElementAtSelection.html b/editor/libeditor/tests/test_nsIHTMLEditor_insertElementAtSelection.html
new file mode 100644
index 0000000000..9a0b7450eb
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIHTMLEditor_insertElementAtSelection.html
@@ -0,0 +1,185 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing `nsIHTMLEditor.insertElementAtSelection()`</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function getRangeDescription(range) {
+ function getNodeDescription(node) {
+ if (!node) {
+ return "null";
+ }
+ switch (node.nodeType) {
+ case Node.TEXT_NODE:
+ case Node.COMMENT_NODE:
+ case Node.CDATA_SECTION_NODE:
+ return `${node.nodeName} "${node.data}"`;
+ case Node.ELEMENT_NODE:
+ return `<${node.nodeName.toLowerCase()}${
+ node.hasAttribute("id")
+ ? ` id="${node.getAttribute("id")}"`
+ : ""
+ }${
+ node.hasAttribute("class")
+ ? ` class="${node.getAttribute("class")}"`
+ : ""
+ }${
+ node.hasAttribute("contenteditable")
+ ? ` contenteditable="${node.getAttribute("contenteditable")}"`
+ : ""
+ }>`;
+ default:
+ return `${node.nodeName}`;
+ }
+ }
+ if (range === null) {
+ return "null";
+ }
+ if (range === undefined) {
+ return "undefined";
+ }
+ return range.startContainer == range.endContainer &&
+ range.startOffset == range.endOffset
+ ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
+ : `(${getNodeDescription(range.startContainer)}, ${
+ range.startOffset
+ }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
+}
+
+function doTest(aEditingHost, aHTMLEditor, aDeleteSelection) {
+ const description = `aDeleteSelection=${aDeleteSelection}:`;
+ (() => {
+ aEditingHost.innerHTML = "abc";
+ aEditingHost.focus();
+ getSelection().collapse(aEditingHost.firstChild, 0);
+ const p = document.createElement("p");
+ p.appendChild(document.createElement("br"));
+ aHTMLEditor.insertElementAtSelection(p, aDeleteSelection);
+ is(
+ aEditingHost.innerHTML,
+ "<p><br></p>abc",
+ `${description} The <p> element should be inserted before the text node when selection is collapsed at start of the text node`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(<div contenteditable="">, 1)',
+ `${description} The selection should be collapsed after the inserted <p> element`
+ );
+ })();
+ (() => {
+ aEditingHost.innerHTML = "abc";
+ aEditingHost.focus();
+ getSelection().collapse(aEditingHost.firstChild, 1);
+ const p = document.createElement("p");
+ p.appendChild(document.createElement("br"));
+ aHTMLEditor.insertElementAtSelection(p, aDeleteSelection);
+ is(
+ aEditingHost.innerHTML,
+ "a<p><br></p>bc",
+ `${description} The <p> element should be inserted middle of the text node when selection is collapsed at middle of the text node`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(<div contenteditable="">, 2)',
+ `${description} The selection should be collapsed after the inserted <p> element (i.e., at right text node)`
+ );
+ })();
+ (() => {
+ aEditingHost.innerHTML = "abc";
+ aEditingHost.focus();
+ getSelection().collapse(aEditingHost.firstChild, 3);
+ const p = document.createElement("p");
+ p.appendChild(document.createElement("br"));
+ aHTMLEditor.insertElementAtSelection(p, aDeleteSelection);
+ is(
+ aEditingHost.innerHTML,
+ "abc<p><br></p>",
+ `${description} The <p> element should be inserted after the text node when selection is collapsed at end of the text node`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(<div contenteditable="">, 2)',
+ `${description} The selection should be collapsed at end of the editing host`
+ );
+ })();
+ (() => {
+ aEditingHost.innerHTML = "abc";
+ aEditingHost.focus();
+ getSelection().setBaseAndExtent(aEditingHost.firstChild, 0, aEditingHost.firstChild, 1);
+ const p = document.createElement("p");
+ p.appendChild(document.createElement("br"));
+ aHTMLEditor.insertElementAtSelection(p, aDeleteSelection);
+ is(
+ aEditingHost.innerHTML,
+ aDeleteSelection ? "<p><br></p>bc" : "a<p><br></p>bc",
+ `${description} The <p> element should be inserted after selected character when selection selects the first character of the text node`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ `(<div contenteditable="">, ${aDeleteSelection ? "1" : "2"})`,
+ `${description} The selection should be collapsed after the inserted <p> element (when selection selected the first character)`
+ );
+ })();
+ (() => {
+ aEditingHost.innerHTML = "abc";
+ aEditingHost.focus();
+ getSelection().setBaseAndExtent(aEditingHost.firstChild, 1, aEditingHost.firstChild, 2);
+ const p = document.createElement("p");
+ p.appendChild(document.createElement("br"));
+ aHTMLEditor.insertElementAtSelection(p, aDeleteSelection);
+ is(
+ aEditingHost.innerHTML,
+ aDeleteSelection ? "a<p><br></p>c" : "ab<p><br></p>c",
+ `${description} The <p> element should be inserted after selected character when selection selects a middle character of the text node`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(<div contenteditable="">, 2)',
+ `${description} The selection should be collapsed after the inserted <p> element (i.e., at right text node, when selection selected the middle character)`
+ );
+ })();
+ (() => {
+ aEditingHost.innerHTML = "abc";
+ aEditingHost.focus();
+ getSelection().setBaseAndExtent(aEditingHost.firstChild, 2, aEditingHost.firstChild, 3);
+ const p = document.createElement("p");
+ p.appendChild(document.createElement("br"));
+ aHTMLEditor.insertElementAtSelection(p, aDeleteSelection);
+ is(
+ aEditingHost.innerHTML,
+ aDeleteSelection ? "ab<p><br></p>" : "abc<p><br></p>",
+ `${description} The <p> element should be inserted after selected character when selection selects the last character of the text node`
+ );
+ is(
+ getRangeDescription(getSelection().getRangeAt(0)),
+ '(<div contenteditable="">, 2)',
+ `${description} The selection should be collapsed at end of the editing host (when selection selected the last character)`
+ );
+ })();
+}
+
+async function runTests() {
+ const editingHost = document.createElement("div");
+ editingHost.setAttribute("contenteditable", "");
+ document.body.appendChild(editingHost);
+ const editor =
+ SpecialPowers.
+ wrap(window).
+ docShell.
+ editingSession.
+ getEditorForWindow(window).
+ QueryInterface(SpecialPowers.Ci.nsIHTMLEditor);
+ doTest(editingHost, editor, true);
+ doTest(editingHost, editor, false);
+ SimpleTest.finish();
+}
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_removeInlineProperty.html b/editor/libeditor/tests/test_nsIHTMLEditor_removeInlineProperty.html
new file mode 100644
index 0000000000..7f7e297efe
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIHTMLEditor_removeInlineProperty.html
@@ -0,0 +1,420 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsIHTMLEditor.removeInlineProperty()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = window.getSelection();
+ let description, condition;
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ let selectionRanges = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editor.addEventListener("beforeinput", onBeforeInput);
+ editor.addEventListener("input", onInput);
+
+ function checkInputEvent(aEvent, aInputType, aDescription) {
+ if (aEvent.type !== "input" && aEvent.type !== "beforeinput") {
+ return;
+ }
+ ok(aEvent instanceof InputEvent, `${aDescription}"${aEvent.type}" event should be dispatched with InputEvent interface`);
+ // If it were cancelable whose inputType is empty string, web apps would
+ // block any Firefox specific modification whose inputType are not declared
+ // by the spec.
+ let expectedCancelable = aEvent.type === "beforeinput" && aInputType !== "";
+ is(aEvent.cancelable, expectedCancelable,
+ `${aDescription}"${aEvent.type}" event should ${expectedCancelable ? "be" : "be never"} cancelable`);
+ is(aEvent.bubbles, true, `${aDescription}"${aEvent.type}" event should always bubble`);
+ is(aEvent.inputType, aInputType, `${aDescription}inputType of "${aEvent.type}" event should be ${aInputType}`);
+ is(aEvent.data, null, `${aDescription}data of "${aEvent.type}" event should be null`);
+ is(aEvent.dataTransfer, null, `${aDescription}dataTransfer of "${aEvent.type}" event should be null`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ is(targetRanges.length, selectionRanges.length,
+ `${aDescription}getTargetRanges() of "${aEvent.type}" event should return selection ranges`);
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `${aDescription}startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `${aDescription}startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `${aDescription}endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `${aDescription}endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0, `${aDescription}getTargetRanges() of "${aEvent.type}" event should return empty array`);
+ }
+ }
+
+ function selectFromTextSiblings(aNode) {
+ condition = "selecting the node from end of previous text to start of next text node";
+ selection.setBaseAndExtent(aNode.previousSibling, aNode.previousSibling.length,
+ aNode.nextSibling, 0);
+ }
+ function selectNode(aNode) {
+ condition = "selecting the node";
+ let range = document.createRange();
+ range.selectNode(aNode);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ function selectAllChildren(aNode) {
+ condition = "selecting all children of the node";
+ selection.selectAllChildren(aNode);
+ }
+ function selectChildContents(aNode) {
+ condition = "selecting all contents of its child";
+ let range = document.createRange();
+ range.selectNodeContents(aNode.firstChild);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+
+ description = "When there is a <b> element and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = "<p>test: <b>here</b> is bolden text</p>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("b", "");
+ is(editor.innerHTML, "<p>test: here is bolden text</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should remove the <b> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ }
+
+ description = "When there is a <b> element which has style attribute and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = '<p>test: <b style="font-style: italic">here</b> is bolden text</p>';
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("b", "");
+ is(editor.innerHTML, '<p>test: <span style="font-style: italic">here</span> is bolden text</p>',
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should replace the <b> element with <span> element to keep the style');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ }
+
+ description = "When there is a <b> element which has class attribute and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = '<p>test: <b class="foo">here</b> is bolden text</p>';
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("b", "");
+ is(editor.innerHTML, '<p>test: <span class="foo">here</span> is bolden text</p>',
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should replace the <b> element with <span> element to keep the class');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ }
+
+ description = "When there is a <b> element which has an <i> element and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("b", "");
+ is(editor.innerHTML, "<p>test: <i>here</i> is bolden and italic text</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should remove only the <b> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ }
+
+ description = "When there is a <b> element which has an <i> element and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("i", "");
+ is(editor.innerHTML, "<p>test: <b>here</b> is bolden and italic text</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should remove only the <i> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): ');
+ }
+
+ description = "When there is an <i> element in a <b> element and ";
+ for (let prepare of [selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling.firstChild);
+ getHTMLEditor().removeInlineProperty("b", "");
+ is(editor.innerHTML, "<p>test: <i>here</i> is bolden and italic text</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should remove only the <b> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ }
+
+ description = "When there is an <i> element in a <b> element and ";
+ for (let prepare of [selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling.firstChild);
+ getHTMLEditor().removeInlineProperty("i", "");
+ is(editor.innerHTML, "<p>test: <b>here</b> is bolden and italic text</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should remove only the <i> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): ');
+ }
+
+ description = "When there is an <i> element between text nodes in a <b> element and ";
+ for (let prepare of [selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = "<p>test: <b>h<i>e</i>re</b> is bolden and italic text</p>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("i", "");
+ is(editor.innerHTML, "<p>test: <b>here</b> is bolden and italic text</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should remove only the <i> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): ');
+ }
+
+ description = "When there is an <i> element between text nodes in a <b> element and ";
+ for (let prepare of [selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = "<p>test: <b>h<i>e</i>re</b> is bolden and italic text</p>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("b", "");
+ is(editor.innerHTML, "<p>test: <b>h</b><i>e</i><b>re</b> is bolden and italic text</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should split the <b> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): ');
+ }
+
+ description = "When there is an <a> element whose href attribute is not empty and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = '<p>test: <a href="about:blank">here</a> is a link</p>';
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("href", "");
+ is(editor.innerHTML, "<p>test: here is a link</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should remove the <a> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): ');
+ }
+
+ // XXX In the case of "name", removeInlineProperty() does not the <a> element when name attribute is empty.
+ description = "When there is an <a> element whose href attribute is empty and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = '<p>test: <a href="">here</a> is a link</p>';
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("href", "");
+ is(editor.innerHTML, "<p>test: here is a link</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should remove the <a> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): ');
+ }
+
+ description = "When there is an <a> element which does not have href attribute and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = "<p>test: <a>here</a> is an anchor</p>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("href", "");
+ is(editor.innerHTML, "<p>test: <a>here</a> is an anchor</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should NOT remove the <a> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause a "beforeinput" event even though HTMLEditor will do nothing');
+ checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): ');
+ is(inputEvents.length, 0,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should NOT cause an "input" event');
+ }
+
+ description = "When there is an <a> element whose name attribute is not empty and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = '<p>test: <a name="foo">here</a> is a named anchor</p>';
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("href", "");
+ is(editor.innerHTML, '<p>test: <a name="foo">here</a> is a named anchor</p>',
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should NOT remove the <a> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause a "beforeinput" event even though HTMLEditor will do nothing');
+ checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): ');
+ is(inputEvents.length, 0,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should NOT cause an "input" event');
+ }
+
+ description = "When there is an <a> element whose name attribute is not empty and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = '<p>test: <a name="foo">here</a> is a named anchor</p>';
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("name", "");
+ is(editor.innerHTML, "<p>test: here is a named anchor</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should remove the <a> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause a "beforeinput" event');
+ checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", ""): ');
+ is(inputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause an "input" event');
+ checkInputEvent(inputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", ""): ');
+ }
+
+ // XXX In the case of "href", removeInlineProperty() removes the <a> element when href attribute is empty.
+ description = "When there is an <a> element whose name attribute is empty and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = '<p>test: <a name="">here</a> is a named anchor</p>';
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("name", "");
+ is(editor.innerHTML, '<p>test: <a name="">here</a> is a named anchor</p>',
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT remove the <a> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause a "beforeinput" event even though HTMLEditor will do nothing');
+ checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", ""): ');
+ is(inputEvents.length, 0,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT cause an "input" event');
+ }
+
+ description = "When there is an <a> element which does not have name attribute and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = "<p>test: <a>here</a> is an anchor</p>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("name", "");
+ is(editor.innerHTML, "<p>test: <a>here</a> is an anchor</p>",
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT remove the <a> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause a "beforeinput" event even though HTMLEditor will do nothing');
+ checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "")');
+ is(inputEvents.length, 0,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT cause an "input" event');
+ }
+
+ description = "When there is an <a> element whose href attribute is not empty and ";
+ for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
+ editor.innerHTML = '<p>test: <a href="about:blank">here</a> is a link</p>';
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ prepare(editor.firstChild.firstChild.nextSibling);
+ getHTMLEditor().removeInlineProperty("name", "");
+ is(editor.innerHTML, '<p>test: <a href="about:blank">here</a> is a link</p>',
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT remove the <a> element');
+ is(beforeInputEvents.length, 1,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause a "beforeinput" event even though HTMLEditor will do nothing');
+ checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "")');
+ is(inputEvents.length, 0,
+ description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT cause an "input" event');
+ }
+
+ editor.removeEventListener("beforeinput", onBeforeInput);
+ editor.removeEventListener("input", onInput);
+
+ SimpleTest.finish();
+});
+
+function getHTMLEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_selectElement.html b/editor/libeditor/tests/test_nsIHTMLEditor_selectElement.html
new file mode 100644
index 0000000000..1ac21635ea
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIHTMLEditor_selectElement.html
@@ -0,0 +1,131 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsIHTMLEditor.selectElement()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+ let selection = window.getSelection();
+
+ editor.innerHTML = "<p>p1<b>b1</b><i>i1</i></p><p><b>b2</b><i>i2</i>p2</p>";
+
+ editor.focus();
+ try {
+ getHTMLEditor().selectElement(editor.firstChild.firstChild);
+ ok(false, "nsIHTMLEditor.selectElement() should throw an exception if given node is not an element");
+ } catch (e) {
+ ok(true, `nsIHTMLEditor.selectElement() should throw an exception if given node is not an element: ${e}`);
+ }
+
+ editor.focus();
+ try {
+ getHTMLEditor().selectElement(editor.firstChild.firstChild.nextSibling);
+ is(selection.anchorNode, editor.firstChild,
+ "nsIHTMLEditor.selectElement() should set anchor node to parent of <b> element in the first paragraph");
+ is(selection.anchorOffset, 1,
+ "nsIHTMLEditor.selectElement() should set anchor offset to the index of <b> element in the first paragraph");
+ is(selection.focusNode, editor.firstChild,
+ "nsIHTMLEditor.selectElement() should set focus node to parent of <b> element in the first paragraph");
+ is(selection.focusOffset, 2,
+ "nsIHTMLEditor.selectElement() should set anchor offset to the index of <b> element + 1 in the first paragraph");
+ } catch (e) {
+ ok(false, `nsIHTMLEditor.selectElement() shouldn't throw exception when selecting an element in focused editor #1: ${e}`);
+ }
+
+ editor.focus();
+ try {
+ getHTMLEditor().selectElement(editor.firstChild.nextSibling.firstChild);
+ is(selection.anchorNode, editor.firstChild.nextSibling,
+ "nsIHTMLEditor.selectElement() should set anchor node to parent of <b> element in the second paragraph");
+ is(selection.anchorOffset, 0,
+ "nsIHTMLEditor.selectElement() should set anchor offset to the index of <b> element in the second paragraph");
+ is(selection.focusNode, editor.firstChild.nextSibling,
+ "nsIHTMLEditor.selectElement() should set focus node to parent of <b> element in the second paragraph");
+ is(selection.focusOffset, 1,
+ "nsIHTMLEditor.selectElement() should set anchor offset to the index of <b> element + 1 in the second paragraph");
+ } catch (e) {
+ ok(false, `nsIHTMLEditor.selectElement() shouldn't throw exception when selecting an element in focused editor #2: ${e}`);
+ }
+
+ editor.focus();
+ try {
+ getHTMLEditor().selectElement(editor);
+ ok(false, "nsIHTMLEditor.selectElement() should throw an exception if given node is the editing host");
+ } catch (e) {
+ ok(true, `nsIHTMLEditor.selectElement() should throw an exception if given node is the editing host: ${e}`);
+ }
+
+ editor.focus();
+ try {
+ getHTMLEditor().selectElement(editor.parentElement);
+ ok(false, "nsIHTMLEditor.selectElement() should throw an exception if given node is outside of the editing host");
+ } catch (e) {
+ ok(true, `nsIHTMLEditor.selectElement() should throw an exception if given node is outside of the editing host: ${e}`);
+ }
+
+ selection.removeAllRanges();
+ editor.blur();
+ try {
+ getHTMLEditor().selectElement(editor.firstChild.nextSibling.firstChild);
+ ok(false, "nsIHTMLEditor.selectElement() should throw an exception if there is no active editing host");
+ } catch (e) {
+ ok(true, `nsIHTMLEditor.selectElement() should throw an exception if there is no active editing host: ${e}`);
+ }
+
+ editor.focus();
+ editor.firstChild.firstChild.nextSibling.nextSibling.setAttribute("contenteditable", "false");
+ try {
+ getHTMLEditor().selectElement(editor.firstChild.firstChild.nextSibling.nextSibling);
+ is(selection.anchorNode, editor.firstChild,
+ "nsIHTMLEditor.selectElement() should set anchor node to parent of <i contenteditable=\"false\"> element in the first paragraph");
+ is(selection.anchorOffset, 2,
+ "nsIHTMLEditor.selectElement() should set anchor offset to the index of <i contenteditable=\"false\"> element in the first paragraph");
+ is(selection.focusNode, editor.firstChild,
+ "nsIHTMLEditor.selectElement() should set focus node to parent of <i contenteditable=\"false\"> element in the first paragraph");
+ is(selection.focusOffset, 3,
+ "nsIHTMLEditor.selectElement() should set anchor offset to the index of <i contenteditable=\"false\"> element + 1 in the first paragraph");
+ } catch (e) {
+ ok(false, `nsIHTMLEditor.selectElement() shouldn't throw exception when selecting an element in focused editor #3: ${e}`);
+ }
+
+ editor.focus();
+ editor.firstChild.nextSibling.setAttribute("contenteditable", "false");
+ try {
+ getHTMLEditor().selectElement(editor.firstChild.nextSibling.firstChild.nextSibling);
+ is(selection.anchorNode, editor.firstChild.nextSibling,
+ "nsIHTMLEditor.selectElement() should set anchor node to parent of <i> element in the second paragraph which is not editable");
+ is(selection.anchorOffset, 1,
+ "nsIHTMLEditor.selectElement() should set anchor offset to the index of <i> element in the second paragraph which is not editable");
+ is(selection.focusNode, editor.firstChild.nextSibling,
+ "nsIHTMLEditor.selectElement() should set focus node to parent of <i> element in the second paragraph which is not editable");
+ is(selection.focusOffset, 2,
+ "nsIHTMLEditor.selectElement() should set anchor offset to the index of <i> element + 1 in the second paragraph which is not editable");
+ } catch (e) {
+ ok(false, `nsIHTMLEditor.selectElement() shouldn't throw exception when selecting an element in focused editor #4: ${e}`);
+ }
+
+ SimpleTest.finish();
+});
+
+function getHTMLEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_setBackgroundColor.html b/editor/libeditor/tests/test_nsIHTMLEditor_setBackgroundColor.html
new file mode 100644
index 0000000000..1d67e7b688
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIHTMLEditor_setBackgroundColor.html
@@ -0,0 +1,187 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsIHTMLEditor.setBackgroundColor()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+
+ (function test_with_selecting_table_cells() {
+ editor.innerHTML =
+ '<table id="table">' +
+ '<tr id="r1"><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' +
+ '<tr id="r2"><th id="c2-1" rowspan="2">cell2-1</th><td id="c2-2">cell2-2</td><td id="c2-3">cell2-3</td></tr>' +
+ '<tr id="r3"><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' +
+ '<tr id="r4"><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">cell4-2</td><td id="c4-3">cell4-3</td><th id="c4-4">cell4-4</th><td id="c4-5">cell4-5</td></tr>' +
+ '<tr id="r5"><th id="c5-2">cell5-2</th><th id="c5-3" colspan="2">' +
+ '<table><tr id="r2-1"><td id="c2-1-1">cell2-1-1</td></tr></table>' +
+ '</th><td id="c5-5">cell5-5</td></tr>' +
+ '<tr id="r6"><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' +
+ '<tr id="r7"><td id="c7-2" colspan="4">cell7-2</td></tr>' +
+ "</table>";
+
+ (function test_without_CSS() {
+ (function test_with_selecting_1_1() {
+ let cell = document.getElementById("c1-1");
+ let range = document.createRange();
+ range.selectNode(cell);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ getHTMLEditor().setBackgroundColor("#ff0000");
+ is(cell.getAttribute("bgcolor"), "#ff0000",
+ '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the first cell element in the first row');
+ ok(!cell.nextSibling.hasAttribute("bgcolor"),
+ '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the other cells');
+ ok(!cell.parentNode.hasAttribute("bgcolor"),
+ '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent row');
+ ok(!cell.parentNode.parentNode.hasAttribute("bgcolor"),
+ '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent tbody');
+ ok(!cell.parentNode.parentNode.parentNode.hasAttribute("bgcolor"),
+ '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent table');
+ ok(!editor.hasAttribute("bgcolor"),
+ '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the editing host');
+ ok(!document.body.hasAttribute("bgcolor"),
+ '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the body');
+ ok(!document.documentElement.hasAttribute("bgcolor"),
+ '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the html');
+
+ getHTMLEditor().setBackgroundColor("");
+ ok(!cell.hasAttribute("bgcolor"),
+ '#1-1 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the first cell element in the first row');
+ })();
+
+ (function test_with_selecting_2_2_and_2_3() {
+ let cell2_2 = editor.querySelector("#c2-2");
+ let cell2_3 = editor.querySelector("#c2-3");
+ selection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell2_2);
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell2_3);
+ selection.addRange(range);
+ getHTMLEditor().setBackgroundColor("#ff0000");
+ is(cell2_2.getAttribute("bgcolor"), "#ff0000",
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the second cell element in the second row');
+ is(cell2_3.getAttribute("bgcolor"), "#ff0000",
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the third cell element in the second row');
+ ok(!cell2_2.parentNode.hasAttribute("bgcolor"),
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent row');
+ ok(!cell2_2.parentNode.parentNode.hasAttribute("bgcolor"),
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent tbody');
+ ok(!cell2_2.parentNode.parentNode.parentNode.hasAttribute("bgcolor"),
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent table');
+ ok(!editor.hasAttribute("bgcolor"),
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the editing host');
+ ok(!document.body.hasAttribute("bgcolor"),
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the body');
+ ok(!document.documentElement.hasAttribute("bgcolor"),
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the html');
+
+ getHTMLEditor().setBackgroundColor("");
+ ok(!cell2_2.hasAttribute("bgcolor"),
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the second cell element in the second row');
+ ok(!cell2_3.hasAttribute("bgcolor"),
+ '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the third cell element in the second row');
+ })();
+
+ (function test_with_selecting_6_3_and_paragraph_in_6_4_and_6_5() {
+ selection.removeAllRanges();
+ let cell6_3 = editor.querySelector("#c6-3");
+ let cell6_4 = editor.querySelector("#c6-4");
+ let cell6_5 = editor.querySelector("#c6-5");
+ let range = document.createRange();
+ range.selectNode(cell6_3);
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell6_4.firstChild);
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell6_5);
+ selection.addRange(range);
+ getHTMLEditor().setBackgroundColor("#ff0000");
+ is(cell6_3.getAttribute("bgcolor"), "#ff0000",
+ '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the second cell element in the sixth row');
+ ok(!cell6_4.hasAttribute("bgcolor"),
+ '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the third cell element in the sixth row');
+ ok(!cell6_4.firstChild.hasAttribute("bgcolor"),
+ '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the paragraph in the third cell element in the sixth row');
+ is(cell6_5.getAttribute("bgcolor"), "#ff0000",
+ '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the forth cell element in the sixth row');
+
+ getHTMLEditor().setBackgroundColor("");
+ ok(!cell6_3.hasAttribute("bgcolor"),
+ '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the second cell element in the sixth row');
+ ok(!cell6_5.hasAttribute("bgcolor"),
+ '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the third cell element in the sixth row');
+ })();
+
+ (function test_with_selecting_3_2_and_2_1_1_and_7_2() {
+ let cell3_2 = editor.querySelector("#c3-2");
+ let cell2_1_1 = editor.querySelector("#c2-1-1");
+ let cell6_5 = editor.querySelector("#c6-5");
+ selection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(cell3_2);
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell2_1_1);
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(cell6_5);
+ selection.addRange(range);
+
+ getHTMLEditor().setBackgroundColor("#ff0000");
+ is(cell3_2.getAttribute("bgcolor"), "#ff0000",
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the first cell element in the third row');
+ is(cell2_1_1.getAttribute("bgcolor"), "#ff0000",
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the cell element in the child table');
+ ok(!cell2_1_1.parentNode.hasAttribute("bgcolor"),
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the row in the child table');
+ ok(!cell2_1_1.parentNode.parentNode.hasAttribute("bgcolor"),
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the tbody in the child table');
+ ok(!cell2_1_1.parentNode.parentNode.parentNode.hasAttribute("bgcolor"),
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the table in the child table');
+ ok(!cell2_1_1.parentNode.parentNode.parentNode.parentNode.hasAttribute("bgcolor"),
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the cell having the child table');
+ is(cell6_5.getAttribute("bgcolor"), "#ff0000",
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the forth cell element in the sixth row');
+
+ getHTMLEditor().setBackgroundColor("");
+ ok(!cell3_2.hasAttribute("bgcolor"),
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the first cell element in the third row');
+ ok(!cell2_1_1.hasAttribute("bgcolor"),
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the cell element in the child table');
+ ok(!cell6_5.hasAttribute("bgcolor"),
+ '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the third cell element in the sixth row');
+ })();
+ })();
+ })();
+
+ SimpleTest.finish();
+});
+
+function getHTMLEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsIHTMLObjectResizer_hideResizers.html b/editor/libeditor/tests/test_nsIHTMLObjectResizer_hideResizers.html
new file mode 100644
index 0000000000..39679a546d
--- /dev/null
+++ b/editor/libeditor/tests/test_nsIHTMLObjectResizer_hideResizers.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for nsIHTMLObjectResizer.hideResizers()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="editor" contenteditable></div>
+<img src="green.png"><!-- for ensuring to load the image at first test of <img> case -->
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ async function waitForSelectionChange() {
+ return new Promise(resolve => {
+ document.addEventListener("selectionchange", () => {
+ resolve();
+ }, {once: true});
+ });
+ }
+
+ document.execCommand("enableObjectResizing", false, true);
+ let editor = document.getElementById("editor");
+ editor.innerHTML = "<img id=\"target\" src=\"green.png\" width=\"100\" height=\"100\">";
+ let img = document.getElementById("target");
+ let promiseSelectionChangeEvent = waitForSelectionChange();
+ synthesizeMouseAtCenter(img, {});
+ await promiseSelectionChangeEvent;
+ ok(img.hasAttribute("_moz_resizing"), "resizers of the <img> should be visible");
+ getHTMLObjectResizer().hideResizers();
+ ok(!img.hasAttribute("_moz_resizing"), "resizers of the <img> should be hidden after a call of hideResizers()");
+
+ SimpleTest.finish();
+});
+
+function getHTMLObjectResizer() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLObjectResizer);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html b/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html
new file mode 100644
index 0000000000..b17fcd6930
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html
@@ -0,0 +1,716 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.deleteTableCell()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+ let selectionRanges = [];
+
+ function checkInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, aEvent.type === "beforeinput",
+ `"${aEvent.type}" event should be ${aEvent.type === "beforeinput" ? "be" : "never cancelable"} ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, "deleteContent",
+ `inputType of "${aEvent.type}" event should be "deleteContent" ${aDescription}`);
+ is(aEvent.data, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ is(targetRanges.length, selectionRanges.length,
+ `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`);
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0,
+ `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`);
+ }
+ }
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editor.addEventListener("beforeinput", onBeforeInput);
+ editor.addEventListener("input", onInput);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.collapse(editor.firstChild, 0);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableCell(1) should do nothing if selection is not in <table>");
+ // If there were specific inputType value for this API, we should dispatch cancelable "beforeinput", though.
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when a call of nsITableEditor.deleteTableCell(1) even though it will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.deleteTableCell(1) does nothing');
+
+ selection.removeAllRanges();
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().deleteTableCell(1);
+ ok(false, "getTableEditor().deleteTableCell(1) without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().deleteTableCell(1) without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.deleteTableCell(1) causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.deleteTableCell(1) causes exception due to no selection range');
+ }
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ let range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-2</td><td>cell1-3</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-1");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when only one cell is selected #1-1');
+ checkInputEvent(beforeInputEvents[0], "when only one cell is selected #1-1");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when only one cell is selected #1-1');
+ checkInputEvent(inputEvents[0], "when only one cell is selected #1-1");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-3</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-2");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when only one cell is selected #1-2');
+ checkInputEvent(beforeInputEvents[0], "when only one cell is selected #1-2");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when only one cell is selected #1-2');
+ checkInputEvent(inputEvents[0], "when only one cell is selected #1-2");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-3");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when only one cell is selected #1-3');
+ checkInputEvent(beforeInputEvents[0], "when only one cell is selected #1-3");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when only one cell is selected #1-3');
+ checkInputEvent(inputEvents[0], "when only one cell is selected #1-3");
+
+ // When only one cell element is selected, the argument should be used.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(2);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-3</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(2) should remove selected cell element and next cell element in same row #1-4");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when only one cell is selected #1-4');
+ checkInputEvent(beforeInputEvents[0], "when only one cell is selected #1-4");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when only one cell is selected #1-4');
+ checkInputEvent(inputEvents[0], "when only one cell is selected #1-4");
+
+ // When the argument is larger than remaining cell elements from selected
+ // cell element, the behavior is really buggy.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(2);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(2) should remove selected cell element and its previous cell element when it reaches the last cell element in the row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when the argument is larger than remaining cell elements from selected cell element');
+ checkInputEvent(beforeInputEvents[0], "when the argument is larger than remaining cell elements from selected cell element");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when the argument is larger than remaining cell elements from selected cell element');
+ checkInputEvent(inputEvents[0], "when the argument is larger than remaining cell elements from selected cell element");
+
+ // XXX If the former case is expected, first row should be removed in this
+ // case, but it removes only selected cell and its previous cell.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(4);
+ todo_is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(4) should remove the first row when a cell in it is selected and the argument is larger than number of cells in the row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when the argument is larger than number of cells in the row');
+ checkInputEvent(beforeInputEvents[0], "when the argument is larger than number of cells in the row");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when the argument is larger than number of cells in the row');
+ checkInputEvent(inputEvents[0], "when the argument is larger than number of cells in the row");
+
+ // If 2 or more cells are selected, the argument should be ignored.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select1">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td>cell2-1</td><td id="select2">cell2-2</td><td>cell2-3</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-2</td><td>cell1-3</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should remove selected cell elements even if the argument is smaller than number of selected cells");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired even if the argument is smaller than number of selected cells');
+ checkInputEvent(beforeInputEvents[0], "even if the argument is smaller than number of selected cells");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired even if the argument is smaller than number of selected cells');
+ checkInputEvent(inputEvents[0], "even if the argument is smaller than number of selected cells");
+
+ // If all cells in a row are selected, the <tr> element should also be removed.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td><td id="select3">cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select3"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should remove also <tr> element when all cell elements in a row is selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all cell elements in a row is selected');
+ checkInputEvent(beforeInputEvents[0], "when all cell elements in a row is selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all cell elements in a row is selected');
+ checkInputEvent(inputEvents[0], "when all cell elements in a row is selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select">cell1-1</td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell2-1</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should remove also <tr> element when a cell element which is only child of a row is selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell element which is only child of a row is selected');
+ checkInputEvent(beforeInputEvents[0], "when a cell element which is only child of a row is selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell element which is only child of a row is selected');
+ checkInputEvent(inputEvents[0], "when a cell element which is only child of a row is selected");
+
+ // If all cells are removed, the <table> element should be removed.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr>' +
+ '<tr><td id="select3">cell2-1</td><td id="select4">cell2-2</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select3"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select4"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableCellContents(1) should remove also <table> element when all cell elements are selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all cell elements are selected');
+ checkInputEvent(beforeInputEvents[0], "when all cell elements are selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all cell elements are selected');
+ checkInputEvent(inputEvents[0], "when all cell elements are selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select">cell1-1</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableCellContents(1) should remove also <table> element when a cell element which is only child of <table> is selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell element which is only child of <table> is selected');
+ checkInputEvent(beforeInputEvents[0], "when a cell element which is only child of <table> is selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell element which is only child of <table> is selected');
+ checkInputEvent(inputEvents[0], "when a cell element which is only child of <table> is selected");
+
+ // rowspan
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-2</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-2</td></tr>" +
+ "<tr><td>cell2-2</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning rows");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when removing the cell spanning rows');
+ checkInputEvent(beforeInputEvents[0], "when removing the cell spanning rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when removing the cell spanning rows');
+ checkInputEvent(inputEvents[0], "when removing the cell spanning rows");
+
+ // XXX cell3-1 is also removed even though it's not selected.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select1" rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-2</td></tr>" +
+ '<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ todo_is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-2</td></tr>" +
+ "<tr><td>cell2-2</td></tr>" +
+ "<tr><td>cell3-1</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning rows (when 2 cell elements are selected)");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when removing the cell spanning rows (when 2 cell elements are selected)');
+ checkInputEvent(beforeInputEvents[0], "when removing the cell spanning rows (when 2 cell elements are selected)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when removing the cell spanning rows (when 2 cell elements are selected)');
+ checkInputEvent(inputEvents[0], "when removing the cell spanning rows (when 2 cell elements are selected)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
+ '<tr><td id="select">cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when spanned <tr>\'s last child cell element is removed');
+ checkInputEvent(beforeInputEvents[0], "when spanned <tr>'s last child cell element is removed");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when spanned <tr>\'s last child cell element is removed');
+ checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed");
+
+ // XXX broken case, the second row isn't removed.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
+ '<tr><td id="select1">cell2-2</td></tr>' +
+ '<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ todo_is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr>' +
+ "<tr><td>cell3-1</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when spanned <tr>\'s last child cell element is removed (when 2 cell elements are selected)');
+ checkInputEvent(beforeInputEvents[0], "when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when spanned <tr>\'s last child cell element is removed (when 2 cell elements are selected)');
+ checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)");
+
+ // XXX broken case, neither the selected cell nor the second row is removed.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr>' +
+ '<tr><td id="select">cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ todo_is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
+ "<tr><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed (removing middle row)");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when spanned <tr>\'s last child cell element is removed (removing middle row)');
+ checkInputEvent(beforeInputEvents[0], "when spanned <tr>'s last child cell element is removed (removing middle row)");
+ todo_is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when spanned <tr>\'s last child cell element is removed (removing middle row)');
+ if (inputEvents.length) {
+ checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed (removing middle row)");
+ }
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr>' +
+ '<tr><td id="select1">cell2-2</td></tr>' +
+ '<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ todo_is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element and remove the last row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when spanned <tr>\'s last child cell element and remove the last row');
+ checkInputEvent(beforeInputEvents[0], "when spanned <tr>'s last child cell element and remove the last row");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when spanned <tr>\'s last child cell element and remove the last row');
+ checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element and remove the last row");
+
+ // colspan
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select" colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-3</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning columns");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when removing the cell spanning columns');
+ checkInputEvent(beforeInputEvents[0], "when removing the cell spanning columns");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when removing the cell spanning columns');
+ checkInputEvent(inputEvents[0], "when removing the cell spanning columns");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select1" colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
+ '<tr><td>cell2-1</td><td id="select2">cell2-2</td><td>cell2-3</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-3</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning columns and the other selected cell element");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when removing the cell spanning columns and the other selected cell element');
+ checkInputEvent(beforeInputEvents[0], "when removing the cell spanning columns and the other selected cell element");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when removing the cell spanning columns and the other selected cell element');
+ checkInputEvent(inputEvents[0], "when removing the cell spanning columns and the other selected cell element");
+
+ // XXX broken case, colspan is not adjusted.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
+ '<tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ todo_is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td colspan="1">cell1-1</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when corresponding cell element is removed');
+ checkInputEvent(beforeInputEvents[0], "when corresponding cell element is removed");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when corresponding cell element is removed');
+ checkInputEvent(inputEvents[0], "when corresponding cell element is removed");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
+ '<tr><td>cell2-1</td><td id="select1">cell2-2</td><td id="select2">cell2-3</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ todo_is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td colspan="1">cell1-1</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed (when 2 cell elements are selected)");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)');
+ checkInputEvent(beforeInputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)');
+ checkInputEvent(inputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select1">cell1-2</td><td>cell1-4</td></tr>' +
+ '<tr><td id="select2" colspan="2">cell2-1</td><td>cell2-3</td><td>cell2-4</td></tr>' +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-4</td></tr>" +
+ "<tr><td>cell2-3</td><td>cell2-4</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed (when 2 cell elements are selected)");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)');
+ checkInputEvent(beforeInputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)');
+ checkInputEvent(inputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)");
+
+ // When a cell contains first selection range, it should be removed.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().deleteTableCell(1);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-2</td><td>cell1-3</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(1) should remove only a cell containing first selection range when there is no selected cell element");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when there is no selected cell element');
+ checkInputEvent(beforeInputEvents[0], "when there is no selected cell element");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when there is no selected cell element');
+ checkInputEvent(inputEvents[0], "when there is no selected cell element");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().deleteTableCell(2);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-3</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents(2) should remove only 2 cell elements starting from a cell containing first selection range when there is no selected cell element");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when there is no selected cell element');
+ checkInputEvent(beforeInputEvents[0], "when there is no selected cell element");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when there is no selected cell element');
+ checkInputEvent(inputEvents[0], "when there is no selected cell element");
+
+ editor.removeEventListener("input", onInput);
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html b/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html
new file mode 100644
index 0000000000..754c22766e
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html
@@ -0,0 +1,296 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.deleteTableCellContents()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+ let selectionRanges = [];
+
+ function checkInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, aEvent.type === "beforeinput",
+ `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, "deleteContent",
+ `inputType of "${aEvent.type}" event should be "deleteContent" ${aDescription}`);
+ is(aEvent.data, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ is(targetRanges.length, selectionRanges.length,
+ `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`);
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0,
+ `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`);
+ }
+ }
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editor.addEventListener("beforeinput", onBeforeInput);
+ editor.addEventListener("input", onInput);
+
+ selection.collapse(editor.firstChild, 0);
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().deleteTableCellContents();
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableCellContents() should do nothing if selection is not in <table>");
+ // If there were specific inputType value for this API, we should dispatch cancelable "beforeinput", though.
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when a call of nsITableEditor.deleteTableCellContents() even though it will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.deleteTableCellContents() does nothing');
+
+ selection.removeAllRanges();
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().deleteTableCellContents();
+ ok(false, "getTableEditor().deleteTableCellContents() without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().deleteTableCellContents() without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.deleteTableCellContents() causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.deleteTableCellContents() causes exception due to no selection range');
+ }
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select">cell1-1</td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ let range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCellContents();
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td id="select"><br></td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all text in a cell is selected');
+ checkInputEvent(beforeInputEvents[0], "when all text in a cell is selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all text in a cell is selected');
+ checkInputEvent(inputEvents[0], "when all text in a cell is selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select"><ul><li>list1</li></ul></td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCellContents();
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td id="select"><br></td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents() should replace the selected cell's <ul> element with <br> element");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when <ul> element in a cell is selected');
+ checkInputEvent(beforeInputEvents[0], "when <ul> element in a cell is selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when <ul> element in a cell is selected');
+ checkInputEvent(inputEvents[0], "when <ul> element in a cell is selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select1">cell1-1</td><td>cell1-2</td></tr>' +
+ '<tr><td id="select2">cell2-1</td><td>cell2-2</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCellContents();
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td id="select1"><br></td><td>cell1-2</td></tr>' +
+ '<tr><td id="select2"><br></td><td>cell2-2</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents() should replace the selected 2 cells' text with <br> element");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when 2 cell elements are selected');
+ checkInputEvent(beforeInputEvents[0], "when 2 cell elements are selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when 2 cell elements are selected');
+ checkInputEvent(inputEvents[0], "when 2 cell elements are selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr>' +
+ '<tr><td id="select3">cell2-1</td><td id="select4">cell2-2</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select3"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select4"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCellContents();
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td id="select1"><br></td><td id="select2"><br></td></tr>' +
+ '<tr><td id="select3"><br></td><td id="select4"><br></td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents() should replace the selected 4 cells' text with <br> element");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when 4 cell elements are selected');
+ checkInputEvent(beforeInputEvents[0], "when 4 cell elements are selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when 4 cell elements are selected');
+ checkInputEvent(inputEvents[0], "when 4 cell elements are selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCellContents();
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td id="select" rowspan="2"><br></td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element (even if the cell is row-spanning)");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell element are selected (even if the cell is row-spanning)');
+ checkInputEvent(beforeInputEvents[0], "when a cell element are selected (even if the cell is row-spanning)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell element are selected (even if the cell is row-spanning)');
+ checkInputEvent(inputEvents[0], "when a cell element are selected (even if the cell is row-spanning)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select" colspan="2">cell1-1</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableCellContents();
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td id="select" colspan="2"><br></td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element (even if the cell is column-spanning)");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell element are selected (even if the cell is column-spanning)');
+ checkInputEvent(beforeInputEvents[0], "when a cell element are selected (even if the cell is column-spanning)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell element are selected (even if the cell is column-spanning)');
+ checkInputEvent(inputEvents[0], "when a cell element are selected (even if the cell is column-spanning)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td id="select">cell1-1</td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().deleteTableCellContents();
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td id="select"><br></td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.deleteTableCellContents() should replace a cell's text with <br> element when the cell contains selection range");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when the cell contains selection range');
+ checkInputEvent(beforeInputEvents[0], "when the cell contains selection range");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when the cell contains selection range');
+ checkInputEvent(inputEvents[0], "when the cell contains selection range");
+
+ editor.removeEventListener("beforeinput", onBeforeInput);
+ editor.removeEventListener("input", onInput);
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_deleteTableColumn.html b/editor/libeditor/tests/test_nsITableEditor_deleteTableColumn.html
new file mode 100644
index 0000000000..3b34a989f4
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableColumn.html
@@ -0,0 +1,515 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.deleteTableColumn()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+ let selectionRanges = [];
+
+ function checkInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, aEvent.type === "beforeinput",
+ `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, "deleteContent",
+ `inputType of "${aEvent.type}" event should be "deleteContent" ${aDescription}`);
+ is(aEvent.data, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ is(targetRanges.length, selectionRanges.length,
+ `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`);
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0,
+ `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`);
+ }
+ }
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editor.addEventListener("beforeinput", onBeforeInput);
+ editor.addEventListener("input", onInput);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.collapse(editor.firstChild, 0);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableColumn(1) should do nothing if selection is not in <table>");
+ // If there were specific inputType value for this API, we should dispatch cancelable "beforeinput", though.
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when a call of nsITableEditor.deleteTableColumn(1) will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.deleteTableColumn(1))");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.deleteTableColumn(1) does nothing');
+
+ selection.removeAllRanges();
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().deleteTableColumn(1);
+ ok(false, "getTableEditor().deleteTableColumn(1) without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().deleteTableColumn(1) without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.deleteTableColumn(1) causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.deleteTableColumn(1) causes exception due to no selection range');
+ }
+
+ // If a cell is selected and the argument is less than number of rows,
+ // specified number of rows should be removed starting from the row
+ // containing the selected cell. But if the argument is same or
+ // larger than actual number of rows, the <table> should be removed.
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ let range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-2</td></tr><tr><td>cell2-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableColumn(1) should delete the first column when a cell in the first column is selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in the first column is selected');
+ checkInputEvent(beforeInputEvents[0], "when a cell in the first column is selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in the first column is selected');
+ checkInputEvent(inputEvents[0], "when a cell in the first column is selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableColumn(1) should delete the second column when a cell in the second column is selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in the second column is selected');
+ checkInputEvent(beforeInputEvents[0], "when a cell in the second column is selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in the second column is selected');
+ checkInputEvent(inputEvents[0], "when a cell in the second column is selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(2);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableColumn(2) should delete the <table> since there is only 2 columns");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in first column is selected and argument is same as number of rows');
+ checkInputEvent(beforeInputEvents[0], "when a cell in first column is selected and argument is same as number of rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in first column is selected and argument is same as number of rows');
+ checkInputEvent(inputEvents[0], "when a cell in first column is selected and argument is same as number of rows");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(3);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableColumn(3) should delete the <table> when argument is larger than actual number of columns");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when argument is larger than actual number of columns');
+ checkInputEvent(beforeInputEvents[0], "when argument is larger than actual number of columns");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when argument is larger than actual number of columns');
+ checkInputEvent(inputEvents[0], "when argument is larger than actual number of columns");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(2);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableColumn(2) should delete the second column containing selected cell and next column");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in second column and argument is same as the remaining columns');
+ checkInputEvent(beforeInputEvents[0], "when a cell in second column and argument is same as the remaining columns");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in second column and argument is same as the remaining columns');
+ checkInputEvent(inputEvents[0], "when a cell in second column and argument is same as the remaining columns");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(3);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableColumn(3) should delete the <table> since the argument equals actual number of columns");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in first column and argument is larger than the remaining columns');
+ checkInputEvent(beforeInputEvents[0], "when a cell in first column and argument is larger than the remaining columns");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in first column and argument is larger than the remaining columns');
+ checkInputEvent(inputEvents[0], "when a cell in first column and argument is larger than the remaining columns");
+
+ // Similar to selected a cell, when selection is in a cell, the cell should
+ // treated as selected.
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-2</td></tr><tr><td>cell2-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableColumn(1) should delete the first column when a cell in the first column contains selection range");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in the first column contains selection range');
+ checkInputEvent(beforeInputEvents[0], "when a cell in the first column contains selection range");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in the first column contains selection range');
+ checkInputEvent(inputEvents[0], "when a cell in the first column contains selection range");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableColumn(1) should delete the second column when a cell in the second column contains selection range");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in the second column contains selection range');
+ checkInputEvent(beforeInputEvents[0], "when a cell in the second column contains selection range");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in the second column contains selection range');
+ checkInputEvent(inputEvents[0], "when a cell in the second column contains selection range");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(2);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableColumn(2) should delete the <table> since there is only 2 columns");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all text in a cell in first column is selected and argument includes next row');
+ checkInputEvent(beforeInputEvents[0], "when all text in a cell in first column is selected and argument includes next row");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all text in a cell in first column is selected and argument includes next row');
+ checkInputEvent(inputEvents[0], "when all text in a cell in first column is selected and argument includes next row");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(3);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableColumn(3) should delete the <table> when argument is larger than actual number of columns");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all text in a cell in first column is selected and argument is same as number of all rows');
+ checkInputEvent(beforeInputEvents[0], "when all text in a cell in first column is selected and argument is same as number of all rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all text in a cell in first column is selected and argument is same as number of all rows');
+ checkInputEvent(inputEvents[0], "when all text in a cell in first column is selected and argument is same as number of all rows");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(2);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableColumn(2) should delete the second column containing a cell containing selection range and next column");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all text in a cell is selected and argument is same than renaming number of columns');
+ checkInputEvent(beforeInputEvents[0], "when all text in a cell is selected and argument is same than renaming number of columns");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all text in a cell is selected and argument is same than renaming number of columns');
+ checkInputEvent(inputEvents[0], "when all text in a cell is selected and argument is same than renaming number of columns");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(3);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableColumn(3) should delete the <table> since the argument equals actual number of columns");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all text in a cell in the first column and argument is larger than renaming number of columns');
+ checkInputEvent(beforeInputEvents[0], "when all text in a cell in the first column and argument is larger than renaming number of columns");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all text in a cell in the first column and argument is larger than renaming number of columns');
+ checkInputEvent(inputEvents[0], "when all text in a cell in the first column and argument is larger than renaming number of columns");
+
+ // The argument should be ignored when 2 or more cells are selected.
+ // XXX Different from deleteTableRow(), this removes the <table> completely.
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="select2">cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableColumn(1) should delete the <table> when both columns have selected cell");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when both columns have selected cell');
+ checkInputEvent(beforeInputEvents[0], "when both columns have selected cell");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when both columns have selected cell');
+ checkInputEvent(inputEvents[0], "when both columns have selected cell");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(2);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableColumn(2) should delete the <table> since 2 is number of columns of the <table>");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when cells in every column are selected #2');
+ checkInputEvent(beforeInputEvents[0], "when cells in every column are selected #2");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when cells in every column are selected #2');
+ checkInputEvent(inputEvents[0], "when cells in every column are selected #2");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(2);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableColumn(2) should delete the <table> since 2 is number of columns of the <table>");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when 2 cells in same column are selected');
+ checkInputEvent(beforeInputEvents[0], "when 2 cells in same column are selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when 2 cells in same column are selected');
+ checkInputEvent(inputEvents[0], "when 2 cells in same column are selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-3</td></tr><tr><td>cell2-3</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableColumn(1) should delete first 2 columns because cells in the both columns are selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when 2 cell elements in different columns are selected #1');
+ checkInputEvent(beforeInputEvents[0], "when 2 cell elements in different columns are selected #1");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when 2 cell elements in different columns are selected #1');
+ checkInputEvent(inputEvents[0], "when 2 cell elements in different columns are selected #1");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td><td id="select2">cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-2</td></tr><tr><td>cell2-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableColumn(1) should delete the first and the last columns because cells in the both columns are selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when 2 cell elements in different columns are selected #2');
+ checkInputEvent(beforeInputEvents[0], "when 2 cell elements in different columns are selected #2");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when 2 cell elements in different columns are selected #2');
+ checkInputEvent(inputEvents[0], "when 2 cell elements in different columns are selected #2");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select" colspan="2">cell1-1</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, '<table><tbody><tr><td id="select" colspan="1"><br></td><td>cell1-3</td></tr><tr><td>cell2-2</td><td>cell2-3</td></tr></tbody></table>',
+ "nsITableEditor.deleteTableColumn(1) with a selected cell is colspan=\"2\" should delete the first column and add empty cell to the second column");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell is selected and its colspan is 2');
+ checkInputEvent(beforeInputEvents[0], "when a cell is selected and its colspan is 2");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell is selected and its colspan is 2');
+ checkInputEvent(inputEvents[0], "when a cell is selected and its colspan is 2");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select" colspan="3">cell1-1</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, '<table><tbody><tr><td id="select" colspan="2"><br></td></tr><tr><td>cell2-2</td><td>cell2-3</td></tr></tbody></table>',
+ "nsITableEditor.deleteTableColumn(1) with a selected cell is colspan=\"3\" should delete the first column and add empty cell whose colspan is 2 to the second column");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell is selected and its colspan is 3');
+ checkInputEvent(beforeInputEvents[0], "when a cell is selected and its colspan is 3");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell is selected and its colspan is 3');
+ checkInputEvent(inputEvents[0], "when a cell is selected and its colspan is 3");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td colspan="3">cell1-1</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, '<table><tbody><tr><td colspan="2">cell1-1</td></tr><tr><td>cell2-1</td><td>cell2-3</td></tr></tbody></table>',
+ "nsITableEditor.deleteTableColumn(1) with selected cell in the second column should delete the second column and the colspan in the first row should be adjusted");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in 2nd column is only cell defined by the column #1');
+ checkInputEvent(beforeInputEvents[0], "when a cell in 2nd column is only cell defined by the column #1");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in 2nd column is only cell defined by the column #1');
+ checkInputEvent(inputEvents[0], "when a cell in 2nd column is only cell defined by the column #1");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableColumn(1);
+ is(editor.innerHTML, '<table><tbody><tr><td colspan="1">cell1-1</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-3</td></tr></tbody></table>',
+ "nsITableEditor.deleteTableColumn(1) with selected cell in the second column should delete the second column and the colspan should be adjusted");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in 2nd column is only cell defined by the column #2');
+ checkInputEvent(beforeInputEvents[0], "when a cell in 2nd column is only cell defined by the column #2");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in 2nd column is only cell defined by the column #2');
+ checkInputEvent(inputEvents[0], "when a cell in 2nd column is only cell defined by the column #2");
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html b/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html
new file mode 100644
index 0000000000..820df15b46
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html
@@ -0,0 +1,522 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.deleteTableRow()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+ let selectionRanges = [];
+
+ function checkInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, aEvent.type === "beforeinput",
+ `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, "deleteContent",
+ `inputType of "${aEvent.type}" event should be "deleteContent" ${aDescription}`);
+ is(aEvent.data, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ is(targetRanges.length, selectionRanges.length,
+ `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`);
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0,
+ `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`);
+ }
+ }
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editor.addEventListener("beforeinput", onBeforeInput);
+ editor.addEventListener("input", onInput);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.collapse(editor.firstChild, 0);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(1) should do nothing if selection is not in <table>");
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when a call of nsITableEditor.deleteTableRow(1) even though it will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.deleteTableRow(1))");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.deleteTableRow(1) does nothing');
+
+ selection.removeAllRanges();
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().deleteTableRow(1);
+ ok(false, "getTableEditor().deleteTableRow(1) without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().deleteTableRow(1) without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.deleteTableRow(1) causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.deleteTableRow(1) causes exception due to no selection range');
+ }
+
+ // If a cell is selected and the argument is less than number of rows,
+ // specified number of rows should be removed starting from the row
+ // containing the selected cell. But if the argument is same or
+ // larger than actual number of rows when a cell in the first row is
+ // selected, the <table> should be removed.
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ let range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(1) should delete the first row when a cell in the first row is selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in the first row is selected');
+ checkInputEvent(beforeInputEvents[0], "when a cell in the first row is selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in the first row is selected');
+ checkInputEvent(inputEvents[0], "when a cell in the first row is selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(1) should delete the second row when a cell in the second row is selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in the second row is selected');
+ checkInputEvent(beforeInputEvents[0], "when a cell in the second row is selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in the second row is selected');
+ checkInputEvent(inputEvents[0], "when a cell in the second row is selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(2);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableRow(2) should delete the <table> since there is only 2 rows");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in first row is selected and argument is same as number of rows');
+ checkInputEvent(beforeInputEvents[0], "when a cell in first row is selected and argument is same as number of rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in first row is selected and argument is same as number of rows');
+ checkInputEvent(inputEvents[0], "when a cell in first row is selected and argument is same as number of rows");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(3);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableRow(3) should delete the <table> when argument is larger than actual number of rows");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when argument is larger than actual number of rows');
+ checkInputEvent(beforeInputEvents[0], "when argument is larger than actual number of rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when argument is larger than actual number of rows');
+ checkInputEvent(inputEvents[0], "when argument is larger than actual number of rows");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(2);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(2) should delete the second row containing selected cell and next row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in second row and argument is same as the remaining rows');
+ checkInputEvent(beforeInputEvents[0], "when a cell in second row and argument is same as the remaining rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in second row and argument is same as the remaining rows');
+ checkInputEvent(inputEvents[0], "when a cell in second row and argument is same as the remaining rows");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(3);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(3) should delete the second row (containing selected cell) and the third row even though the argument is larger than the rows");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in second row and argument is larger than the remaining rows');
+ checkInputEvent(beforeInputEvents[0], "when a cell in second row and argument is larger than the remaining rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in second row and argument is larger than the remaining rows');
+ checkInputEvent(inputEvents[0], "when a cell in second row and argument is larger than the remaining rows");
+
+ // Similar to selected a cell, when selection is in a cell, the cell should
+ // treated as selected.
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(1) should delete the first row when a cell in the first row contains selection range");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in the first row contains selection range');
+ checkInputEvent(beforeInputEvents[0], "when a cell in the first row contains selection range");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in the first row contains selection range');
+ checkInputEvent(inputEvents[0], "when a cell in the first row contains selection range");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(1) should delete the second row when a cell in the second row contains selection range");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in the second row contains selection range');
+ checkInputEvent(beforeInputEvents[0], "when a cell in the second row contains selection range");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in the second row contains selection range');
+ checkInputEvent(inputEvents[0], "when a cell in the second row contains selection range");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(2);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableRow(2) should delete the <table> since there is only 2 rows");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all text in a cell in first row is selected and argument includes next row');
+ checkInputEvent(beforeInputEvents[0], "when all text in a cell in first row is selected and argument includes next row");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all text in a cell in first row is selected and argument includes next row');
+ checkInputEvent(inputEvents[0], "when all text in a cell in first row is selected and argument includes next row");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(3);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableRow(3) should delete the <table> when argument is larger than actual number of rows");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all text in a cell in first row is selected and argument is same as number of all rows');
+ checkInputEvent(beforeInputEvents[0], "when all text in a cell in first row is selected and argument is same as number of all rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all text in a cell in first row is selected and argument is same as number of all rows');
+ checkInputEvent(inputEvents[0], "when all text in a cell in first row is selected and argument is same as number of all rows");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(2);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(2) should delete the second row containing a cell containing selection range and next row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all text in a cell is selected and argument is same than renaming number of rows');
+ checkInputEvent(beforeInputEvents[0], "when all text in a cell is selected and argument is same than renaming number of rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all text in a cell is selected and argument is same than renaming number of rows');
+ checkInputEvent(inputEvents[0], "when all text in a cell is selected and argument is same than renaming number of rows");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select").firstChild);
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(3);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(3) should delete the second row (containing selection range) and the third row even though the argument is larger than the rows");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when all text in a cell in the second row and argument is larger than renaming number of rows');
+ checkInputEvent(beforeInputEvents[0], "when all text in a cell in the second row and argument is larger than renaming number of rows");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when all text in a cell in the second row and argument is larger than renaming number of rows');
+ checkInputEvent(inputEvents[0], "when all text in a cell in the second row and argument is larger than renaming number of rows");
+
+ // The argument should be ignored when 2 or more cells are selected.
+ // XXX If the argument is less than number of rows and cells in all rows are
+ // selected, only all rows are removed. However, this leaves empty <table>
+ // element. Is this expected?
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, "<table><tbody></tbody></table>",
+ "nsITableEditor.deleteTableRow(1) should delete all rows if every row's cell is selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when cells in every row are selected #1');
+ checkInputEvent(beforeInputEvents[0], "when cells in every row are selected #1");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when cells in every row are selected #1');
+ checkInputEvent(inputEvents[0], "when cells in every row are selected #1");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(2);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableRow(2) should delete the <table> since 2 is number of rows of the <table>");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when cells in every row are selected #2');
+ checkInputEvent(beforeInputEvents[0], "when cells in every row are selected #2");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when cells in every row are selected #2');
+ checkInputEvent(inputEvents[0], "when cells in every row are selected #2");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(2);
+ is(editor.innerHTML, "",
+ "nsITableEditor.deleteTableRow(2) should delete the <table> since 2 is number of rows of the <table>");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when 2 cells in same row are selected');
+ checkInputEvent(beforeInputEvents[0], "when 2 cells in same row are selected");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when 2 cells in same row are selected');
+ checkInputEvent(inputEvents[0], "when 2 cells in same row are selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(1) should delete first 2 rows because cells in the both rows are selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when 2 cell elements in different rows are selected #1');
+ checkInputEvent(beforeInputEvents[0], "when 2 cell elements in different rows are selected #1");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when 2 cell elements in different rows are selected #1');
+ checkInputEvent(inputEvents[0], "when 2 cell elements in different rows are selected #1");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td id="select2">cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("select2"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>",
+ "nsITableEditor.deleteTableRow(1) should delete the first and the last rows because cells in the both rows are selected");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when 2 cell elements in different rows are selected #2');
+ checkInputEvent(beforeInputEvents[0], "when 2 cell elements in different rows are selected #2");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when 2 cell elements in different rows are selected #2');
+ checkInputEvent(inputEvents[0], "when 2 cell elements in different rows are selected #2");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, '<table><tbody><tr><td valign="top"><br></td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>',
+ "nsITableEditor.deleteTableRow(1) with a selected cell is rowspan=\"2\" should delete the first row and add empty cell to the second row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell is selected and its rowspan is 2');
+ checkInputEvent(beforeInputEvents[0], "when a cell is selected and its rowspan is 2");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell is selected and its rowspan is 2');
+ checkInputEvent(inputEvents[0], "when a cell is selected and its rowspan is 2");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td id="select" rowspan="3">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-2</td></tr><tr><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, '<table><tbody><tr><td valign="top" rowspan="2"><br></td><td>cell2-2</td></tr><tr><td>cell3-2</td></tr></tbody></table>',
+ "nsITableEditor.deleteTableRow(1) with a selected cell is rowspan=\"3\" should delete the first row and add empty cell whose rowspan is 2 to the second row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell is selected and its rowspan is 3');
+ checkInputEvent(beforeInputEvents[0], "when a cell is selected and its rowspan is 3");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell is selected and its rowspan is 3');
+ checkInputEvent(inputEvents[0], "when a cell is selected and its rowspan is 3");
+
+ // XXX Must be buggy case. When removing a row which does not have a cell due
+ // to rowspan, the rowspan is not changed properly.
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr><tr><td id="select1">cell2-2</td></tr><tr><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select1"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, '<table><tbody><tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell3-2</td></tr></tbody></table>',
+ "nsITableEditor.deleteTableRow(1) with selected cell in the second row should delete the second row and the row span should be adjusted");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in 2nd row which is only cell defined by the row #1');
+ checkInputEvent(beforeInputEvents[0], "when a cell in 2nd row which is only cell defined by the row #1");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in 2nd row which is only cell defined by the row #1');
+ checkInputEvent(inputEvents[0], "when a cell in 2nd row which is only cell defined by the row #1");
+
+ selection.removeAllRanges();
+ editor.innerHTML =
+ '<table><tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+ beforeInputEvents = [];
+ inputEvents = [];
+ range = document.createRange();
+ range.selectNode(document.getElementById("select"));
+ selection.addRange(range);
+ getTableEditor().deleteTableRow(1);
+ is(editor.innerHTML, '<table><tbody><tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>',
+ "nsITableEditor.deleteTableRow(1) with selected cell in the second row should delete the second row and the row span should be adjusted");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when a cell in 2nd row which is only cell defined by the row #2');
+ checkInputEvent(beforeInputEvents[0], "when a cell in 2nd row which is only cell defined by the row #2");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when a cell in 2nd row which is only cell defined by the row #2');
+ checkInputEvent(inputEvents[0], "when a cell in 2nd row which is only cell defined by the row #2");
+
+ editor.removeEventListener("beforeinput", onBeforeInput);
+ editor.removeEventListener("input", onInput);
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_getCellAt.html b/editor/libeditor/tests/test_nsITableEditor_getCellAt.html
new file mode 100644
index 0000000000..f2544a2b00
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getCellAt.html
@@ -0,0 +1,138 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.getCellAt()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+
+ try {
+ SpecialPowers.unwrap(getTableEditor().getCellAt(undefined, 0, 0));
+ ok(false, "nsITableEditor.getCellAt(undefined) should cause throwing an exception when editor does not have Selection");
+ } catch (e) {
+ ok(true, "nsITableEditor.getCellAt(undefined) should cause throwing an exception when editor does not have Selection");
+ }
+
+ try {
+ SpecialPowers.unwrap(getTableEditor().getTableSize(null, 0, 0));
+ ok(false, "nsITableEditor.getCellAt(null) should cause throwing an exception when editor does not have Selection");
+ } catch (e) {
+ ok(true, "nsITableEditor.getCellAt(null) should cause throwing an exception when editor does not have Selection");
+ }
+
+ // XXX This is inconsistent behavior with other APIs.
+ try {
+ let cell = SpecialPowers.unwrap(getTableEditor().getCellAt(editor, 0, 0));
+ ok(true, "nsITableEditor.getCellAt() should not cause throwing exception even if given node is not a <table>");
+ is(cell, null, "nsITableEditor.getCellAt() should return null if given node is not a <table>");
+ } catch (e) {
+ ok(false, "nsITableEditor.getCellAt() should not cause throwing exception even if given node is not a <table>");
+ }
+
+ editor.innerHTML =
+ '<table id="table">' +
+ '<tr><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' +
+ '<tr><td id="c2-1" rowspan="2">cell2-1</td><td id="c2-2">cell2-2<td id="c2-3">cell2-3</td></tr>' +
+ '<tr><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' +
+ '<tr><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">' +
+ '<table id="inner-table"><tr><td id="c2-1-1">cell2-1-1</td><td id="c2-1-2">cell2-1-2</td></tr>' +
+ '<tr><td id="c2-2-1">cell2-2-1</td><td id="c2-2-2">cell2-2-2</td></table>' +
+ '</td><td id="c4-3">cell4-3</td><td id="c4-4">cell4-4</td><td id="c4-5">cell4-5</td></tr>' +
+ '<tr><td id="c5-2">cell5-2</td><td id="c5-3" colspan="2">cell5-3</td><td id="c5-5">cell5-5</td></tr>' +
+ '<tr><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' +
+ '<tr><td id="c7-2" colspan="4">cell7-2</td></tr>' +
+ "</table>";
+
+ const kTestsInParent = [
+ { row: 0, column: 0, expected: "c1-1" },
+ { row: 0, column: 3, expected: "c1-4" },
+ { row: 0, column: 4, expected: "c1-4" },
+ { row: 1, column: 3, expected: "c1-4" },
+ { row: 1, column: 4, expected: "c1-4" },
+ { row: 1, column: 0, expected: "c2-1" },
+ { row: 2, column: 0, expected: "c2-1" },
+ { row: 3, column: 0, expected: "c4-1" },
+ { row: 4, column: 0, expected: "c4-1" },
+ { row: 5, column: 0, expected: "c4-1" },
+ { row: 6, column: 0, expected: "c4-1" },
+ { row: 4, column: 2, expected: "c5-3" },
+ { row: 4, column: 3, expected: "c5-3" },
+ { row: 4, column: 4, expected: "c5-5" },
+ { row: 6, column: 1, expected: "c7-2" },
+ { row: 6, column: 2, expected: "c7-2" },
+ { row: 6, column: 3, expected: "c7-2" },
+ { row: 6, column: 4, expected: "c7-2" },
+ { row: 6, column: 5, expected: null },
+ ];
+
+ let table = document.getElementById("table");
+ for (const kTest of kTestsInParent) {
+ let cell = SpecialPowers.unwrap(getTableEditor().getCellAt(table, kTest.row, kTest.column));
+ if (kTest.expected === null) {
+ is(cell, null,
+ `Specified the parent <table> element directly (${kTest.row} - ${kTest.column})`);
+ } else {
+ is(cell.getAttribute("id"), kTest.expected,
+ `Specified the parent <table> element directly (${kTest.row} - ${kTest.column})`);
+ }
+ if (cell && cell.firstChild && cell.firstChild.nodeType == Node.TEXT_NODE) {
+ selection.collapse(cell.firstChild, 0);
+ cell = getTableEditor().getCellAt(null, kTest.row, kTest.column);
+ is(cell.getAttribute("id"), kTest.expected,
+ `Selection is collapsed in a cell element in the parent <table> (${kTest.row} - ${kTest.column})`);
+ }
+ }
+
+ const kTestsInChild = [
+ { row: 0, column: 0, expected: "c2-1-1" },
+ { row: 0, column: 1, expected: "c2-1-2" },
+ { row: 0, column: 2, expected: null },
+ { row: 1, column: 0, expected: "c2-2-1" },
+ { row: 1, column: 1, expected: "c2-2-2" },
+ { row: 2, column: 0, expected: null },
+ ];
+
+ let innerTable = document.getElementById("inner-table");
+ for (const kTest of kTestsInChild) {
+ let cell = SpecialPowers.unwrap(getTableEditor().getCellAt(innerTable, kTest.row, kTest.column));
+ if (kTest.expected === null) {
+ is(cell, null,
+ `Specified the inner <table> element directly (${kTest.row} - ${kTest.column})`);
+ } else {
+ is(cell.getAttribute("id"), kTest.expected,
+ `Specified the inner <table> element directly (${kTest.row} - ${kTest.column})`);
+ }
+ if (cell && cell.firstChild && cell.firstChild.nodeType == Node.TEXT_NODE) {
+ selection.collapse(cell.firstChild, 0);
+ cell = getTableEditor().getCellAt(null, kTest.row, kTest.column);
+ is(cell.getAttribute("id"), kTest.expected,
+ `Selection is collapsed in a cell element in the inner <table> (${kTest.row} - ${kTest.column})`);
+ }
+ }
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_getCellDataAt.html b/editor/libeditor/tests/test_nsITableEditor_getCellDataAt.html
new file mode 100644
index 0000000000..388281c044
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getCellDataAt.html
@@ -0,0 +1,751 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for nsITableEditor.getCellDataAt()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+
+ let cellElementWrapper;
+ let startRowIndexWrapper, startColumnIndexWrapper;
+ let rowspanWrapper, colspanWrapper;
+ let effectiveRowspanWrapper, effectiveColspanWrapper;
+ let isSelectedWrapper;
+
+ function reset() {
+ cellElementWrapper = {};
+ startRowIndexWrapper = {};
+ startColumnIndexWrapper = {};
+ rowspanWrapper = {};
+ colspanWrapper = {};
+ effectiveRowspanWrapper = {};
+ effectiveColspanWrapper = {};
+ isSelectedWrapper = {};
+ }
+
+ editor.focus();
+ selection.collapse(editor.firstChild, 0);
+ try {
+ getTableEditor().getCellDataAt(null, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ ok(false, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection is outside of any <table>s");
+ } catch (e) {
+ ok(true, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection is outside of any <table>s");
+ }
+
+ selection.removeAllRanges();
+ try {
+ getTableEditor().getCellDataAt(null, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ ok(false, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection has no ranges");
+ } catch (e) {
+ ok(true, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection has no ranges");
+ }
+
+ // Collapse in text node in the cell element.
+ selection.collapse(editor.firstChild.nextSibling.firstChild.firstChild.firstChild.firstChild, 0);
+ reset();
+ getTableEditor().getCellDataAt(null, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when selection is in it");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when selection is in the cell");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when selection is in the cell");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when selection is in the cell");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when selection is in the cell");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when selection is in the cell");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when selection is in the cell");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return false for isSelected when selection is in the cell");
+
+ // Select the cell
+ selection.setBaseAndExtent(editor.firstChild.nextSibling.firstChild.firstChild, 0,
+ editor.firstChild.nextSibling.firstChild.firstChild, 1);
+ reset();
+ getTableEditor().getCellDataAt(null, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when it's selected");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when the cell is selected");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when the cell is selected");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when the cell is selected");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when the cell is selected");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when the cell is selected");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when the cell is selected");
+ is(isSelectedWrapper.value, true,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return true for isSelected when the cell is selected");
+
+ // Select the <tr>
+ selection.setBaseAndExtent(editor.firstChild.nextSibling.firstChild, 0,
+ editor.firstChild.nextSibling.firstChild, 1);
+ reset();
+ getTableEditor().getCellDataAt(null, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when the <tr> is selected");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when the <tr> is selected");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when the <tr> is selected");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when the <tr> is selected");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when the <tr> is selected");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when the <tr> is selected");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when the <tr> is selected");
+ is(isSelectedWrapper.value, true,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return true for isSelected when the <tr> is selected");
+
+ // Select the <table>
+ selection.setBaseAndExtent(editor, 1, editor, 2);
+ reset();
+ getTableEditor().getCellDataAt(null, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when the <table> is selected");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when the <table> is selected");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when the <table> is selected");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when the <table> is selected");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when the <table> is selected");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when the <table> is selected");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when the <table> is selected");
+ is(isSelectedWrapper.value, true,
+ "getTableEditor().getCellDataAt(null, 0, 0) should return true for isSelected when the <table> is selected");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for rowspan");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveRowspan");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 1,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild.nextSibling,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return the second <td> element");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startRowIndex");
+ is(startColumnIndexWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for startColumnIndex");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for rowspan");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for colspan");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveRowspan");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveColspan");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return false for isSelected");
+
+ try {
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 2,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ ok(false, "getTableEditor().getCellDataAt(<table>, 0, 2) should throw exception since column index is out of bounds");
+ } catch (e) {
+ ok(true, "getTableEditor().getCellDataAt(<table>, 0, 2) should throw exception since column index is out of bounds");
+ }
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 1, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element in the second row");
+ is(startRowIndexWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for startRowIndex");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for rowspan");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for colspan");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveRowspan");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 2, 1,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.firstChild.nextSibling,
+ "getTableEditor().getCellDataAt(<table>, 2, 1) should return the second <td> element in the last row");
+ is(startRowIndexWrapper.value, 2,
+ "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for startRowIndex");
+ is(startColumnIndexWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for startColumnIndex");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for rowspan");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for colspan");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for effectiveRowspan");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for effectiveColspan");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 2, 1) should return false for isSelected");
+
+ try {
+ getTableEditor().getCellDataAt(editor.firstChild, 2, 2,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ ok(false, "getTableEditor().getCellDataAt(<table>, 2, 2) should throw exception since column index is out of bounds");
+ } catch (e) {
+ ok(true, "getTableEditor().getCellDataAt(<table>, 2, 2) should throw exception since column index is out of bounds");
+ }
+
+ try {
+ getTableEditor().getCellDataAt(editor.firstChild, 3, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ ok(false, "getTableEditor().getCellDataAt(<table>, 3, 0) should throw exception since row index is out of bounds");
+ } catch (e) {
+ ok(true, "getTableEditor().getCellDataAt(<table>, 3, 0) should throw exception since row index is out of bounds");
+ }
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td rowspan="3">cell1-1</td><td>cell1-2</td><td>cell1-3</td><td>cell1-4</td></tr>' +
+ "<tr><td>cell2-2</td></tr>" +
+ "<tr><td>cell3-2</td><td>cell3-3</td></tr>" +
+ '<tr><td colspan="3">cell4-1</td><td>cell4-4</td></tr>' +
+ "</table>";
+ editor.focus();
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose rowspan is 3");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's rowspan is 3)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's rowspan is 3)");
+ is(rowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for rowspan (the cell's rowspan is 3)");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan (the cell's rowspan is 3)");
+ is(effectiveRowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's rowspan is 3)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan (the cell's rowspan is 3)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's rowspan is 3)");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 1, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element whose rowspan is 3");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startRowIndex (the cell's rowspan is 3)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex (the cell's rowspan is 3)");
+ is(rowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 3 for rowspan (the cell's rowspan is 3)");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for colspan (the cell's rowspan is 3)");
+ is(effectiveRowspanWrapper.value, 2,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 2 for effectiveRowspan (the cell's rowspan is 3)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan (the cell's rowspan is 3)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected (the cell's rowspan is 3)");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 2, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 2, 0) should return the first <td> element whose rowspan is 3");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 2, 0) should return 0 for startRowIndex (the cell's rowspan is 3)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 2, 0) should return 0 for startColumnIndex (the cell's rowspan is 3)");
+ is(rowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 2, 0) should return 3 for rowspan (the cell's rowspan is 3)");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 2, 0) should return 1 for colspan (the cell's rowspan is 3)");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 2, 0) should return 1 for effectiveRowspan (the cell's rowspan is 3)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 2, 0) should return 1 for effectiveColspan (the cell's rowspan is 3)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 2, 0) should return false for isSelected (the cell's rowspan is 3)");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 3, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 3, 0) should return the first <td> element in the last row whose colspan is 3");
+ is(startRowIndexWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 3, 0) should return 3 for startRowIndex (the cell's colspan is 3)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 3, 0) should return 0 for startColumnIndex (the cell's colspan is 3)");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 0) should return 1 for rowspan (the cell's colspan is 3)");
+ is(colspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 3, 0) should return 3 for colspan (the cell's colspan is 3)");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 0) should return 1 for effectiveRowspan (the cell's colspan is 3)");
+ is(effectiveColspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 3, 0) should return 3 for effectiveColspan (the cell's colspan is 3)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 3, 0) should return false for isSelected (the cell's colspan is 3)");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 3, 1,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 3, 1) should return the first <td> element in the last row whose colspan is 3");
+ is(startRowIndexWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 3, 1) should return 3 for startRowIndex (the cell's colspan is 3)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 3, 1) should return 0 for startColumnIndex (the cell's colspan is 3)");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 1) should return 1 for rowspan (the cell's colspan is 3)");
+ is(colspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 3, 1) should return 3 for colspan (the cell's colspan is 3)");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 1) should return 1 for effectiveRowspan (the cell's colspan is 3)");
+ is(effectiveColspanWrapper.value, 2,
+ "getTableEditor().getCellDataAt(<table>, 3, 1) should return 2 for effectiveColspan (the cell's colspan is 3)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 3, 1) should return false for isSelected (the cell's colspan is 3)");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 3, 2,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 3, 2) should return the first <td> element in the last row whose colspan is 3");
+ is(startRowIndexWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 3, 2) should return 3 for startRowIndex (the cell's colspan is 3)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 3, 2) should return 0 for startColumnIndex (the cell's colspan is 3)");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 2) should return 1 for rowspan (the cell's colspan is 3)");
+ is(colspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 3, 2) should return 3 for colspan (the cell's colspan is 3)");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 2) should return 1 for effectiveRowspan (the cell's colspan is 3)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 2) should return 1 for effectiveColspan (the cell's colspan is 3)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 3, 2) should return false for isSelected (the cell's colspan is 3)");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 3, 3,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild.nextSibling,
+ "getTableEditor().getCellDataAt(<table>, 3, 3) should return the second <td> element in the last row");
+ is(startRowIndexWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 3, 3) should return 3 for startRowIndex (right cell of the cell whose colspan is 3)");
+ is(startColumnIndexWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 3, 3) should return 3 for startColumnIndex (right cell of the cell whose colspan is 3)");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for rowspan (right cell of the cell whose colspan is 3)");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for colspan (right cell of the cell whose colspan is 3)");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for effectiveRowspan (right cell of the cell whose colspan is 3)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for effectiveColspan (right cell of the cell whose colspan is 3)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 3, 3) should return false for isSelected (right cell of the cell whose colspan is 3)");
+
+ try {
+ getTableEditor().getCellDataAt(editor.firstChild, 3, 4,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ ok(false, "getTableEditor().getCellDataAt(<table>, 3, 4) should throw exception since column index is out of bounds");
+ } catch (e) {
+ ok(true, "getTableEditor().getCellDataAt(<table>, 3, 4) should throw exception since column index is out of bounds");
+ }
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 1,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild.nextSibling,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return the second <td> element in the first row");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startRowIndex (right cell of the cell whose rowspan is 3)");
+ is(startColumnIndexWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for startColumnIndex (right cell of the cell whose rowspan is 3)");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for rowspan (right cell of the cell whose rowspan is 3)");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for colspan (right cell of the cell whose rowspan is 3)");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveRowspan (right cell of the cell whose rowspan is 3)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveColspan (right cell of the cell whose rowspan is 3)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return false for isSelected (right cell of the cell whose rowspan is 3)");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 1, 1,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 1, 1) should return the first <td> element in the second row");
+ is(startRowIndexWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for startRowIndex (right cell of the cell whose rowspan is 3)");
+ is(startColumnIndexWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for startColumnIndex (right cell of the cell whose rowspan is 3)");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for rowspan (right cell of the cell whose rowspan is 3)");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for colspan (right cell of the cell whose rowspan is 3)");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for effectiveRowspan (right cell of the cell whose rowspan is 3)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for effectiveColspan (right cell of the cell whose rowspan is 3)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 1, 1) should return false for isSelected (right cell of the cell whose rowspan is 3)");
+
+ try {
+ getTableEditor().getCellDataAt(editor.firstChild, 1, 2,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ ok(false, "getTableEditor().getCellDataAt(<table>, 1, 2) should throw exception since there is no cell due to non-rectangular table");
+ } catch (e) {
+ ok(true, "getTableEditor().getCellDataAt(<table>, 1, 2) should throw exception since there is no cell due to non-rectangular table");
+ }
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td rowspan="3" colspan="2">cell1-1</td></tr>' +
+ "</table>";
+ editor.focus();
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose rowspan is 3 and colspan is 2");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's rowspan is 3 and colspan is 2)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's rowspan is 3 and colspan is 2)");
+ is(rowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for rowspan (the cell's rowspan is 3 and colspan is 2)");
+ is(colspanWrapper.value, 2,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 2 for colspan (the cell's rowspan is 3 and colspan is 2)");
+ // XXX Not sure whether expected behavior or not.
+ todo_is(effectiveRowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's rowspan is 3 and colspan is 2)");
+ is(effectiveColspanWrapper.value, 2,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 2 for effectiveColspan (the cell's rowspan is 3 and colspan is 2)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's rowspan is 3 and colspan is 2)");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 1,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return the first <td> element whose rowspan is 3 and colspan is 2");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startRowIndex (the cell's rowspan is 3 and colspan is 2)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startColumnIndex (the cell's rowspan is 3 and colspan is 2)");
+ is(rowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 3 for rowspan (the cell's rowspan is 3 and colspan is 2)");
+ is(colspanWrapper.value, 2,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 2 for colspan (the cell's rowspan is 3 and colspan is 2)");
+ // XXX Not sure whether expected behavior or not.
+ todo_is(effectiveRowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 3 for effectiveRowspan (the cell's rowspan is 3 and colspan is 2)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveColspan (the cell's rowspan is 3 and colspan is 2)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 0, 1) should return false for isSelected (the cell's rowspan is 3 and colspan is 2)");
+
+ try {
+ getTableEditor().getCellDataAt(editor.firstChild, 1, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element whose rowspan is 3 and colspan is 2");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startRowIndex (the cell's rowspan is 3 and colspan is 2)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex (the cell's rowspan is 3 and colspan is 2)");
+ is(rowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 3 for rowspan (the cell's rowspan is 3 and colspan is 2)");
+ is(colspanWrapper.value, 2,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 2 for colspan (the cell's rowspan is 3 and colspan is 2)");
+ // XXX Not sure whether expected behavior or not.
+ todo_is(effectiveRowspanWrapper.value, 2,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 2 for effectiveRowspan (the cell's rowspan is 3 and colspan is 2)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan (the cell's rowspan is 3 and colspan is 2)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected (the cell's rowspan is 3 and colspan is 2)");
+ } catch (e) {
+ todo(false, "getTableEditor().getCellDataAt(<table>, 1, 0) shouldn't throw exception since rowspan expands the table");
+ }
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td rowspan="0">cell1-1</td><td>cell1-2</td></tr>' +
+ "<tr><td>cell2-2</td></tr>" +
+ "<tr><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose rowspan is 0");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's rowspan is 0)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's rowspan is 0)");
+ is(rowspanWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for rowspan (the cell's rowspan is 0)");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan (the cell's rowspan is 0)");
+ is(effectiveRowspanWrapper.value, 3,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's rowspan is 0)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan (the cell's rowspan is 0)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's rowspan is 0)");
+
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 1, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element whose rowspan is 0");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startRowIndex (the cell's rowspan is 0)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex (the cell's rowspan is 0)");
+ is(rowspanWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for rowspan (the cell's rowspan is 0)");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for colspan (the cell's rowspan is 0)");
+ is(effectiveRowspanWrapper.value, 2,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 3 for effectiveRowspan (the cell's rowspan is 0)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan (the cell's rowspan is 0)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected (the cell's rowspan is 0)");
+
+ // FYI: colspan must be 1 - 1000, 0 is invalid.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td colspan="0">cell1-1</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ editor.focus();
+ reset();
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose colspan is 0");
+ is(startRowIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's colspan is 0)");
+ is(startColumnIndexWrapper.value, 0,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's colspan is 0)");
+ is(rowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for rowspan (the cell's colspan is 0)");
+ is(colspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan (the cell's colspan is 0)");
+ is(effectiveRowspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's colspan is 0)");
+ is(effectiveColspanWrapper.value, 1,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan (the cell's colspan is 0)");
+ is(isSelectedWrapper.value, false,
+ "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's colspan is 0)");
+
+ try {
+ getTableEditor().getCellDataAt(editor.firstChild, 0, 1,
+ cellElementWrapper,
+ startRowIndexWrapper, startColumnIndexWrapper,
+ rowspanWrapper, colspanWrapper,
+ effectiveRowspanWrapper, effectiveColspanWrapper,
+ isSelectedWrapper);
+ ok(false, "getTableEditor().getCellDataAt(<table>, 0, 1) should throw exception since there is no cell due to right side of a cell whose colspan is 0");
+ } catch (e) {
+ ok(true, "getTableEditor().getCellDataAt(<table>, 0, 1) should throw exception since there is no cell due to right side of a cell whose colspan is 0");
+ }
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_getCellIndexes.html b/editor/libeditor/tests/test_nsITableEditor_getCellIndexes.html
new file mode 100644
index 0000000000..08f0e83838
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getCellIndexes.html
@@ -0,0 +1,91 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.getCellIndexes()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+ let rowIndex = {}, columnIndex = {};
+
+ try {
+ getTableEditor().getCellIndexes(undefined, rowIndex, columnIndex);
+ ok(false, "nsITableEditor.getCellIndexes(undefined) should cause throwing an exception");
+ } catch (e) {
+ ok(true, "nsITableEditor.getCellIndexes(undefined) should cause throwing an exception");
+ }
+
+ try {
+ getTableEditor().getCellIndexes(null, rowIndex, columnIndex);
+ ok(false, "nsITableEditor.getCellIndexes(null) should cause throwing an exception");
+ } catch (e) {
+ ok(true, "nsITableEditor.getCellIndexes(null) should cause throwing an exception");
+ }
+
+ try {
+ getTableEditor().getCellIndexes(editor, rowIndex, columnIndex);
+ ok(false, "nsITableEditor.getCellIndexes() should cause throwing an exception if given node is not a <td> nor a <th>");
+ } catch (e) {
+ ok(true, "nsITableEditor.getCellIndexes() should cause throwing an exception if given node is not a <td> nor a <th>");
+ }
+
+ // Set id to "test" for the argument for getCellIndexes().
+ // Set data-row and data-col to expected indexes.
+ const kTests = [
+ '<table><tr><td id="test" data-row="0" data-col="0">cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td id="test" data-row="0" data-col="2">cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0">cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td id="test" data-row="1" data-col="2">cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td id="test" data-row="2" data-col="0">cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td id="test" data-row="2" data-col="1">cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td id="test" data-row="2" data-col="2">cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1" rowspan="2">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td rowspan="2">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td id="test" data-row="2" data-col="1">cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td rowspan="2">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1" rowspan="2">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0" colspan="2">cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td colspan="2">cell2-1</td><td id="test" data-row="1" data-col="2">cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0">cell2-1</td><td colspan="2">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1" colspan="2">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+ '<table><tr><th id="test" data-row="0" data-col="0">cell1-1</th><th>cell1-2</th><th>cell1-3</tr><tr><th>cell2-1</th><th>cell2-2</th><th>cell2-3</th></tr><tr><th>cell3-1</th><th>cell3-2</th><th>cell3-3</th></tr></table>',
+ ];
+
+ for (const kTest of kTests) {
+ editor.innerHTML = kTest;
+ let cell = document.getElementById("test");
+ getTableEditor().getCellIndexes(cell, rowIndex, columnIndex);
+ is(rowIndex.value.toString(10), cell.getAttribute("data-row"), `Specified cell element directly, row Index value of ${kTest}`);
+ is(columnIndex.value.toString(10), cell.getAttribute("data-col"), `Specified cell element directly, column Index value of ${kTest}`);
+ selection.collapse(cell.firstChild, 0);
+ getTableEditor().getCellIndexes(null, rowIndex, columnIndex);
+ is(rowIndex.value.toString(10), cell.getAttribute("data-row"), `Selection is collapsed in the cell element, row Index value of ${kTest}`);
+ is(columnIndex.value.toString(10), cell.getAttribute("data-col"), `Selection is collapsed in the cell element, column Index value of ${kTest}`);
+ }
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_getFirstRow.html b/editor/libeditor/tests/test_nsITableEditor_getFirstRow.html
new file mode 100644
index 0000000000..9234538733
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getFirstRow.html
@@ -0,0 +1,105 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.getFirstRow()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+
+ try {
+ SpecialPowers.unwrap(getTableEditor().getFirstRow(undefined));
+ ok(false, "nsITableEditor.getFirstRow(undefined) should cause throwing an exception");
+ } catch (e) {
+ ok(true, "nsITableEditor.getFirstRow(undefined) should cause throwing an exception");
+ }
+
+ try {
+ SpecialPowers.unwrap(getTableEditor().getFirstRow(null));
+ ok(false, "nsITableEditor.getFirstRow(null) should cause throwing an exception");
+ } catch (e) {
+ ok(true, "nsITableEditor.getFirstRow(null) should cause throwing an exception");
+ }
+
+ try {
+ SpecialPowers.unwrap(getTableEditor().getFirstRow(editor));
+ ok(false, "nsITableEditor.getFirstRow() should cause throwing an exception if given node is not in <table>");
+ } catch (e) {
+ ok(true, "nsITableEditor.getFirstRow() should cause throwing an exception if given node is not in <table>");
+ }
+
+ // Set id to "test" for the argument for getFirstRow().
+ // Set id to "expected" for the expected <tr> element result (if there is).
+ // Set class of <table> to "hasAnonymousRow" if it does not has <tr> but will be anonymous <tr> element is created.
+ const kTests = [
+ '<table id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>',
+ '<table><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>',
+ '<table><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>',
+ '<table><tr id="expected"><td>cell1-1</td><td id="test">cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>',
+ '<table><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr id="test"><td>cell2-1</td><td>cell2-2</td></tr></table>',
+ '<table><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="test">cell2-1</td><td>cell2-2</td></tr></table>',
+ '<table><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="test">cell2-2</td></tr></table>',
+ '<table><tbody id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>',
+ '<table><tbody><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>',
+ '<table><thead id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></thead></table>',
+ '<table><thead><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></thead></table>',
+ '<table><tfoot id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></tfoot></table>',
+ '<table><tfoot><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></tfoot></table>',
+ '<table><thead id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr></thead><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>',
+ '<table><thead><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr></thead><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>',
+ '<table><thead><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr></thead><tbody><tr><td id="test">cell2-1</td><td>cell2-2</td></tr></tbody></table>',
+ '<table><tfoot id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr></tfoot><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>',
+ '<table><tfoot><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr></tfoot><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>',
+ '<table><tfoot><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr></tfoot><tbody><tr><td id="test">cell2-1</td><td>cell2-2</td></tr></tbody></table>',
+ '<table><tr><td><table id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table></td></tr></table>',
+ '<table><tr><td><table><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="test">cell2-1</td><td>cell2-2</td></tr></table></td></tr></table>',
+ '<table id="test"></table>',
+ '<table><caption id="test">table-caption</caption></table>',
+ '<table><caption>table-caption</caption><tr id="expected"><td id="test">cell</td></tr></table>',
+ '<table class="hasAnonymousRow"><td id="test">cell</td></table>',
+ '<table class="hasAnonymousRow"><td>cell-1</td><td id="test">cell-2</td></table>',
+ '<table><tr><td><table id="test"></table></td></tr></table>',
+ '<table><tr><td><table><caption id="test">table-caption</caption></table></td></tr></table>',
+ '<table><tr><td><table class="hasAnonymousRow"><td id="test">cell</td></table></td></tr></table>',
+ '<table><tr><td><table class="hasAnonymousRow"><td>cell-1</td><td id="test">cell-2</td></table></td></tr></table>',
+ '<table><tr id="expected"><td><p id="test">paragraph</p></td></tr></table>',
+ ];
+
+ for (const kTest of kTests) {
+ editor.innerHTML = kTest;
+ let firstRow = SpecialPowers.unwrap(getTableEditor().getFirstRow(document.getElementById("test")));
+ if (document.getElementById("expected")) {
+ is(firstRow.tagName, "TR", `Result should be a <tr>: ${kTest}`);
+ is(firstRow.getAttribute("id"), "expected", `Result should be the first <tr> element in the <table>: ${kTest}`);
+ } else if (document.querySelector(".hasAnonymousRow")) {
+ is(firstRow.tagName, "TR", `Result should be a <tr>: ${kTest}`);
+ is(firstRow, document.querySelector(".hasAnonymousRow tr"), `Result should be the anonymous <tr> element in the <table>: ${kTest}`);
+ } else {
+ is(firstRow, null, `Result should be null if there is no <tr> element in the <table>: ${kTest}`);
+ }
+ }
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_getFirstSelectedCellInTable.html b/editor/libeditor/tests/test_nsITableEditor_getFirstSelectedCellInTable.html
new file mode 100644
index 0000000000..f85c338f99
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getFirstSelectedCellInTable.html
@@ -0,0 +1,201 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.getFirstSelectedCellInTable()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+
+ selection.collapse(editor, 0);
+ let rowWrapper = {};
+ let colWrapper = {};
+ let cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ is(cell, null,
+ "nsITableEditor.getFirstSelectedCellInTable() should return null if Selection does not select cells");
+ is(rowWrapper.value, 0,
+ "nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number if Selection does not select cells");
+ is(colWrapper.value, 0,
+ "nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number if Selection does not select cells");
+
+ editor.innerHTML =
+ '<table id="table">' +
+ '<tr id="r1"><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' +
+ '<tr id="r2"><th id="c2-1" rowspan="2">cell2-1</th><td id="c2-2">cell2-2<td id="c2-3">cell2-3</td></tr>' +
+ '<tr id="r3"><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' +
+ '<tr id="r4"><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">cell4-2</td><td id="c4-3">cell4-3</td><th id="c4-4">cell4-4</th><td id="c4-5">cell4-5</td></tr>' +
+ '<tr id="r5"><th id="c5-2">cell5-2</th><th id="c5-3" colspan="2">cell5-3</th><td id="c5-5">cell5-5</td></tr>' +
+ '<tr id="r6"><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' +
+ '<tr id="r7"><td id="c7-2" colspan="4">cell7-2</td></tr>' +
+ "</table>";
+
+ let tr = document.getElementById("r1");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ is(cell, document.getElementById("c1-1"),
+ "#1-1 nsITableEditor.getFirstSelectedCellInTable() should return the first cell element in the first row");
+ is(rowWrapper.value, 0,
+ "#1-1 nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number for the first row");
+ is(colWrapper.value, 0,
+ "#1-1 nsITableEditor.getFirstSelectedCellInTable() should return 0 to column number for the first column");
+
+ tr = document.getElementById("r1");
+ selection.setBaseAndExtent(tr, 3, tr, 4);
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ is(cell, document.getElementById("c1-4"),
+ "#1-4 nsITableEditor.getFirstSelectedCellInTable() should return the last cell element whose colspan and rowspan are 2 in the first row");
+ is(rowWrapper.value, 0,
+ "#1-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number for the first row");
+ is(colWrapper.value, 3,
+ "#1-4 nsITableEditor.getFirstSelectedCellInTable() should return 3 to column number for the forth column");
+
+ tr = document.getElementById("r2");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ is(cell, document.getElementById("c2-1"),
+ "#2-1 nsITableEditor.getFirstSelectedCellInTable() should return the first cell element in the second row");
+ is(rowWrapper.value, 1,
+ "#2-1 nsITableEditor.getFirstSelectedCellInTable() should return 1 to row number for the second row");
+ is(colWrapper.value, 0,
+ "#2-1 nsITableEditor.getFirstSelectedCellInTable() should return 0 to column number for the first column");
+
+ tr = document.getElementById("r7");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ is(cell, document.getElementById("c7-2"),
+ "#7-2 nsITableEditor.getFirstSelectedCellInTable() should return the second cell element in the last row");
+ is(rowWrapper.value, 6,
+ "#7-2 nsITableEditor.getFirstSelectedCellInTable() should return 6 to row number for the seventh row");
+ is(colWrapper.value, 1,
+ "#7-2 nsITableEditor.getFirstSelectedCellInTable() should return 1 to column number for the second column");
+
+ selection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(document.getElementById("c2-2"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("c2-3"));
+ selection.addRange(range);
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ is(cell, document.getElementById("c2-2"),
+ "#2-2 nsITableEditor.getFirstSelectedCellInTable() should return the second cell element in the second row");
+ is(rowWrapper.value, 1,
+ "#2-2 nsITableEditor.getFirstSelectedCellInTable() should return 1 to row number for the second row");
+ is(colWrapper.value, 1,
+ "#2-2 nsITableEditor.getFirstSelectedCellInTable() should return 1 to column number for the second column");
+
+ selection.removeAllRanges();
+ range = document.createRange();
+ range.selectNode(document.getElementById("c3-4"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("c5-2"));
+ selection.addRange(range);
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ is(cell, document.getElementById("c3-4"),
+ "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return the last cell element in the third row");
+ is(rowWrapper.value, 2,
+ "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 2 to row number for the third row");
+ is(colWrapper.value, 3,
+ "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 3 to column number for the forth column");
+
+ cell = document.getElementById("c6-4");
+ selection.selectAllChildren(cell);
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ is(cell, null,
+ "nsITableEditor.getFirstSelectedCellInTable() should return null if neither <td> nor <th> element node is selected");
+ is(rowWrapper.value, 0,
+ "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number if neither <td> nor <th> element node is selected");
+ is(colWrapper.value, 0,
+ "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to column number if neither <td> nor <th> element node is selected");
+
+ cell = document.getElementById("c6-5");
+ selection.setBaseAndExtent(cell.firstChild, 0, cell.firstChild, 0);
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ is(cell, null,
+ "nsITableEditor.getFirstSelectedCellInTable() should return null if a text node is selected");
+ is(rowWrapper.value, 0,
+ "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number if a text node is selected");
+ is(colWrapper.value, 0,
+ "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to column number if a text node is selected");
+
+ // XXX If cell is not selected, nsITableEditor.getFirstSelectedCellInTable()
+ // returns null without throwing exception, however, if there is no
+ // selection ranges, throwing an exception. This inconsistency is odd.
+ selection.removeAllRanges();
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper));
+ ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if there is no selection ranges");
+ } catch (e) {
+ ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if there is no selection ranges");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable());
+ ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if it does not have argument");
+ } catch (e) {
+ ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if it does not have argument");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(null));
+ ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its argument is only one null");
+ } catch (e) {
+ ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its argument is only one null");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(null, null));
+ ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its arguments are all null");
+ } catch (e) {
+ ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its arguments are all null");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, null));
+ ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its column argument is null");
+ } catch (e) {
+ ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its column argument is null");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(null, colWrapper));
+ ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its row argument is null");
+ } catch (e) {
+ ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its row argument is null");
+ }
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ let editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_getSelectedCells.html b/editor/libeditor/tests/test_nsITableEditor_getSelectedCells.html
new file mode 100644
index 0000000000..cff75652df
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getSelectedCells.html
@@ -0,0 +1,295 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.getSelectedCells()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+
+ (function test_with_collapsed_selection() {
+ selection.collapse(editor, 0);
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 0,
+ "nsITableEditor.getSelectedCells() should return empty array if Selection does not select cells");
+ })();
+
+ editor.innerHTML =
+ '<table id="table">' +
+ '<tr id="r1"><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' +
+ '<tr id="r2"><th id="c2-1" rowspan="2">cell2-1</th><td id="c2-2">cell2-2</td><td id="c2-3">cell2-3</td></tr>' +
+ '<tr id="r3"><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' +
+ '<tr id="r4"><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">cell4-2</td><td id="c4-3">cell4-3</td><th id="c4-4">cell4-4</th><td id="c4-5">cell4-5</td></tr>' +
+ '<tr id="r5"><th id="c5-2">cell5-2</th><th id="c5-3" colspan="2">' +
+ '<table><tr id="r2-1"><td id="c2-1-1">cell2-1-1</td></tr></table>' +
+ '</th><td id="c5-5">cell5-5</td></tr>' +
+ '<tr id="r6"><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' +
+ '<tr id="r7"><td id="c7-2" colspan="4">cell7-2</td></tr>' +
+ "</table>";
+
+ (function test_with_selecting_1_1() {
+ let tr = document.getElementById("r1");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 1,
+ "#1-1 nsITableEditor.getSelectedCells() should return an array whose length is 1");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c1-1"),
+ "#1-1 nsITableEditor.getSelectedCells() should set the first item of the result to the first cell element in the first row");
+ })();
+
+ (function test_with_selecting_1_4() {
+ let tr = document.getElementById("r1");
+ selection.setBaseAndExtent(tr, 3, tr, 4);
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 1,
+ "#1-4 nsITableEditor.getSelectedCells() should return an array whose length is 1");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c1-4"),
+ "#1-4 nsITableEditor.getSelectedCells() should set the first item of the result to the last cell element whose colspan and rowspan are 2 in the first row");
+ })();
+
+ (function test_with_selecting_2_1() {
+ let tr = document.getElementById("r2");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 1,
+ "#2-1 nsITableEditor.getSelectedCells() should return an array whose length is 1");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c2-1"),
+ "#2-1 nsITableEditor.getSelectedCells() should set the first item of the result to the first cell element in the second row");
+ })();
+
+ (function test_with_selecting_7_2() {
+ let tr = document.getElementById("r7");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 1,
+ "#7-2 nsITableEditor.getSelectedCells() should return an array whose length is 1");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c7-2"),
+ "#7-2 nsITableEditor.getSelectedCells() should set the first item of the result to the second cell element in the last row");
+ })();
+
+ (function test_with_selecting_2_2_and_2_3() {
+ let tr = document.getElementById("r2");
+ selection.removeAllRanges();
+ let range = document.createRange();
+ range.setStart(tr, 1);
+ range.setEnd(tr, 2);
+ selection.addRange(range);
+ range = document.createRange();
+ range.setStart(tr, 2);
+ range.setEnd(tr, 3);
+ selection.addRange(range);
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 2,
+ "#2-2 nsITableEditor.getSelectedCells() should return an array whose length is 2");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c2-2"),
+ "#2-2 nsITableEditor.getSelectedCells() should set the first item of the result to the second cell element in the second row");
+ })();
+
+ (function test_with_selecting_3_4_and_5_2() {
+ let tr = document.getElementById("r3");
+ selection.removeAllRanges();
+ let range = document.createRange();
+ range.setStart(tr, 2);
+ range.setEnd(tr, 3);
+ selection.addRange(range);
+ range = document.createRange();
+ range.setStart(document.getElementById("r5"), 0);
+ range.setEnd(document.getElementById("r5"), 1);
+ selection.addRange(range);
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 2,
+ "#3-4, #5-2 nsITableEditor.getSelectedCells() should return an array whose length is 2");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c3-4"),
+ "#3-4, #5-2 nsITableEditor.getSelectedCells() should set the first item of the result to the last cell element in the third row");
+ is(SpecialPowers.unwrap(cells[1]), document.getElementById("c5-2"),
+ "#3-4, #5-2 nsITableEditor.getSelectedCells() should set the second item of the result to the first cell element in the fifth row");
+ })();
+
+ (function test_with_selecting_1_2_and_1_3_and_1_4_and_2_1_and_2_2() {
+ selection.removeAllRanges();
+ let tr = document.getElementById("r1");
+ let range = document.createRange();
+ range.setStart(tr, 1);
+ range.setEnd(tr, 2);
+ selection.addRange(range);
+ range = document.createRange();
+ range.setStart(tr, 2);
+ range.setEnd(tr, 3);
+ selection.addRange(range);
+ range = document.createRange();
+ range.setStart(tr, 3);
+ range.setEnd(tr, 4);
+ selection.addRange(range);
+ tr = document.getElementById("r2");
+ range = document.createRange();
+ range.setStart(tr, 0);
+ range.setEnd(tr, 1);
+ selection.addRange(range);
+ range = document.createRange();
+ range.setStart(tr, 1);
+ range.setEnd(tr, 2);
+ selection.addRange(range);
+
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 5,
+ "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should return an array whose length is 5");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c1-2"),
+ "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the first item of the result to the second cell element in the first row");
+ is(SpecialPowers.unwrap(cells[1]), document.getElementById("c1-3"),
+ "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the second item of the result to the third cell element in the first row");
+ is(SpecialPowers.unwrap(cells[2]), document.getElementById("c1-4"),
+ "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the third item of the result to the forth cell element in the first row");
+ is(SpecialPowers.unwrap(cells[3]), document.getElementById("c2-1"),
+ "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the forth item of the result to the first cell element in the second row");
+ is(SpecialPowers.unwrap(cells[4]), document.getElementById("c2-2"),
+ "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the forth item of the result to the second cell element in the second row");
+ })();
+
+ (function test_with_selecting_6_3_and_paragraph_in_6_4_and_6_5() {
+ selection.removeAllRanges();
+ let tr = document.getElementById("r6");
+ let range = document.createRange();
+ range.setStart(tr, 1);
+ range.setEnd(tr, 2);
+ selection.addRange(range);
+ range = document.createRange();
+ range.setStart(document.getElementById("c6-4").firstChild, 0);
+ range.setEnd(document.getElementById("c6-4").firstChild, 1);
+ selection.addRange(range);
+ range = document.createRange();
+ range.setStart(tr, 3);
+ range.setEnd(tr, 4);
+ selection.addRange(range);
+
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 2,
+ "#6-3, #6-5 nsITableEditor.getSelectedCells() should return an array whose length is 2");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c6-3"),
+ "#6-3, #6-5 nsITableEditor.getSelectedCells() should set the first item of the result to the second cell element in the sixth row");
+ // The <p> element in c6-4 is selected, this does not select the cell
+ // element so that it should be ignored.
+ is(SpecialPowers.unwrap(cells[1]), document.getElementById("c6-5"),
+ "#6-3, #6-5 nsITableEditor.getSelectedCells() should set the first item of the result to the forth cell element in the sixth row");
+ })();
+
+ (function test_with_selecting_2_3_and_text_in_4_1_and_7_2() {
+ selection.removeAllRanges();
+ let tr = document.getElementById("r2");
+ let range = document.createRange();
+ range.setStart(tr, 2);
+ range.setEnd(tr, 3);
+ selection.addRange(range);
+ range = document.createRange();
+ range.setStart(document.getElementById("c4-1").firstChild, 0);
+ range.setEnd(document.getElementById("c4-1").firstChild, 7);
+ selection.addRange(range);
+ tr = document.getElementById("r7");
+ range = document.createRange();
+ range.setStart(tr, 0);
+ range.setEnd(tr, 1);
+ selection.addRange(range);
+
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 2,
+ "#2-3, #7-2 nsITableEditor.getSelectedCells() should return an array whose length is 2");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c2-3"),
+ "#2-3, #7-2 nsITableEditor.getSelectedCells() should set the first item of the result to the third cell element in the second row");
+ // Text in c4-1 is selected, this does not select the cell element so that
+ // it should be ignored. Note that we've ignored the following selected
+ // cell elements in old API, but it causes inconsistent behavior with the
+ // previous test case. Therefore, we take this behavior.
+ is(SpecialPowers.unwrap(cells[1]), document.getElementById("c7-2"),
+ "#2-3, #7-2 nsITableEditor.getSelectedCells() should set the second item of the result to the cell element in the seventh row");
+ })();
+
+ (function test_with_selecting_3_2_and_2_1_1_and_7_2() {
+ selection.removeAllRanges();
+ let tr = document.getElementById("r3");
+ let range = document.createRange();
+ range.setStart(tr, 0);
+ range.setEnd(tr, 1);
+ selection.addRange(range);
+ tr = document.getElementById("r2-1");
+ range = document.createRange();
+ range.setStart(tr, 0);
+ range.setEnd(tr, 1);
+ selection.addRange(range);
+ tr = document.getElementById("r7");
+ range = document.createRange();
+ range.setStart(tr, 0);
+ range.setEnd(tr, 1);
+ selection.addRange(range);
+
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 3,
+ "#3-2, #2-1-1, #7-2 nsITableEditor.getSelectedCells() should return an array whose length is 3");
+ is(SpecialPowers.unwrap(cells[0]), document.getElementById("c3-2"),
+ "#3-2, #2-1-1, #7-2 nsITableEditor.getSelectedCells() should set the first item of the result to the first cell element in the third row");
+ // c2-1-1 is in another <table>, however, getSelectedCells() returns it
+ // since it works only with ranges of Selection.
+ is(SpecialPowers.unwrap(cells[1]), document.getElementById("c2-1-1"),
+ "#3-2, #2-1-1, #7-2 nsITableEditor.getSelectedCells() should set the second item of the result to the cell element in the child <table> element");
+ is(SpecialPowers.unwrap(cells[2]), document.getElementById("c7-2"),
+ "#3-2, #2-1-1, #7-2 nsITableEditor.getSelectedCells() should set the third item of the result to the cell element in the seventh row");
+ })();
+
+ (function test_with_selecting_all_children_of_cell() {
+ selection.selectAllChildren(document.getElementById("c6-4"));
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 0,
+ "nsITableEditor.getSelectedCells() should return an empty array when no cell element is selected");
+ })();
+
+ (function test_with_selecting_text_in_cell() {
+ let cell = document.getElementById("c6-5");
+ selection.collapse(cell.firstChild, 0);
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 0,
+ "nsITableEditor.getSelectedCells() should return an empty array when selecting text in a cell element");
+ })();
+
+ (function test_with_selecting_text_in_1_1_and_1_2() {
+ let cell = document.getElementById("c1-1");
+ selection.setBaseAndExtent(cell.firstChild, 0, cell.firstChild, 3);
+ let range = document.createRange();
+ range.setStart(cell.parentNode, 1);
+ range.setEnd(cell.parentNode, 2);
+ selection.addRange(range);
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 0,
+ "nsITableEditor.getSelectedCells() should return an empty array when the first range does not select a cell element");
+ })();
+
+ (function test_without_selection_ranges() {
+ selection.removeAllRanges();
+ let cells = getTableEditor().getSelectedCells();
+ is(cells.length, 0,
+ "nsITableEditor.getSelectedCells() should return an empty array even when there is no selection range");
+ })();
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_getSelectedCellsType.html b/editor/libeditor/tests/test_nsITableEditor_getSelectedCellsType.html
new file mode 100644
index 0000000000..33884b8e20
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getSelectedCellsType.html
@@ -0,0 +1,251 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for nsITableEditor.getSelectedCellsType()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+
+ const kTableSelectionMode_None = 0;
+ const kTableSelectionMode_Cell = 1;
+ const kTableSelectionMode_Row = 2;
+ const kTableSelectionMode_Column = 3;
+
+ (function test_without_selection_range() {
+ selection.removeAllRanges();
+ try {
+ getTableEditor().getSelectedCellsType(null);
+ ok(false, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when editor does not have Selection");
+ } catch (e) {
+ ok(true, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when editor does not have Selection");
+ }
+ })();
+
+ (function test_without_table() {
+ editor.innerHTML = "<p>Here is a paragraph</p>";
+ selection.collapse(editor.querySelector("p").firstChild, 0);
+ try {
+ getTableEditor().getSelectedCellsType(null);
+ ok(false, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when editor does not have a <table>");
+ } catch (e) {
+ ok(true, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when editor does not have a <table>");
+ }
+ })();
+
+ (function test_with_selection_outside_table() {
+ editor.innerHTML = "<p>Here is a paragraph before the table</p>" +
+ "<table><tr><td>cell</td></tr></table>";
+ selection.collapse(editor.querySelector("p").firstChild, 0);
+ try {
+ getTableEditor().getSelectedCellsType(null);
+ ok(false, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when selection is outside the <table>");
+ } catch (e) {
+ ok(true, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when selection is outside the <table>");
+ }
+ })();
+
+ (function test_with_selection_in_text_in_cell() {
+ editor.innerHTML = "<table><tr><td>cell</td></tr></table>";
+ selection.collapse(editor.querySelector("td").firstChild, 0);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_None,
+ "nsITableEditor.getSelectedCellsType(null) should return None when selection is collapsed in a text node in a cell");
+ })();
+
+ (function test_with_selecting_a_cell_which_is_only_one_cell_in_table() {
+ editor.innerHTML = "<table><tr><td>cell</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row,
+ "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects the cell which is only one in the table");
+ })();
+
+ (function test_with_selecting_a_cell_whose_table_has_one_row() {
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Column,
+ "nsITableEditor.getSelectedCellsType(null) should return Column when selection selects a cell in the first row which is only one in the table");
+ })();
+
+ (function test_with_selecting_a_cell_whose_table_has_one_column() {
+ editor.innerHTML = "<table><tr><td>cell1</td></tr><tr><td>cell2</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row,
+ "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects a cell in the first row which is only one in the row");
+ })();
+
+ (function test_with_selecting_a_cell_at_1_1_whose_table_has_2x2_cells() {
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell,
+ "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects a cell at first row and first column");
+ })();
+
+ (function test_with_selecting_a_cell_at_1_2_whose_table_has_2x2_cells() {
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tr"), 1, editor.querySelector("tr"), 2);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell,
+ "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects a cell at first row and second column");
+ })();
+
+ (function test_with_selecting_a_cell_at_2_1_whose_table_has_2x2_cells() {
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tr + tr"), 0, editor.querySelector("tr + tr"), 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell,
+ "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects a cell at second row and first column");
+ })();
+
+ (function test_with_selecting_a_cell_at_2_2_whose_table_has_2x2_cells() {
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tr + tr"), 1, editor.querySelector("tr + tr"), 2);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell,
+ "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects a cell at second row and second column");
+ })();
+
+ (function test_with_selecting_a_cell_at_1_1_whose_colspan_2_and_whose_table_has_2x2_cells() {
+ editor.innerHTML = "<table><tr><td colspan=\"2\">cell1</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row,
+ "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects a cell whose colspan is 2 at first row");
+ })();
+
+ (function test_with_selecting_a_cell_at_1_1_whose_rowspan_2_and_whose_table_has_2x2_cells() {
+ editor.innerHTML = "<table><tr><td rowspan=\"2\">cell1</td><td>cell2</td></tr><tr><td>cell4</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Column,
+ "nsITableEditor.getSelectedCellsType(null) should return Column when selection selects a cell whose row is 2 at first column");
+ })();
+
+ (function test_with_selecting_all_cells_in_first_row_whose_table_has_2x2_cells() {
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ synthesizeMouseAtCenter(editor.querySelector("td"), {accelKey: true});
+ synthesizeMouseAtCenter(editor.querySelector("td + td"), {accelKey: true});
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row,
+ "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects all cells in the first row whose table has 2 rows and 2 columns");
+ })();
+
+ (function test_with_selecting_all_cells_in_first_column_whose_table_has_2x2_cells() {
+ selection.removeAllRanges();
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ synthesizeMouseAtCenter(editor.querySelector("td"), {accelKey: true});
+ synthesizeMouseAtCenter(editor.querySelector("tr + tr > td"), {accelKey: true});
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Column,
+ "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects all cells in the first column whose table has 2 rows and 2 columns");
+ })();
+
+ (function test_with_selecting_all_cells_whose_table_has_2x2_cells() {
+ selection.removeAllRanges();
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ synthesizeMouseAtCenter(editor.querySelector("td"), {accelKey: true});
+ synthesizeMouseAtCenter(editor.querySelector("td + td"), {accelKey: true});
+ synthesizeMouseAtCenter(editor.querySelector("tr + tr > td"), {accelKey: true});
+ synthesizeMouseAtCenter(editor.querySelector("tr + tr > td + td"), {accelKey: true});
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row,
+ "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects all cells whose table has 2 rows and 2 columns");
+ })();
+
+ (function test_with_selecting_a_raw() {
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("tbody"), 0, editor.querySelector("tbody"), 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_None,
+ "nsITableEditor.getSelectedCellsType(null) should return None when selection selects a row");
+ })();
+
+ (function test_with_selecting_a_tbody() {
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ selection.setBaseAndExtent(editor.querySelector("table"), 0, editor.querySelector("table"), 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_None,
+ "nsITableEditor.getSelectedCellsType(null) should return None when selection selects a tbody");
+ })();
+
+ (function test_with_selecting_a_table() {
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ selection.setBaseAndExtent(editor, 0, editor, 1);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_None,
+ "nsITableEditor.getSelectedCellsType(null) should return None when selection selects a table");
+ })();
+
+ (function test_with_selecting_cells_in_different_table() {
+ selection.removeAllRanges();
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" +
+ "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ let range = document.createRange();
+ range.selectNode(editor.querySelector("td"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(editor.querySelector("table + table td"));
+ selection.addRange(range);
+ is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell,
+ "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects 2 cells in different tables");
+ })();
+
+ (function test_with_selecting_a_cell_in_the_table() {
+ selection.removeAllRanges();
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" +
+ "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ let range = document.createRange();
+ range.selectNode(editor.querySelector("td"));
+ selection.addRange(range);
+ is(getTableEditor().getSelectedCellsType(editor.querySelector("table")), kTableSelectionMode_Cell,
+ "nsITableEditor.getSelectedCellsType(editor.querySelector(\"table\")) should return Cell when selection selects a cell in the table");
+ })();
+
+ (function test_with_selecting_a_cell_in_the_table_specifying_different_cell() {
+ selection.removeAllRanges();
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" +
+ "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ let range = document.createRange();
+ range.selectNode(editor.querySelector("tr + tr > td"));
+ selection.addRange(range);
+ is(getTableEditor().getSelectedCellsType(editor.querySelector("td")), kTableSelectionMode_Cell,
+ "nsITableEditor.getSelectedCellsType(editor.querySelector(\"td\")) should return Cell when selection selects a cell in the table");
+ })();
+
+ (function test_with_selecting_a_cell_in_the_other_table() {
+ selection.removeAllRanges();
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" +
+ "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ let range = document.createRange();
+ range.selectNode(editor.querySelector("td"));
+ selection.addRange(range);
+ todo_is(getTableEditor().getSelectedCellsType(editor.querySelector("table + table")), kTableSelectionMode_None,
+ "nsITableEditor.getSelectedCellsType(editor.querySelector(\"table + table\")) should return None when selection selects a cell in the other table");
+ })();
+
+ (function test_with_selecting_a_cell_in_the_other_table_specifying_a_cell_in_the_other_one() {
+ selection.removeAllRanges();
+ editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" +
+ "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>";
+ let range = document.createRange();
+ range.selectNode(editor.querySelector("td"));
+ selection.addRange(range);
+ todo_is(getTableEditor().getSelectedCellsType(editor.querySelector("table + table td")), kTableSelectionMode_None,
+ "nsITableEditor.getSelectedCellsType(editor.querySelector(\"table + table td\")) should return None when selection selects a cell in the other table");
+ })();
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_getSelectedOrParentTableElement.html b/editor/libeditor/tests/test_nsITableEditor_getSelectedOrParentTableElement.html
new file mode 100644
index 0000000000..665359545c
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getSelectedOrParentTableElement.html
@@ -0,0 +1,283 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.getSelectedOrParentTableElement()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+
+ selection.collapse(editor, 0);
+ let tagNameWrapper = {};
+ let countWrapper = {};
+ let cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, null,
+ "nsITableEditor.getSelectedOrParentTableElement() should return null if Selection does not select cells nor in <table>");
+ is(tagNameWrapper.value, "",
+ "nsITableEditor.getSelectedOrParentTableElement() should return empty string to tag name if Selection does not select cells nor in <table>");
+ is(countWrapper.value, 0,
+ "nsITableEditor.getSelectedOrParentTableElement() should return 0 to count if Selection does not select cells nor in <table>");
+
+ editor.innerHTML =
+ '<table id="table">' +
+ '<tr id="r1"><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' +
+ '<tr id="r2"><th id="c2-1" rowspan="2">cell2-1</th><th id="c2-2">cell2-2</th><td id="c2-3">cell2-3</td></tr>' +
+ '<tr id="r3"><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' +
+ '<tr id="r4"><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">cell4-2</td><td id="c4-3">' +
+ '<table id="table2">' +
+ '<tr id="r2-1"><th id="c2-1-1">cell2-1-1-</td><td id="c2-1-2">cell2-1-2</td></tr>' +
+ '<tr id="r2-2"><td id="c2-2-1">cell2-2-1-</td><th id="c2-2-2">cell2-2-2</th></tr>' +
+ "</table>" +
+ '</td><th id="c4-4">cell4-4</th><td id="c4-5">cell4-5</td></tr>' +
+ '<tr id="r5"><th id="c5-2">cell5-2</th><th id="c5-3" colspan="2">cell5-3</th><td id="c5-5">cell5-5</td></tr>' +
+ '<tr id="r6"><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' +
+ '<tr id="r7"><td id="c7-2" colspan="4">cell7-2</td></tr>' +
+ "</table>";
+
+ let tr = document.getElementById("r1");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c1-1"),
+ "#1-1 nsITableEditor.getSelectedOrParentTableElement() should return the first cell element in the first row");
+ is(tagNameWrapper.value, "td",
+ "#1-1 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell is selected");
+ is(countWrapper.value, 1,
+ "#1-1 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell is selected");
+
+ tr = document.getElementById("r1");
+ selection.setBaseAndExtent(tr, 3, tr, 4);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c1-4"),
+ "#1-4 nsITableEditor.getSelectedOrParentTableElement() should return the last cell element whose colspan and rowspan are 2 in the first row");
+ is(tagNameWrapper.value, "td",
+ "#1-4 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell is selected");
+ is(countWrapper.value, 1,
+ "#1-4 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell is selected");
+
+ tr = document.getElementById("r2");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c2-1"),
+ "#2-1 nsITableEditor.getSelectedOrParentTableElement() should return the first cell element in the second row");
+ is(tagNameWrapper.value, "td",
+ "#2-1 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell is selected but even if the cell is <th>");
+ is(countWrapper.value, 1,
+ "#2-1 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell is selected");
+
+ tr = document.getElementById("r7");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c7-2"),
+ "#7-2 nsITableEditor.getSelectedOrParentTableElement() should return the second cell element in the last row");
+ is(tagNameWrapper.value, "td",
+ "#7-2 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell is selected");
+ is(countWrapper.value, 1,
+ "#7-2 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell is selected");
+
+ selection.removeAllRanges();
+ let range = document.createRange();
+ range.selectNode(document.getElementById("c2-2"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("c2-3"));
+ selection.addRange(range);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c2-2"),
+ "#2-2 nsITableEditor.getSelectedOrParentTableElement() should return the second cell element in the second row");
+ is(tagNameWrapper.value, "td",
+ "#2-2 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when first range selects a cell");
+ is(countWrapper.value, 2,
+ "#2-2 nsITableEditor.getSelectedOrParentTableElement() should return 2 to count when there are 2 selection ranges");
+
+ selection.removeAllRanges();
+ range = document.createRange();
+ range.selectNode(document.getElementById("c3-4"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("c5-2"));
+ selection.addRange(range);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c3-4"),
+ "#3-4 nsITableEditor.getSelectedOrParentTableElement() should return the last cell element in the third row");
+ is(tagNameWrapper.value, "td",
+ "#3-4 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when first range selects a cell");
+ is(countWrapper.value, 2,
+ "#3-4 nsITableEditor.getSelectedOrParentTableElement() should return 2 to count when there are 2 selection ranges");
+
+ cell = document.getElementById("c2-2");
+ selection.removeAllRanges();
+ range = document.createRange();
+ range.setStart(cell.firstChild, 0);
+ selection.addRange(range);
+ cell = document.getElementById("c2-1-1");
+ range = document.createRange();
+ range.setStart(cell.firstChild, 1);
+ range.setEnd(cell.firstChild, 2);
+ selection.addRange(range);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c2-1-1"),
+ "#2-1-1 nsITableEditor.getSelectedOrParentTableElement() should return the cell which contains the last selection range if first selection range does not select a cell");
+ is(tagNameWrapper.value, "td",
+ "#2-1-1 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when the first range does not select a cell and the last range is in a cell");
+ is(countWrapper.value, 0,
+ "#2-1-1 nsITableEditor.getSelectedOrParentTableElement() should return 0 to count when the first range does not select a cell");
+
+ tr = document.getElementById("r2-2");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c2-2-1"),
+ "#2-2-1 nsITableEditor.getSelectedOrParentTableElement() should return the first cell element in the first row of nested <table>");
+ is(tagNameWrapper.value, "td",
+ "#2-2-1 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell in nested <table> is selected");
+ is(countWrapper.value, 1,
+ "#2-2-1 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell in nested <table> is selected");
+
+ cell = document.getElementById("c2-1-2");
+ selection.setBaseAndExtent(cell.firstChild, 0, cell.firstChild, 0);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c2-1-2"),
+ "#2-1-2 nsITableEditor.getSelectedOrParentTableElement() should return the first cell element in the first row of nested <table>");
+ is(tagNameWrapper.value, "td",
+ "#2-1-2 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell in nested <table> contains the first selection range");
+ is(countWrapper.value, 0,
+ "#2-1-2 nsITableEditor.getSelectedOrParentTableElement() should return 0 to count when a cell in nested <table> contains the first selection range");
+
+ let table = document.getElementById("table2");
+ selection.removeAllRanges();
+ range = document.createRange();
+ range.selectNode(table);
+ selection.addRange(range);
+ table = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(table, document.getElementById("table2"),
+ "nsITableEditor.getSelectedOrParentTableElement() should return a <table> element which is selected");
+ is(tagNameWrapper.value, "table",
+ "nsITableEditor.getSelectedOrParentTableElement() should return 'table' to tag name when a <table> is selected");
+ is(countWrapper.value, 1,
+ "nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a <table> is selected");
+
+ selection.removeAllRanges();
+ range = document.createRange();
+ range.selectNode(document.getElementById("r2-1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("r2-2"));
+ selection.addRange(range);
+ table = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(table, document.getElementById("r2-2"),
+ "nsITableEditor.getSelectedOrParentTableElement() should return a <tr> element which is selected by the last selection range");
+ is(tagNameWrapper.value, "tr",
+ "nsITableEditor.getSelectedOrParentTableElement() should return 'tr' to tag name when a <tr> is selected");
+ is(countWrapper.value, 1,
+ "nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a <tr> is selected");
+
+ selection.removeAllRanges();
+ range = document.createRange();
+ range.selectNode(document.getElementById("r1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("c5-5"));
+ selection.addRange(range);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, document.getElementById("c5-5"),
+ "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return the cell selected by the last range when first range selects <tr>");
+ is(tagNameWrapper.value, "td",
+ "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name if the last range selects the cell when first range selects <tr>");
+ is(countWrapper.value, 2,
+ "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return 2 to count if the last range selects <td> when first range selects <tr>");
+
+ selection.removeAllRanges();
+ range = document.createRange();
+ range.selectNode(document.getElementById("r1"));
+ selection.addRange(range);
+ range = document.createRange();
+ range.selectNode(document.getElementById("c5-2"));
+ selection.addRange(range);
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ is(cell, null,
+ "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return null if the last range selects <th> when first range selects <tr>");
+ is(tagNameWrapper.value, "",
+ "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return empty string to tag name if the last range selects <th> when first range selects <tr>");
+ is(countWrapper.value, 0,
+ "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return 0 to count if the last range selects <th> when first range selects <tr>");
+
+ // XXX If cell is not selected, nsITableEditor.getSelectedOrParentTableElement()
+ // returns null without throwing exception, however, if there is no
+ // selection ranges, throwing an exception. This inconsistency is odd.
+ selection.removeAllRanges();
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper));
+ ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if there is no selection ranges");
+ } catch (e) {
+ ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if there is no selection ranges");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement());
+ ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if it does not have argument");
+ } catch (e) {
+ ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if it does not have argument");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(null));
+ ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its argument is only one null");
+ } catch (e) {
+ ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its argument is only one null");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(null, null));
+ ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its arguments are all null");
+ } catch (e) {
+ ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its arguments are all null");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, null));
+ ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its count argument is null");
+ } catch (e) {
+ ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its count argument is null");
+ }
+
+ tr = document.getElementById("r6");
+ selection.setBaseAndExtent(tr, 0, tr, 1);
+ try {
+ cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(null, countWrapper));
+ ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its tag name argument is null");
+ } catch (e) {
+ ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its tag name argument is null");
+ }
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_getTableSize.html b/editor/libeditor/tests/test_nsITableEditor_getTableSize.html
new file mode 100644
index 0000000000..986d8e3e49
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getTableSize.html
@@ -0,0 +1,94 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.getTableSize()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+ let rowCount = {}, columnCount = {};
+
+ try {
+ getTableEditor().getTableSize(undefined, rowCount, columnCount);
+ ok(false, "nsITableEditor.getTableSize(undefined) should cause throwing an exception");
+ } catch (e) {
+ ok(true, "nsITableEditor.getTableSize(undefined) should cause throwing an exception");
+ }
+
+ try {
+ getTableEditor().getTableSize(null, rowCount, columnCount);
+ ok(false, "nsITableEditor.getTableSize(null) should cause throwing an exception");
+ } catch (e) {
+ ok(true, "nsITableEditor.getTableSize(null) should cause throwing an exception");
+ }
+
+ try {
+ getTableEditor().getTableSize(editor, rowCount, columnCount);
+ ok(false, "nsITableEditor.getTableSize() should cause throwing an exception if given node is not in a <table>");
+ } catch (e) {
+ ok(true, "nsITableEditor.getTableSize() should cause throwing an exception if given node is not in a <table>");
+ }
+
+ // Set id to "test" for the argument for getTableSize().
+ // Set data-rows and data-cols to expected count of them.
+ const kTests = [
+ '<table><tr><td id="test" data-rows="2" data-cols="3">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>',
+ '<table><tr id="test" data-rows="2" data-cols="3"><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>',
+ '<table id="test" data-rows="2" data-cols="3"><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>',
+ '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td><p id="test" data-rows="2" data-cols="3">cell2-3</p></td></tr></table>',
+ '<table><caption id="test" data-rows="2" data-cols="3">caption</caption><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>',
+ '<table id="test" data-rows="0" data-cols="0"></table>',
+ '<table id="test" data-rows="0" data-cols="0"><caption>caption</caption></table>',
+ '<table id="test" data-rows="1" data-cols="1"><td>cell1-1</td></table>',
+ // rowspan does not affect, but colspan affects...
+ '<table id="test" data-rows="1" data-cols="12"><tr><td rowspan="8" colspan="12">cell1-1</td></tr></table>',
+ '<table id="test" data-rows="1" data-cols="1"><tr><td><table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>',
+ '<table><tr><td id="test" data-rows="1" data-cols="1"><table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>',
+ '<table><tr><td><table id="test" data-rows="3" data-cols="2"><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>',
+ '<table><tr><td><table><tr><td id="test" data-rows="3" data-cols="2">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>',
+ '<table><tr><td><table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td><p id="test" data-rows="3" data-cols="2">cell2-2</p></td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>',
+ ];
+
+ for (const kTest of kTests) {
+ editor.innerHTML = kTest;
+ let element = document.getElementById("test");
+ getTableEditor().getTableSize(element, rowCount, columnCount);
+ is(rowCount.value.toString(10), element.getAttribute("data-rows"),
+ `Specified an element in a <table> directly, its parent table row count should be retrieved: ${kTest}`);
+ is(columnCount.value.toString(10), element.getAttribute("data-cols"),
+ `Specified an element in a <table> directly, its parent table column count should be retrieved: ${kTest}`);
+ if (element.firstChild && element.firstChild.nodeType == Node.TEXT_NODE) {
+ selection.collapse(element.firstChild, 0);
+ getTableEditor().getTableSize(null, rowCount, columnCount);
+ is(rowCount.value.toString(10), element.getAttribute("data-rows"),
+ `Selection is collapsed in a cell element, its parent table row count should be retrieved: ${kTest}`);
+ is(columnCount.value.toString(10), element.getAttribute("data-cols"),
+ `Selection is collapsed in a cell element, its parent table column count should be retrieved: ${kTest}`);
+ }
+ }
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html b/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html
new file mode 100644
index 0000000000..ccd8ed5c7e
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html
@@ -0,0 +1,558 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.insertTableCell()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+ let selectionRanges = [];
+
+ function checkInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, false,
+ `"${aEvent.type}" event should be never cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, "",
+ `inputType of "${aEvent.type}" event should be empty string ${aDescription}`);
+ is(aEvent.data, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ is(targetRanges.length, selectionRanges.length,
+ `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`);
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0,
+ `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`);
+ }
+ }
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editor.addEventListener("beforeinput", onBeforeInput);
+ editor.addEventListener("input", onInput);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.collapse(editor.firstChild, 0);
+ getTableEditor().insertTableCell(1, false);
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.insertTableCell(1, false) should do nothing if selection is not in <table>");
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when a call of nsITableEditor.insertTableCell(1, false) even though it will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed outside table element (nsITableEditor.insertTableCell(1, false))");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.insertTableCell(1, false) does nothing');
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().insertTableCell(1, true);
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.insertTableCell(1, true) should do nothing if selection is not in <table>");
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when a call of nsITableEditor.insertTableCell(1, true) even though it will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.insertTableCell(1, true))");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.insertTableCell(1, true) does nothing');
+
+ selection.removeAllRanges();
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().insertTableCell(1, false);
+ ok(false, "getTableEditor().insertTableCell(1, false) without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().insertTableCell(1, false) without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.insertTableCell(1, false) causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.insertTableCell(1, false) causes exception due to no selection range');
+ }
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().insertTableCell(1, true);
+ ok(false, "getTableEditor().insertTableCell(1, true) without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().insertTableCell(1, true) without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.insertTableCell(1, true) causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.insertTableCell(1, true) causes exception due to no selection range');
+ }
+
+ (function testInsertZeroCellsBefore() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableCell(0, false);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "testInsertZeroCellsBefore: nsITableEditor.insertTableCell(0, false) should do nothing without throwing exception"
+ );
+ is(
+ beforeInputEvents.length,
+ 0,
+ 'testInsertZeroCellsBefore: No "beforeinput" event should be fired'
+ );
+ is(
+ inputEvents.length,
+ 0,
+ 'testInsertZeroCellsBefore: No "input" event should be fired'
+ );
+ })();
+
+ (function testInsertZeroCellsAfter() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableCell(0, true);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "testInsertZeroCellsAfter: nsITableEditor.insertTableCell(0, true) should do nothing without throwing exception"
+ );
+ is(
+ beforeInputEvents.length,
+ 0,
+ 'testInsertZeroCellsAfter: No "beforeinput" event should be fired'
+ );
+ is(
+ inputEvents.length,
+ 0,
+ 'testInsertZeroCellsAfter: No "input" event should be fired'
+ );
+ })();
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableCell(1, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td valign="top"><br></td><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell in middle row (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in middle row (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell in middle row (before)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableCell(1, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td valign="top"><br></td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell in middle row (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in middle row (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell in middle row (after)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row (after)");
+
+ (function testInsertBeforeCellFollowingTextNode() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ "<tr><td>cell1-1</td> <td>cell1-2</td> <td>cell1-3</td></tr>" +
+ '<tr><td>cell2-1</td> <td id="select">cell2-2</td> <td>cell2-3</tr>' +
+ "<tr><td>cell3-1</td> <td>cell3-2</td> <td>cell3-3</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableCell(1, false);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ "<tr><td>cell1-1</td> <td>cell1-2</td> <td>cell1-3</td></tr>" +
+ '<tr><td>cell2-1</td> <td valign="top"><br></td><td id="select">cell2-2</td> <td>cell2-3</td></tr>' +
+ "<tr><td>cell3-1</td> <td>cell3-2</td> <td>cell3-3</td></tr>" +
+ "</tbody></table>",
+ "testInsertBeforeCellFollowingTextNode: nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection"
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertBeforeCellFollowingTextNode: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection collapsed in a cell following a text node (testInsertBeforeCellFollowingTextNode)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertBeforeCellFollowingTextNode: Only one "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection collapsed in a cell following a text node (testInsertBeforeCellFollowingTextNode)"
+ );
+ })();
+
+ (function testInsertAfterCellFollowedByTextNode() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ "<tr><td>cell1-1</td> <td>cell1-2</td> <td>cell1-3</td></tr>" +
+ '<tr><td>cell2-1</td> <td id="select">cell2-2</td> <td>cell2-3</tr>' +
+ "<tr><td>cell3-1</td> <td>cell3-2</td> <td>cell3-3</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableCell(1, true);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ "<tr><td>cell1-1</td> <td>cell1-2</td> <td>cell1-3</td></tr>" +
+ '<tr><td>cell2-1</td> <td id="select">cell2-2</td><td valign="top"><br></td> <td>cell2-3</td></tr>' +
+ "<tr><td>cell3-1</td> <td>cell3-2</td> <td>cell3-3</td></tr>" +
+ "</tbody></table>",
+ "testInsertAfterCellFollowedByTextNode: nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection"
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertAfterCellFollowedByTextNode: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection collapsed in a cell followed by a text node (testInsertAfterCellFollowedByTextNode)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertAfterCellFollowedByTextNode: Only one "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection collapsed in a cell followed by a text node (testInsertAfterCellFollowedByTextNode)"
+ );
+ })();
+
+ // with rowspan.
+
+ // Odd case. This puts the cell containing selection moves right of row-spanning cell.
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td rowspan="2">cell1-2</td></tr>' +
+ '<tr><td id="select">cell2-1</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableCell(1, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td rowspan="2">cell1-2</td></tr>' +
+ '<tr><td valign="top"><br></td><td id="select">cell2-1</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection and moves the cell to right of the row-spanning cell element");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (before)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' +
+ '<tr><td id="select">cell2-1</td></tr>' +
+ "<tr><td>cell3-1</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableCell(1, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' +
+ '<tr><td id="select">cell2-1</td><td valign="top"><br></td></tr>' +
+ "<tr><td>cell3-1</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection and moves the cell to right of the row-spanning cell element");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (after)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (after)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().insertTableCell(2, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td valign="top"><br></td><td valign="top"><br></td><td id="select" rowspan="2">cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(2, false) should insert 2 cells before the row-spanning cell containing selection");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell in row-spanning (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in row-spanning (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell in row-spanning (before)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell in row-spanning (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().insertTableCell(2, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td><td valign="top"><br></td><td valign="top"><br></td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(2, false) should insert 2 cells after the row-spanning cell containing selection");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell in row-spanning (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in row-spanning (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell in row-spanning (after)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell in row-spanning (after)");
+
+ // with colspan
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableCell(1, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td valign="top"><br></td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection but do not modify col-spanning cell");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (before)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td colspan="3">cell2-1</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableCell(1, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td valign="top"><br></td><td>cell1-3</td></tr>' +
+ '<tr><td colspan="3">cell2-1</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection but do not modify col-spanning cell");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (after)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (after)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
+ '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().insertTableCell(2, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(2, false) should insert 2 cells before the col-spanning cell containing selection");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell which is col-spanning (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell which is col-spanning (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell which is col-spanning (before)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell which is col-spanning (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
+ '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().insertTableCell(2, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
+ '<tr><td id="select" colspan="2">cell2-1</td><td valign="top"><br></td><td valign="top"><br></td><td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableCell(2, false) should insert 2 cells after the col-spanning cell containing selection");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection collapsed in a cell which is col-spanning (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell which is col-spanning (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection collapsed in a cell which is col-spanning (after)');
+ checkInputEvent(inputEvents[0], "when selection collapsed in a cell which is col-spanning (after)");
+
+ editor.removeEventListener("beforeinput", onBeforeInput);
+ editor.removeEventListener("input", onInput);
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html b/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html
new file mode 100644
index 0000000000..acf853aed1
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html
@@ -0,0 +1,547 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.insertTableColumn()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+ let selectionRanges = [];
+
+ function checkInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, false,
+ `"${aEvent.type}" event should be never cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, "",
+ `inputType of "${aEvent.type}" event should be empty string ${aDescription}`);
+ is(aEvent.data, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ is(targetRanges.length, selectionRanges.length,
+ `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`);
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0,
+ `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`);
+ }
+ }
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editor.addEventListener("beforeinput", onBeforeInput);
+ editor.addEventListener("input", onInput);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.collapse(editor.firstChild, 0);
+ getTableEditor().insertTableColumn(1, false);
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.insertTableColumn(1, false) should do nothing if selection is not in <table>");
+ is(beforeInputEvents.length, 1,
+ 'beforeinput" event should be fired when a call of nsITableEditor.insertTableColumn(1, false) even though it will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.insertTableColumn(1, false))");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.insertTableColumn(1, false) does nothing');
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().insertTableColumn(1, true);
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.insertTableColumn(1, true) should do nothing if selection is not in <table>");
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when a call of nsITableEditor.insertTableColumn(1, true) even though it will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.insertTableColumn(1, true))");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.insertTableColumn(1, true) does nothing');
+
+ selection.removeAllRanges();
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().insertTableColumn(1, false);
+ ok(false, "getTableEditor().insertTableColumn(1, false) without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().insertTableColumn(1, false) without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.insertTableColumn(1, false) causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.insertTableColumn(1, false) causes exception due to no selection range');
+ }
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().insertTableColumn(1, true);
+ ok(false, "getTableEditor().insertTableColumn(1, true) without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().insertTableColumn(1, true) without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.insertTableColumn(1, true) causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.insertTableColumn(1, true) causes exception due to no selection range');
+ }
+
+ (function testInsertBeforeFirstColumn() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableColumn(1, false);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ '<tr><td valign="top"><br></td><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td valign="top"><br></td><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "testInsertBeforeFirstColumn: nsITableEditor.insertTableColumn(1, false) should insert a column before the first column"
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertBeforeFirstColumn: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection is collapsed in the first column (testInsertBeforeFirstColumn)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertBeforeFirstColumn: Only one "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection is collapsed in the first column (testInsertBeforeFirstColumn)"
+ );
+ })();
+
+ (function testInsertAfterLastColumn() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableColumn(1, true);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td><td valign="top"><br></td></tr>' +
+ '<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td><td valign="top"><br></td></tr>' +
+ "</tbody></table>",
+ "testInsertAfterLastColumn: nsITableEditor.insertTableColumn(1, true) should insert a column after the last column"
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertAfterLastColumn: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection is collapsed in the last column (testInsertAfterLastColumn)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertAfterLastColumn: One "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection is collapsed in the last column (testInsertAfterLastColumn)"
+ );
+ })();
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableColumn(1, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td valign="top"><br></td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td>cell2-1</td><td valign="top"><br></td><td>cell2-2</td><td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableColumn(1, false) should insert a column to left of the second column");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second column (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second column (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell in second column (before)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableColumn(1, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td valign="top"><br></td><td>cell1-3</td></tr>' +
+ '<tr><td>cell2-1</td><td>cell2-2</td><td valign="top"><br></td><td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableColumn(1, false) should insert a column to right of the second column");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second column (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second column (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell in second column (after)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column (after)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableColumn(1, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td valign="top"><br></td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td colspan="3">cell2-1</td><td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableColumn(1, false) should insert a column to left of the second column and colspan in the first column should be increased");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td colspan="3">cell2-1</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableColumn(1, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td id="select">cell1-2</td><td valign="top"><br></td><td>cell1-3</td></tr>' +
+ '<tr><td colspan="4">cell2-1</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableColumn(1, true) should insert a column to right of the second column and colspan in the first column should be increased");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
+ '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().insertTableColumn(2, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableColumn(2, false) should insert 2 columns to left of the first column");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell which is col-spanning (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell which is col-spanning (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell which is col-spanning (before)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is col-spanning (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" +
+ '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().insertTableColumn(2, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td>cell1-2</td><td valign="top"><br></td><td valign="top"><br></td><td>cell1-3</td></tr>' +
+ '<tr><td id="select" colspan="2">cell2-1</td><td valign="top"><br></td><td valign="top"><br></td><td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "nsITableEditor.insertTableColumn(2, false) should insert 2 columns to right of the second column (i.e., right of the right-most column of the column-spanning cell");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell which is col-spanning (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell which is col-spanning (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell which is col-spanning (after)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is col-spanning (after)");
+
+ (function testInsertBeforeFirstColumnFollowingTextNode() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ '<tr> <td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td> </tr>' +
+ "<tr> <td>cell2-1</td><td>cell2-2</td><td>cell2-3</td> </tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableColumn(1, false);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ '<tr> <td valign="top"><br></td><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td> </tr>' +
+ '<tr> <td valign="top"><br></td><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td> </tr>' +
+ "</tbody></table>",
+ "testInsertBeforeFirstColumnFollowingTextNode: nsITableEditor.insertTableColumn(1, false) should insert a column before the first column"
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertBeforeFirstColumnFollowingTextNode: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection is collapsed in the first column (testInsertBeforeFirstColumnFollowingTextNode)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertBeforeFirstColumnFollowingTextNode: Only one "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection is collapsed in the first column (testInsertBeforeFirstColumnFollowingTextNode)"
+ );
+ })();
+
+ (function testInsertAfterLastColumnFollowedByTextNode() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ '<tr> <td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td> </tr>' +
+ "<tr> <td>cell2-1</td><td>cell2-2</td><td>cell2-3</td> </tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableColumn(1, true);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ '<tr> <td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td><td valign="top"><br></td> </tr>' +
+ '<tr> <td>cell2-1</td><td>cell2-2</td><td>cell2-3</td><td valign="top"><br></td> </tr>' +
+ "</tbody></table>",
+ "testInsertAfterLastColumnFollowedByTextNode: nsITableEditor.insertTableColumn(1, true) should insert a column after the last column"
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertAfterLastColumnFollowedByTextNode: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection is collapsed in the last column (testInsertAfterLastColumnFollowedByTextNode)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertAfterLastColumnFollowedByTextNode: One "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection is collapsed in the last column (testInsertAfterLastColumnFollowedByTextNode)"
+ );
+ })();
+
+ (function testInsertBeforeColumnFollowingTextNode() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ '<tr><td>cell1-1</td> <td id="select">cell1-2</td> <td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td> <td>cell2-2</td> <td>cell2-3</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableColumn(1, false);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ '<tr><td>cell1-1</td> <td valign="top"><br></td><td id="select">cell1-2</td> <td>cell1-3</td></tr>' +
+ '<tr><td>cell2-1</td> <td valign="top"><br></td><td>cell2-2</td> <td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "testInsertBeforeColumnFollowingTextNode: nsITableEditor.insertTableColumn(1, false) should insert a column before the first column"
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertBeforeColumnFollowingTextNode: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection is collapsed in the column following a text node (testInsertBeforeColumnFollowingTextNode)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertBeforeColumnFollowingTextNode: Only one "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection is collapsed in the column following a text node (testInsertBeforeColumnFollowingTextNode)"
+ );
+ })();
+
+ (function testInsertAfterColumnFollowedByTextNode() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ '<tr><td>cell1-1</td> <td id="select">cell1-2</td> <td>cell1-3</td></tr>' +
+ "<tr><td>cell2-1</td> <td>cell2-2</td> <td>cell2-3</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableColumn(1, true);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ '<tr><td>cell1-1</td> <td id="select">cell1-2</td><td valign="top"><br></td> <td>cell1-3</td></tr>' +
+ '<tr><td>cell2-1</td> <td>cell2-2</td><td valign="top"><br></td> <td>cell2-3</td></tr>' +
+ "</tbody></table>",
+ "testInsertAfterColumnFollowedByTextNode: nsITableEditor.insertTableColumn(1, true) should insert a column before the first column"
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertAfterColumnFollowedByTextNode: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection is collapsed in the column followed by a text node (testInsertAfterColumnFollowedByTextNode)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertAfterColumnFollowedByTextNode: Only one "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection is collapsed in the column followed by a text node (testInsertAfterColumnFollowedByTextNode)"
+ );
+ })();
+
+ editor.removeEventListener("beforeinput", onBeforeInput);
+ editor.removeEventListener("input", onInput);
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html b/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html
new file mode 100644
index 0000000000..84c2443af8
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html
@@ -0,0 +1,432 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for nsITableEditor.insertTableRow()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let editor = document.getElementById("content");
+ let selection = document.getSelection();
+ let selectionRanges = [];
+
+ function checkInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, false,
+ `"${aEvent.type}" event should be never cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, "",
+ `inputType of "${aEvent.type}" event should be empty string ${aDescription}`);
+ is(aEvent.data, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ is(targetRanges.length, selectionRanges.length,
+ `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`);
+ if (targetRanges.length === selectionRanges.length) {
+ for (let i = 0; i < selectionRanges.length; i++) {
+ is(targetRanges[i].startContainer, selectionRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].startOffset, selectionRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endContainer, selectionRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ is(targetRanges[i].endOffset, selectionRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0,
+ `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`);
+ }
+ }
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ selectionRanges = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset,
+ endContainer: range.endContainer, endOffset: range.endOffset});
+ }
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editor.addEventListener("beforeinput", onBeforeInput);
+ editor.addEventListener("input", onInput);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.collapse(editor.firstChild, 0);
+ getTableEditor().insertTableRow(1, false);
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.insertTableRow(1, false) should do nothing if selection is not in <table>");
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when a call of nsITableEditor.insertTableRow(1, false) even though it will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside of table element (nsITableEditor.insertTableRow(1, false))");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.insertTableRow(1, false) does nothing');
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().insertTableRow(1, true);
+ is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
+ "nsITableEditor.insertTableRow(1, true) should do nothing if selection is not in <table>");
+ is(beforeInputEvents.length, 1,
+ '"beforeinput" event should be fired when a call of nsITableEditor.insertTableRow(1, true) even though it will do nothing');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside of table element (nsITableEditor.insertTableRow(1, true))");
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when a call of nsITableEditor.insertTableRow(1, true) does nothing');
+
+ selection.removeAllRanges();
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().insertTableRow(1, false);
+ ok(false, "getTableEditor().insertTableRow(1, false) without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().insertTableRow(1, false) without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.insertTableRow(1, false) causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.insertTableRow(1, false) causes exception due to no selection range');
+ }
+ try {
+ beforeInputEvents = [];
+ inputEvents = [];
+ getTableEditor().insertTableRow(1, true);
+ ok(false, "getTableEditor().insertTableRow(1, true) without selection ranges should throw exception");
+ } catch (e) {
+ ok(true, "getTableEditor().insertTableRow(1, true) without selection ranges should throw exception");
+ is(beforeInputEvents.length, 0,
+ 'No "beforeinput" event should be fired when nsITableEditor.insertTableRow(1, true) causes exception due to no selection range');
+ is(inputEvents.length, 0,
+ 'No "input" event should be fired when nsITableEditor.insertTableRow(1, true) causes exception due to no selection range');
+ }
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableRow(1, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableRow(1, false) should insert a row above the second row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second row (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second row (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell in second row (before)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableRow(1, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableRow(1, true) should insert a row below the second row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second row (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second row (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell in second row (after)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row (after)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td rowspan="2">cell1-2</td></tr>' +
+ '<tr><td id="select">cell2-1</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableRow(1, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' +
+ '<tr><td valign="top"><br></td></tr>' +
+ '<tr><td id="select">cell2-1</td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableRow(1, false) should insert a row above the second row and rowspan in the first row should be increased");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (before)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' +
+ '<tr><td id="select">cell2-1</td></tr>' +
+ "<tr><td>cell3-1</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableRow(1, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td rowspan="4">cell1-2</td></tr>' +
+ '<tr><td id="select">cell2-1</td></tr>' +
+ '<tr><td valign="top"><br></td></tr>' +
+ "<tr><td>cell3-1</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableRow(1, true) should insert a row below the second row and rowspan in the first row should be increased");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (after)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (after)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().insertTableRow(2, false);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
+ '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableRow(2, false) should insert 2 rows above the first row");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell which is row-spanning (before)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell which is row-spanning (before)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell which is row-spanning (before)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is row-spanning (before)");
+
+ selection.removeAllRanges();
+ editor.innerHTML = "<table>" +
+ '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
+ document.getElementById("select").firstChild, 1);
+ getTableEditor().insertTableRow(2, true);
+ is(editor.innerHTML, "<table><tbody>" +
+ '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' +
+ "<tr><td>cell2-1</td></tr>" +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "nsITableEditor.insertTableRow(2, false) should insert 2 rows below the second row (i.e., below the bottom row of the row-spanning cell");
+ is(beforeInputEvents.length, 1,
+ 'Only one "beforeinput" event should be fired when selection is collapsed in a cell which is row-spanning (after)');
+ checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell which is row-spanning (after)");
+ is(inputEvents.length, 1,
+ 'Only one "input" event should be fired when selection is collapsed in a cell which is row-spanning (after)');
+ checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is row-spanning (after)");
+
+ (function testInsertBeforeRowFollowingTextNode() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>\n' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableRow(1, false);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>\n' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "testInsertBeforeRowFollowingTextNode: nsITableEditor.insertTableRow(1, false) should insert a row above the second row");
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertBeforeRowFollowingTextNode: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection is collapsed in a cell whose row follows a text node (testInsertBeforeRowFollowingTextNode)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertBeforeRowFollowingTextNode: Only one "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection is collapsed in a cell whose row follows a text node (testInsertBeforeRowFollowingTextNode)"
+ );
+ })();
+
+ (function testInsertAfterRowFollowedTextNode() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>\n' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.setBaseAndExtent(
+ document.getElementById("select").firstChild,
+ 0,
+ document.getElementById("select").firstChild,
+ 0
+ );
+ getTableEditor().insertTableRow(1, true);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>\n' +
+ "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+ "</tbody></table>",
+ "testInsertAfterRowFollowedTextNode: nsITableEditor.insertTableRow(1, true) should insert a row above the second row");
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertAfterRowFollowedTextNode: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection is collapsed in a cell whose row follows a text node (testInsertAfterRowFollowedTextNode)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertAfterRowFollowedTextNode: Only one "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection is collapsed in a cell whose row follows a text node (testInsertAfterRowFollowedTextNode)"
+ );
+ })();
+
+ (function testInsertAfterLastRow() {
+ selection.removeAllRanges();
+ editor.innerHTML =
+ "<table>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ "</table>";
+ editor.focus();
+ beforeInputEvents = [];
+ inputEvents = [];
+ selection.collapse(document.getElementById("select").firstChild, 0);
+ getTableEditor().insertTableRow(1, true);
+ is(
+ editor.innerHTML,
+ "<table><tbody>" +
+ "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+ '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' +
+ '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' +
+ "</tbody></table>",
+ "testInsertAfterLastRow: nsITableEditor.insertTableRow(1, true) should insert a row after the last row"
+ );
+ is(
+ beforeInputEvents.length,
+ 1,
+ 'testInsertAfterLastRow: Only one "beforeinput" event should be fired'
+ );
+ checkInputEvent(
+ beforeInputEvents[0],
+ "when selection is collapsed in a cell whose row follows a text node (testInsertAfterLastRow)"
+ );
+ is(
+ inputEvents.length,
+ 1,
+ 'testInsertAfterLastRow: Only one "input" event should be fired'
+ );
+ checkInputEvent(
+ inputEvents[0],
+ "when selection is collapsed in a cell whose row follows a text node (testInsertAfterLastRow)"
+ );
+ })();
+
+ editor.removeEventListener("beforeinput", onBeforeInput);
+ editor.removeEventListener("input", onInput);
+
+ SimpleTest.finish();
+});
+
+function getTableEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_password_input_with_unmasked_range.html b/editor/libeditor/tests/test_password_input_with_unmasked_range.html
new file mode 100644
index 0000000000..205884219b
--- /dev/null
+++ b/editor/libeditor/tests/test_password_input_with_unmasked_range.html
@@ -0,0 +1,417 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for inputting new text while there is unmasked range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<input type="password" value="012345">
+
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ let input = document.getElementsByTagName("input")[0];
+ let editor = SpecialPowers.wrap(input).editor;
+
+ async function doTest(aDelay) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["editor.password.mask_delay", aDelay]],
+ });
+ input.focus();
+ input.value = "012345";
+
+ // Setting value
+ editor.unmask(3, 4);
+ input.value = "abcdef";
+ is(editor.unmaskedStart, 0,
+ "delay=" + aDelay + ": Setting value should mask all characters (start)");
+ is(editor.unmaskedEnd, 6,
+ "delay=" + aDelay + ": Setting value should mask all characters (end)");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after setting value");
+ editor.unmask(3, 4);
+ input.value = "abcdef";
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Setting same value shouldn't change unmasked range (start)");
+ is(editor.unmaskedEnd, 4,
+ "delay=" + aDelay + ": Setting same value shouldn't change unmasked range (end)");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after setting same value");
+ input.value = "";
+ is(editor.unmaskedStart, 0,
+ "delay=" + aDelay + ": Setting empty value should collapse unmasked range (start)");
+ is(editor.unmaskedEnd, 0,
+ "delay=" + aDelay + ": Setting empty value should collapse unmasked range (end)");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after setting empty value");
+ input.value = "abcdef";
+ is(editor.unmaskedStart, 0,
+ "delay=" + aDelay + ": Setting empty field to new value should unmask all (start)");
+ is(editor.unmaskedEnd, 6,
+ "delay=" + aDelay + ": Setting empty field to new value should unmask all (end)");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after setting empty field to new value");
+
+ // Simply typing a character
+ editor.unmask(3, 4);
+ input.setSelectionRange(3, 3);
+ synthesizeKey("1");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Inserting character at start of unmasked range should expand the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Inserting character at start of unmasked range should expand the range (end)");
+ is(input.value, "abc1def",
+ "delay=" + aDelay + ": Inserting character at start of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character at start of unmasked range");
+
+ editor.unmask(3, 5);
+ synthesizeKey("2");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Inserting character in the unmasked range should expand the range (start)");
+ is(editor.unmaskedEnd, 6,
+ "delay=" + aDelay + ": Inserting character in the unmasked range should expand the range (end)");
+ is(input.value, "abc12def",
+ "delay=" + aDelay + ": Inserting character in the unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character in the unmasked range");
+
+ editor.unmask(3, 6);
+ input.setSelectionRange(6, 6);
+ synthesizeKey("3");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Inserting character at end of unmasked range should expand the range (start)");
+ is(editor.unmaskedEnd, 7,
+ "delay=" + aDelay + ": Inserting character at end of unmasked range should expand the range (end)");
+ is(input.value, "abc12d3ef",
+ "delay=" + aDelay + ": Inserting character at end of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character at end of unmasked range");
+
+ editor.unmask(3, 4);
+ input.setSelectionRange(2, 2);
+ synthesizeKey("4");
+ is(editor.unmaskedStart, 2,
+ "delay=" + aDelay + ": Inserting character before the unmasked range should expand the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Inserting character before the unmasked range should expand the range (end)");
+ is(input.value, "ab4c12d3ef",
+ "delay=" + aDelay + ": Inserting character before the unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character before the unmasked range");
+
+ editor.unmask(2, 5);
+ input.setSelectionRange(6, 6);
+ synthesizeKey("5");
+ is(editor.unmaskedStart, 2,
+ "delay=" + aDelay + ": Inserting character after the unmasked range should expand the range (start)");
+ is(editor.unmaskedEnd, 7,
+ "delay=" + aDelay + ": Inserting character after the unmasked range should expand the range (end)");
+ is(input.value, "ab4c125d3ef",
+ "delay=" + aDelay + ": Inserting character after the unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character after the unmasked range");
+
+ // Simply removing characters
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(3, 3);
+ editor.deleteSelection(editor.eNext, editor.eStrip);
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Removing first character of unmasked range should shrink the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Removing first character of unmasked range should shrink the range (end)");
+ is(input.value, "abcefgh",
+ "delay=" + aDelay + ": Removing first character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing first character of unmasked range");
+
+ editor.unmask(3, 5);
+ editor.deleteSelection(editor.ePrevious, editor.eStrip);
+ is(editor.unmaskedStart, 2,
+ "delay=" + aDelay + ": Removing previous character of unmasked range should move the range (start)");
+ is(editor.unmaskedEnd, 4,
+ "delay=" + aDelay + ": Removing previous character of unmasked range should move the range (end)");
+ is(input.value, "abefgh",
+ "delay=" + aDelay + ": Removing previous character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing previous character of unmasked range");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(6, 6);
+ editor.deleteSelection(editor.ePrevious, editor.eStrip);
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Removing last character of unmasked range should shrink the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Removing last character of unmasked range should shrink the range (end)");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing last character of unmasked range should shrink the range");
+
+ editor.unmask(3, 5);
+ editor.deleteSelection(editor.eNext, editor.eStrip);
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Removing next character of unmasked range shouldn't change the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Removing next character of unmasked range shouldn't change the range (end)");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing next character of unmasked range shouldn't change the range");
+
+ input.value = "abcdef";
+ editor.unmask(3, 6);
+ input.setSelectionRange(4, 4);
+ editor.deleteSelection(editor.eNext, editor.eStrip);
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Removing middle character of unmasked range should shrink the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Removing middle character of unmasked range should shrink the range (end)");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing middle character of unmasked range should shrink the range");
+
+ // Removing selection
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(3, 4);
+ editor.deleteSelection(editor.eNone, editor.eStrip);
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Removing selected first character of unmasked range should shrink the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Removing selected first character of unmasked range should shrink the range (end)");
+ is(input.value, "abcefgh",
+ "delay=" + aDelay + ": Removing selected first character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected first character of unmasked range");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(2, 3);
+ editor.deleteSelection(editor.eNone, editor.eStrip);
+ is(editor.unmaskedStart, 2,
+ "delay=" + aDelay + ": Removing selected previous character of unmasked range should move the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Removing selected previous character of unmasked range should move the range (end)");
+ is(input.value, "abdefgh",
+ "delay=" + aDelay + ": Removing selected previous character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected previous character of unmasked range");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(5, 6);
+ editor.deleteSelection(editor.eNone, editor.eStrip);
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Removing selected last character of unmasked range should shrink the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Removing selected last character of unmasked range should shrink the range (end)");
+ is(input.value, "abcdegh",
+ "delay=" + aDelay + ": Removing selected last character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected last character of unmasked range");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(6, 7);
+ editor.deleteSelection(editor.eNone, editor.eStrip);
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Removing selected next character of unmasked range should keep the range (start)");
+ is(editor.unmaskedEnd, 6,
+ "delay=" + aDelay + ": Removing selected next character of unmasked range should keep the range (end)");
+ is(input.value, "abcdefh",
+ "delay=" + aDelay + ": Removing selected next character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected next character of unmasked range");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(4, 5);
+ editor.deleteSelection(editor.eNone, editor.eStrip);
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Removing selected middle character of unmasked range should shrink the range (start)");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Removing selected middle character of unmasked range should shrink the range (end)");
+ is(input.value, "abcdfgh",
+ "delay=" + aDelay + ": Removing selected middle character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected middle character of unmasked range");
+
+ // Replacing a character
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(3, 4);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Replacing first character of unmasked range should keep the range (start)");
+ is(editor.unmaskedEnd, 6,
+ "delay=" + aDelay + ": Replacing first character of unmasked range should keep the range (end)");
+ is(input.value, "abc0efgh",
+ "delay=" + aDelay + ": Replacing first character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing first character of unmasked range");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(2, 3);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 2,
+ "delay=" + aDelay + ": Replacing previous character of unmasked range should expand the range (start)");
+ is(editor.unmaskedEnd, 6,
+ "delay=" + aDelay + ": Replacing previous character of unmasked range should expand the range (end)");
+ is(input.value, "ab0defgh",
+ "delay=" + aDelay + ": Replacing previous character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing previous character of unmasked range");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(5, 6);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Replacing last character of unmasked range should keep the range (start)");
+ is(editor.unmaskedEnd, 6,
+ "delay=" + aDelay + ": Replacing last character of unmasked range should keep the range (end)");
+ is(input.value, "abcde0gh",
+ "delay=" + aDelay + ": Replacing last character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing last character of unmasked range");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(6, 7);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Replacing next character of unmasked range should expand the range (start)");
+ is(editor.unmaskedEnd, 7,
+ "delay=" + aDelay + ": Replacing next character of unmasked range should expand the range (end)");
+ is(input.value, "abcdef0h",
+ "delay=" + aDelay + ": Replacing next character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing next character of unmasked range");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(4, 5);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Replacing middle character of unmasked range should keep the range (start)");
+ is(editor.unmaskedEnd, 6,
+ "delay=" + aDelay + ": Replacing middle character of unmasked range should keep the range (end)");
+ is(input.value, "abcd0fgh",
+ "delay=" + aDelay + ": Replacing middle character of unmasked range");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing middle character of unmasked range");
+
+ // Replace part of the range
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(1, 4);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 1,
+ "delay=" + aDelay + ": Replacing start edge of unmasked range should move and shrink the range (start) #1");
+ is(editor.unmaskedEnd, 4,
+ "delay=" + aDelay + ": Replacing start edge of unmasked range should move and shrink the range (end) #1");
+ is(input.value, "a0efgh",
+ "delay=" + aDelay + ": Replacing start edge of unmasked range #1");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing start edge of unmasked range #1");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(2, 5);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 2,
+ "delay=" + aDelay + ": Replacing start edge of unmasked range should move and shrink the range (start) #2");
+ is(editor.unmaskedEnd, 4,
+ "delay=" + aDelay + ": Replacing start edge of unmasked range should move and shrink the range (end) #2");
+ is(input.value, "ab0fgh",
+ "delay=" + aDelay + ": Replacing start edge of unmasked range #2");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing start edge of unmasked range #2");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(4, 7);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Replacing end edge of unmasked range should shrink the range (start) #1");
+ is(editor.unmaskedEnd, 5,
+ "delay=" + aDelay + ": Replacing end edge of unmasked range should shrink the range (end) #1");
+ is(input.value, "abcd0h",
+ "delay=" + aDelay + ": Replacing end edge of unmasked range #1");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing end edge of unmasked range #1");
+
+ input.value = "abcdefghi";
+ editor.unmask(3, 6);
+ input.setSelectionRange(5, 8);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Replacing end edge of unmasked range should shrink the range (start) #2");
+ is(editor.unmaskedEnd, 6,
+ "delay=" + aDelay + ": Replacing end edge of unmasked range should shrink the range (end) #2");
+ is(input.value, "abcde0i",
+ "delay=" + aDelay + ": Replacing end edge of unmasked range #2");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing end edge of unmasked range #2");
+
+ // Replaceing all of the range
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(3, 6);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 3,
+ "delay=" + aDelay + ": Replacing all of unmasked range should shrink the range (start) #1");
+ is(editor.unmaskedEnd, 4,
+ "delay=" + aDelay + ": Replacing all of unmasked range should shrink the range (end) #1");
+ is(input.value, "abc0gh",
+ "delay=" + aDelay + ": Replacing all of unmasked range #1");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing all of unmasked range #1");
+
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(2, 7);
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 2,
+ "delay=" + aDelay + ": Replacing all of unmasked range should shrink the range (start) #2");
+ is(editor.unmaskedEnd, 3,
+ "delay=" + aDelay + ": Replacing all of unmasked range should shrink the range (end) #2");
+ is(input.value, "ab0h",
+ "delay=" + aDelay + ": Replacing all of unmasked range #2");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing all of unmasked range #2");
+
+ // Removing all characters and type new character
+ input.value = "abcdefgh";
+ editor.unmask(3, 6);
+ input.setSelectionRange(0, 8);
+ editor.deleteSelection(editor.eNone, editor.eStrip);
+ is(editor.unmaskedStart, 0,
+ "delay=" + aDelay + ": Removing all characters should shrink the range (start)");
+ is(editor.unmaskedEnd, 0,
+ "delay=" + aDelay + ": Removing all characters should shrink the range (end)");
+ is(input.value, "",
+ "delay=" + aDelay + ": Removing all characters");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after removing all characters");
+ synthesizeKey("0");
+ is(editor.unmaskedStart, 0,
+ "delay=" + aDelay + ": Typing first character should expand unmasked range (start)");
+ is(editor.unmaskedEnd, 1,
+ "delay=" + aDelay + ": Typing first character should expand unmasked range (end)");
+ is(input.value, "0",
+ "delay=" + aDelay + ": Typing first character");
+ ok(!editor.autoMaskingEnabled,
+ "delay=" + aDelay + ": auto masking shouldn't be enabled after typing first character");
+ }
+
+ await doTest(0);
+ await doTest(600);
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_password_paste.html b/editor/libeditor/tests/test_password_paste.html
new file mode 100644
index 0000000000..817de9b277
--- /dev/null
+++ b/editor/libeditor/tests/test_password_paste.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for masking password</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<input type="password" id="input1" value="abcdef">
+
+<pre id="test">
+<script class="testbody" type="application/javascript">
+function getEditor() {
+ return SpecialPowers.wrap(document.getElementById("input1")).editor;
+}
+
+function getLoadContext() {
+ return SpecialPowers.wrap(window).docShell.QueryInterface(
+ SpecialPowers.Ci.nsILoadContext);
+}
+
+function pasteText(str) {
+ const Cc = SpecialPowers.Cc;
+ const Ci = SpecialPowers.Ci;
+ let trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ let s = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ s.data = str;
+ trans.setTransferData("text/plain", s);
+ let inputEvent = null;
+ window.addEventListener("input", aEvent => { inputEvent = aEvent; }, {once: true});
+ getEditor().pasteTransferable(trans);
+ is(inputEvent.type, "input", "input event should be fired");
+ is(inputEvent.inputType, "insertFromPaste", "inputType should be insertFromPaste");
+ is(inputEvent.data, str, `data should be "${str}"`);
+ is(inputEvent.dataTransfer, null, "dataTransfer should be null on password field");
+}
+
+SimpleTest.waitForFocus(async () => {
+ let input1 = document.getElementById("input1");
+ input1.focus();
+ let reference = snapshotWindow(window, false);
+
+ // Bug 1501376 - Password should be masked immediately when pasting text
+ input1.value = "";
+ pasteText("abcdef");
+ assertSnapshots(reference, snapshotWindow(window), true, null,
+ "Password should be masked immediately when pasting text",
+ "reference is masked");
+ SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_password_per_word_operation.html b/editor/libeditor/tests/test_password_per_word_operation.html
new file mode 100644
index 0000000000..847f2e9854
--- /dev/null
+++ b/editor/libeditor/tests/test_password_per_word_operation.html
@@ -0,0 +1,148 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for operations in a password field</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<input type="password" value="abcdef ghijk" size="50">
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.word_select.eat_space_to_next_word", false]],
+ });
+ // Double click on the anonymous text node
+ let input = document.getElementsByTagName("input")[0];
+ let editor = SpecialPowers.wrap(input).editor;
+ let anonymousDiv = editor.rootElement;
+ input.select();
+ const kTextNodeRectInAnonymousDiv = {
+ left: editor.selection.getRangeAt(0).getBoundingClientRect().left - anonymousDiv.getBoundingClientRect().left,
+ top: editor.selection.getRangeAt(0).getBoundingClientRect().top - anonymousDiv.getBoundingClientRect().top,
+ width: editor.selection.getRangeAt(0).getBoundingClientRect().width,
+ height: editor.selection.getRangeAt(0).getBoundingClientRect().height,
+ };
+ kTextNodeRectInAnonymousDiv.right = kTextNodeRectInAnonymousDiv.left + kTextNodeRectInAnonymousDiv.width;
+ kTextNodeRectInAnonymousDiv.bottom = kTextNodeRectInAnonymousDiv.top + kTextNodeRectInAnonymousDiv.height;
+ input.setSelectionRange(0, 0);
+ const kHalfHeightOfAnonymousDiv = anonymousDiv.getBoundingClientRect().height / 2;
+ synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.left + 5, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 1});
+ synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.left + 5, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 2});
+ is(input.selectionStart, 0,
+ "Double clicking on the anonymous text node in a password field should select all");
+ is(input.selectionEnd, input.value.length,
+ "Double clicking on the anonymous text node in a password field should select all");
+
+ // Double click on the anonymous div element
+ synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.right + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 1});
+ synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.right + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 2});
+ is(input.selectionStart, 0,
+ "Double clicking on the anonymous div element in a password field should select all");
+ is(input.selectionEnd, input.value.length,
+ "Double clicking on the anonymous div element in a password field should select all");
+
+ // Move caret per word
+ let selectionController = editor.selectionController;
+ input.focus();
+ input.setSelectionRange(12, 12);
+ selectionController.wordMove(false, false);
+ is(input.selectionStart, 0,
+ "Moving caret one word from the end should move caret to the start");
+ input.setSelectionRange(0, 0);
+ selectionController.wordMove(true, false);
+ is(input.selectionStart, 12,
+ "Moving caret one word from the start should move caret to the end");
+
+ // Expand selection per word
+ input.setSelectionRange(12, 12);
+ selectionController.wordMove(false, true);
+ is(input.selectionStart, 0,
+ "Selecting one word from the end should move selection start to the start");
+ input.setSelectionRange(0, 0);
+ selectionController.wordMove(true, true);
+ is(input.selectionEnd, 12,
+ "Selecting one word from the start should move selection end to the end");
+
+ // Delete one word
+ input.setSelectionRange(12, 12);
+ editor.deleteSelection(editor.ePreviousWord, editor.eStrip);
+ is(input.value, "",
+ "Deleting one word from the end should delete all characters");
+ input.value = "abcdef ghijk";
+ document.documentElement.scrollTop; // Flush frames for setting the value.
+ input.setSelectionRange(0, 0);
+ editor.deleteSelection(editor.eNextWord, editor.eStrip);
+ is(input.value, "",
+ "Deleting one word from the start should delete all characters");
+ input.value = "abcdef ghijk";
+ document.documentElement.scrollTop; // Flush frames for setting the value.
+
+ // Test same things when the space is unmasked.
+
+ // Double click on the anonymous text node
+ editor.unmask(6, 7);
+ synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.left + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 1});
+ synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.left + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 2});
+ is(input.selectionStart, 0,
+ "Double clicking on the first word should select it");
+ is(input.selectionEnd, 6,
+ "Double clicking on the first word should select it");
+
+ // Double click on the anonymous div element
+ editor.unmask(6, 7);
+ synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.right + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 1});
+ synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.right + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 2});
+ is(input.selectionStart, 7,
+ "Double clicking on the anonymous div element in a password field should select the last word");
+ is(input.selectionEnd, input.value.length,
+ "Double clicking on the anonymous div element in a password field should select the last word");
+
+ // Move caret per word
+ input.focus();
+ input.setSelectionRange(12, 12);
+ editor.unmask(6, 7);
+ selectionController.wordMove(false, false);
+ is(input.selectionStart, 6,
+ "Moving caret one word from the end should move caret to end of the first word");
+ input.setSelectionRange(0, 0);
+ editor.unmask(6, 7);
+ selectionController.wordMove(true, false);
+ is(input.selectionStart, 7,
+ "Moving caret one word from the start should move caret to start of the last word");
+
+ // Expand selection per word
+ input.setSelectionRange(12, 12);
+ editor.unmask(6, 7);
+ selectionController.wordMove(false, true);
+ is(input.selectionStart, 6,
+ "Selecting one word from the end should move selection start to end of the first word");
+ input.setSelectionRange(0, 0);
+ editor.unmask(6, 7);
+ selectionController.wordMove(true, true);
+ is(input.selectionEnd, 7,
+ "Selecting one word from the start should move selection end to start of the last word");
+
+ // Delete one word
+ input.setSelectionRange(12, 12);
+ editor.unmask(6, 7);
+ editor.deleteSelection(editor.ePreviousWord, editor.eStrip);
+ is(input.value, "abcdef",
+ "Deleting one word from the end should delete the last word");
+ input.value = "abcdef ghijk";
+ document.documentElement.scrollTop; // Flush frames for setting the value.
+ input.setSelectionRange(0, 0);
+ editor.unmask(6, 7);
+ editor.deleteSelection(editor.eNextWord, editor.eStrip);
+ is(input.value, "ghijk",
+ "Deleting one word from the start should delete the first word");
+ input.value = "abcdef ghijk";
+ document.documentElement.scrollTop; // Flush frames for setting the value.
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_password_unmask_API.html b/editor/libeditor/tests/test_password_unmask_API.html
new file mode 100644
index 0000000000..0fc7ef8194
--- /dev/null
+++ b/editor/libeditor/tests/test_password_unmask_API.html
@@ -0,0 +1,318 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for unmasking password API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<input type="text">
+<input type="password">
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let input = document.getElementsByTagName("input")[0];
+ let password = document.getElementsByTagName("input")[1];
+
+ let editor, passwordEditor;
+ function updateEditors() {
+ editor = SpecialPowers.wrap(input).editor;
+ passwordEditor = SpecialPowers.wrap(password).editor;
+ }
+
+ try {
+ updateEditors();
+ editor.mask();
+ ok(false,
+ `nsIEditor.mask() should throw exception when called for <input type="text"> before nsIEditor.unmask()`);
+ } catch (e) {
+ ok(true,
+ `nsIEditor.mask() should throw exception when called for <input type="text"> before nsIEditor.unmask() ${e}`);
+ }
+
+ try {
+ updateEditors();
+ editor.unmask();
+ ok(false,
+ `nsIEditor.unmask() should throw exception when called for <input type="text">`);
+ } catch (e) {
+ ok(true,
+ `nsIEditor.unmask() should throw exception when called for <input type="text"> ${e}`);
+ }
+
+ try {
+ updateEditors();
+ editor.unmask(0);
+ ok(false,
+ `nsIEditor.unmask(0) should throw exception when called for <input type="text">`);
+ } catch (e) {
+ ok(true,
+ `nsIEditor.unmask(0) should throw exception when called for <input type="text"> ${e}`);
+ }
+
+ input.value = "abcdef";
+ try {
+ updateEditors();
+ editor.unmask();
+ ok(false,
+ `nsIEditor.unmask() should throw exception when called for <input type="text" value="abcdef">`);
+ } catch (e) {
+ ok(true,
+ `nsIEditor.unmask() should throw exception when called for <input type="text" value="abcdef"> ${e}`);
+ }
+
+ try {
+ updateEditors();
+ editor.mask();
+ ok(false,
+ `nsIEditor.mask() should throw exception when called for <input type="text" value="abcdef"> after nsIEditor.unmask()`);
+ } catch (e) {
+ ok(true,
+ `nsIEditor.mask() should throw exception when called for <input type="text" value="abcdef"> after nsIEditor.unmask() ${e}`);
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.mask();
+ ok(true,
+ `nsIEditor.mask() shouldn't throw exception when called for <input type="password"> before nsIEditor.unmask()`);
+ ok(passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be true after nsIEditor.mask() for <input type="password"> before nsIEditor.unmask()`);
+ } catch (e) {
+ ok(false,
+ `nsIEditor.mask() shouldn't throw exception when called for <input type="password"> before nsIEditor.unmask() ${e}`);
+ }
+
+ try {
+ updateEditors();
+ editor.unmask(5);
+ ok(false,
+ `nsIEditor.unmask(5) should throw exception when called for <input type="password" value="">`);
+ } catch (e) {
+ ok(true,
+ `nsIEditor.unmask(5) should throw exception when called for <input type="password" value=""> ${e}`);
+ ok(passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should keep true (<input type="password">)`);
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.unmask();
+ ok(true,
+ `nsIEditor.unmask() shouldn't throw exception when called for <input type="password">`);
+ ok(!passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be false after nsIEditor.unmask() for <input type="password">)`);
+ is(passwordEditor.unmaskedStart, 0,
+ `nsIEditor.unmaskedStart should be 0 after nsIEditor.unmask() for <input type="password">`);
+ is(passwordEditor.unmaskedEnd, 0,
+ `nsIEditor.unmaskedEnd should be 0 after nsIEditor.unmask() for <input type="password">`);
+ } catch (e) {
+ ok(false,
+ `nsIEditor.unmask() shouldn't throw exception when called for <input type="password"> ${e}`);
+ }
+
+ password.value = "abcdef";
+ try {
+ updateEditors();
+ passwordEditor.unmask();
+ ok(true,
+ `nsIEditor.unmask() shouldn't throw exception when called for <input type="password" value="abcdef">)`);
+ ok(!passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be false after nsIEditor.unmask() for <input type="password" value="abcdef">`);
+ is(passwordEditor.unmaskedStart, 0,
+ `nsIEditor.unmaskedStart should be 0 after nsIEditor.unmask() for <input type="password" value="abcdef">`);
+ is(passwordEditor.unmaskedEnd, 6,
+ `nsIEditor.unmaskedEnd should be 0 after nsIEditor.unmask() for <input type="password" value="abcdef">`);
+ } catch (e) {
+ ok(false,
+ `nsIEditor.unmask() shouldn't throw exception when called for <input type="password" value="abcdef"> ${e}`);
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.mask();
+ ok(true,
+ `nsIEditor.mask() shouldn't throw exception when called for <input type="password" value="abcdef">`);
+ ok(passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be true after nsIEditor.mask() for <input type="password" value="abcdef">`);
+ } catch (e) {
+ ok(false,
+ `nsIEditor.mask() shouldn't throw exception when called for <input type="password" value="abcdef"> ${e}`);
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.unmask(0, 100, 1000);
+ ok(true,
+ `nsIEditor.unmask(0, 100, 1000) shouldn't throw exception when called for <input type="password" value="abcdef">`);
+ ok(passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be true after nsIEditor.unmask(0, 100, 1000) for <input type="password" value="abcdef">`);
+ is(passwordEditor.unmaskedStart, 0,
+ `nsIEditor.unmaskedStart should be 0 after nsIEditor.unmask(0, 100, 1000) for <input type="password" value="abcdef">`);
+ is(passwordEditor.unmaskedEnd, 6,
+ `nsIEditor.unmaskedEnd should be 6 after nsIEditor.unmask(0, 100, 1000) for <input type="password" value="abcdef">`);
+ } catch (e) {
+ ok(false,
+ `nsIEditor.unmask(0, 100, 1000) shouldn't throw exception when called for <input type="password" value="abcdef"> ${e}`);
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.unmask(3);
+ ok(true,
+ `nsIEditor.unmask(3) shouldn't throw exception when called for <input type="password" value="abcdef">`);
+ ok(!passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be false after nsIEditor.unmask(3) for <input type="password" value="abcdef">`);
+ is(passwordEditor.unmaskedStart, 3,
+ `nsIEditor.unmaskedStart should be 3 after nsIEditor.unmask(3) for <input type="password" value="abcdef">`);
+ is(passwordEditor.unmaskedEnd, 6,
+ `nsIEditor.unmaskedEnd should be 6 after nsIEditor.unmask(3) for <input type="password" value="abcdef">`);
+ } catch (e) {
+ ok(false,
+ `nsIEditor.unmask(3) shouldn't throw exception when called for <input type="password" value="abcdef"> ${e}`);
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.unmask(0);
+ password.style.fontSize = "32px"; // reframe the `<input>` element
+ password.getBoundingClientRect(); // flush pending reflow if there is
+ // Then, new `TextEditor` should keep unmasked range.
+ passwordEditor = SpecialPowers.wrap(password).editor;
+ ok(!passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be false after the password field reframed`);
+ is(passwordEditor.unmaskedStart, 0,
+ `nsIEditor.unmaskedStart should be 0 after the password field reframed`);
+ is(passwordEditor.unmaskedEnd, 6,
+ `nsIEditor.unmaskedEnd should be 6 after the password field reframed`);
+ } catch (e) {
+ ok(false, `Shouldn't throw exception while testing unmasked range after reframing ${e}`);
+ } finally {
+ password.style.fontSize = "";
+ password.getBoundingClientRect();
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.unmask(0);
+ password.style.display = "none"; // Hide the password field temporarily
+ password.getBoundingClientRect();
+ password.style.display = "block"; // And show it again
+ password.getBoundingClientRect();
+ updateEditors();
+ // Then, new `TextEditor` should keep unmasked range.
+ ok(!passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be false after the password field was temporarily hidden`);
+ is(passwordEditor.unmaskedStart, 0,
+ `nsIEditor.unmaskedStart should be 0 after the password field was temporarily hidden`);
+ is(passwordEditor.unmaskedEnd, 6,
+ `nsIEditor.unmaskedEnd should be 6 after the password field was temporarily hidden`);
+ } catch (e) {
+ ok(false, `Shouldn't throw exception while testing unmasked range after temporarily hiding the password field ${e}`);
+ } finally {
+ password.style.display = "";
+ password.getBoundingClientRect();
+ passwordEditor = SpecialPowers.wrap(password).editor;
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.unmask(0);
+ password.style.display = "none"; // Hide the password field temporarily
+ password.getBoundingClientRect();
+ password.value = "ghijkl"; // And modify the value
+ password.style.display = "block"; // And show it again
+ password.getBoundingClientRect();
+ // Then, new `TextEditor` shouldn't keep unmasked range due to the value change.
+ updateEditors();
+ ok(passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be true after the password field was temporarily hidden and changed its value`);
+ is(passwordEditor.unmaskedStart, 0,
+ `nsIEditor.unmaskedStart should be 0 after the password field was temporarily hidden and changed its value`);
+ is(passwordEditor.unmaskedEnd, 0,
+ `nsIEditor.unmaskedEnd should be 0 after the password field was temporarily hidden and changed its value`);
+ } catch (e) {
+ ok(false, `Shouldn't throw exception while testing unmasked range after temporarily hiding the password field and changing the value ${e}`);
+ } finally {
+ password.style.display = "";
+ password.getBoundingClientRect();
+ password.value = "abcdef";
+ passwordEditor = SpecialPowers.wrap(password).editor;
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.unmask(0);
+ password.style.display = "none"; // Hide the password field temporarily
+ password.getBoundingClientRect();
+ password.value = "abcdef"; // And overwrite the value with same value
+ password.style.display = "block"; // And show it again
+ password.getBoundingClientRect();
+ // Then, new `TextEditor` shouldn't keep unmasked range due to setting the value.
+ updateEditors();
+ ok(passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be true after the password field was temporarily hidden and changed its value`);
+ is(passwordEditor.unmaskedStart, 0,
+ `nsIEditor.unmaskedStart should be 0 after the password field was temporarily hidden and changed its value`);
+ is(passwordEditor.unmaskedEnd, 0,
+ `nsIEditor.unmaskedEnd should be 0 after the password field was temporarily hidden and changed its value`);
+ } catch (e) {
+ ok(false, `Shouldn't throw exception while testing unmasked range after temporarily hiding the password field and changing the value ${e}`);
+ } finally {
+ password.style.display = "";
+ password.getBoundingClientRect();
+ password.value = "abcdef";
+ passwordEditor = SpecialPowers.wrap(password).editor;
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.unmask(0, 6, 10000);
+ password.style.display = "none"; // Hide the password field temporarily
+ password.getBoundingClientRect();
+ password.style.display = "block"; // And show it again
+ password.getBoundingClientRect();
+ updateEditors();
+ // Then, new `TextEditor` should mask all characters since nobody can mask it with the timer.
+ ok(passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be true after the password field was temporarily hidden (if auto-masking timer was set)`);
+ is(passwordEditor.unmaskedStart, 0,
+ `nsIEditor.unmaskedStart should be 0 after the password field was temporarily hidden (if auto-masking timer was set)`);
+ is(passwordEditor.unmaskedEnd, 0,
+ `nsIEditor.unmaskedEnd should be 0 after the password field was temporarily hidden (if auto-masking timer was set)`);
+ } catch (e) {
+ ok(false, `Shouldn't throw exception while testing unmasked range after temporarily hiding the password field whose auto-masking timer was set ${e}`);
+ } finally {
+ password.style.display = "";
+ password.getBoundingClientRect();
+ passwordEditor = SpecialPowers.wrap(password).editor;
+ }
+
+ try {
+ updateEditors();
+ passwordEditor.unmask(0);
+ password.type = "text";
+ password.getBoundingClientRect();
+ password.type = "password";
+ password.getBoundingClientRect();
+ updateEditors();
+ // Then, new `TextEditor` should mask all characters after `type` attribute was changed.
+ ok(passwordEditor.autoMaskingEnabled,
+ `nsIEditor.autoMaskingEnabled should be true after "type" attribute of the password field was changed`);
+ is(passwordEditor.unmaskedStart, 0,
+ `nsIEditor.unmaskedStart should be 0 after "type" attribute of the password field was changed`);
+ is(passwordEditor.unmaskedEnd, 0,
+ `nsIEditor.unmaskedEnd should be 0 after "type" attribute of the password field was changed`);
+ } catch (e) {
+ ok(false, `Shouldn't throw exception while testing unmasked range after "type" attribute of the password field was changed ${e}`);
+ } finally {
+ password.type = "password";
+ password.getBoundingClientRect();
+ passwordEditor = SpecialPowers.wrap(password).editor;
+ }
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_pasteImgFromTransferable.html b/editor/libeditor/tests/test_pasteImgFromTransferable.html
new file mode 100644
index 0000000000..a45bb5b574
--- /dev/null
+++ b/editor/libeditor/tests/test_pasteImgFromTransferable.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<div id="edit" contenteditable></div>
+
+<script>
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+
+function getHTMLEditor(aWindow) {
+ let editingSession = SpecialPowers.wrap(aWindow).docShell.editingSession;
+ if (!editingSession) {
+ return null;
+ }
+ let editor = editingSession.getEditorForWindow(aWindow);
+ if (!editor) {
+ return null;
+ }
+ return editor.QueryInterface(Ci.nsIHTMLEditor);
+}
+
+const TESTS = [
+ {
+ mimeType: "image/gif",
+ base64: "R0lGODdhAQACAPABAAD/AP///ywAAAAAAQACAAACAkQKADs="
+ },
+ {
+ mimeType: "image/jpeg",
+ base64: "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wgALCAABAAEBAREA/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxA="
+ },
+ {
+ mimeType: "image/png",
+ base64: "iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAIAAABvrngfAAAAFklEQVQImWMwjWhCQwxECoW3oCHihAB0LyYv5/oAHwAAAABJRU5ErkJggg=="
+ },
+];
+
+add_task(async function() {
+ await new Promise(resolve => SimpleTest.waitForFocus(resolve, window));
+
+ let edit = document.getElementById("edit");
+ edit.focus();
+
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+
+ for (const test of TESTS) {
+ let bin = window.atob(test.base64);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stringStream.setData(bin, bin.length);
+
+ let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.setTransferData(test.mimeType, stringStream);
+
+ let evt = new Promise(resolve =>
+ edit.addEventListener("input", resolve, {once: true}));
+
+ getHTMLEditor(window).pasteTransferable(trans);
+
+ await evt;
+
+ is(edit.innerHTML,
+ "<img src=\"data:" + test.mimeType + ";base64," + test.base64 + "\" alt=\"\">",
+ "pastedTransferable pastes image as data URL");
+ edit.innerHTML = "";
+ }
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_pasteImgTextarea.html b/editor/libeditor/tests/test_pasteImgTextarea.html
new file mode 100644
index 0000000000..c19417e4c0
--- /dev/null
+++ b/editor/libeditor/tests/test_pasteImgTextarea.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<img id="i" src="green.png">
+<textarea id="t"></textarea>
+
+<script>
+let loaded = new Promise(resolve => addLoadEvent(resolve));
+ add_task(async function() {
+ await loaded;
+ SpecialPowers.setCommandNode(window, document.getElementById("i"));
+ SpecialPowers.doCommand(window, "cmd_copyImageContents");
+ let input = document.getElementById("t");
+ input.focus();
+ var controller =
+ SpecialPowers.wrap(input).controllers.getControllerForCommand("cmd_paste");
+ is(controller.isCommandEnabled("cmd_paste"), true,
+ "paste should be enabled in html textareas when an image is on the clipboard");
+ });
+</script>
diff --git a/editor/libeditor/tests/test_pasteImgTextarea.xhtml b/editor/libeditor/tests/test_pasteImgTextarea.xhtml
new file mode 100644
index 0000000000..2dbf0b6427
--- /dev/null
+++ b/editor/libeditor/tests/test_pasteImgTextarea.xhtml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <html:img id="i" src="green.png" />
+ <html:textarea id="t"></html:textarea>
+ </body>
+ <script type="text/javascript"><![CDATA[
+ let loaded = new Promise(resolve => addLoadEvent(resolve));
+ add_task(async function() {
+ await loaded;
+ SpecialPowers.setCommandNode(window, document.getElementById("i"));
+ SpecialPowers.doCommand(window, "cmd_copyImageContents");
+ let input = document.getElementById("t");
+ input.focus();
+ var controller =
+ SpecialPowers.wrap(input).controllers.getControllerForCommand("cmd_paste");
+ is(controller.isCommandEnabled("cmd_paste"), false,
+ "paste should not be enabled in xul textareas when an image is on the clipboard");
+ });
+ ]]></script>
+</window>
diff --git a/editor/libeditor/tests/test_paste_as_quote_in_text_control.html b/editor/libeditor/tests/test_paste_as_quote_in_text_control.html
new file mode 100644
index 0000000000..443ee00eaa
--- /dev/null
+++ b/editor/libeditor/tests/test_paste_as_quote_in_text_control.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing "paste" event dispatching for cmd_pasteQuote command</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ info("Waiting for initializing clipboard...");
+ await SimpleTest.promiseClipboardChange(
+ "plain text",
+ () => SpecialPowers.clipboardCopyString("plain text")
+ );
+
+ for (let selector of ["input", "textarea"]) {
+ const textControl = document.querySelector(selector);
+ textControl.focus();
+ textControl.addEventListener(
+ "paste",
+ event => event.preventDefault(),
+ {once: true}
+ );
+ SpecialPowers.doCommand(window, "cmd_pasteQuote");
+ is(
+ textControl.value,
+ "",
+ `<${selector}> should not have pasted text because "paste" event should've been canceled`
+ );
+ SpecialPowers.doCommand(window, "cmd_pasteQuote");
+ is(
+ textControl.value.replace(/\n/g, ""),
+ "> plain text",
+ `<${selector}> should have pasted text with a ">"`
+ );
+ }
+
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body>
+<input><textarea></textarea>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_paste_no_formatting.html b/editor/libeditor/tests/test_paste_no_formatting.html
new file mode 100644
index 0000000000..384510e405
--- /dev/null
+++ b/editor/libeditor/tests/test_paste_no_formatting.html
@@ -0,0 +1,214 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test pasting formatted test into various fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<input id="input">
+<textarea id="textarea"></textarea>
+<div id="editable" contenteditable="true"></div>
+<div id="noneditable">Text</div>
+
+<div id="source">Some <b>Bold</b> Text</div>
+<script>
+
+const expectedText = "Some Bold Text";
+const expectedHTML = "<div id=\"source\">Some <b>Bold</b> Text</div>";
+
+const htmlPrefix = navigator.platform.includes("Win")
+ ? "<html><body>\n<!--StartFragment-->"
+ : "";
+const htmlPostfix = navigator.platform.includes("Win")
+ ? "<!--EndFragment-->\n</body>\n</html>"
+ : "";
+
+add_task(async function test_paste_formatted() {
+ window.getSelection().selectAllChildren(document.getElementById("source"));
+ synthesizeKey("c", { accelKey: true });
+
+ function doKey(element, withShiftKey)
+ {
+ let inputEventPromise = new Promise(resolve => {
+ element.addEventListener("input", event => {
+ is(event.inputType, "insertFromPaste", "correct inputType");
+ resolve();
+ }, { once: true });
+ });
+ synthesizeKey("v", { accelKey: true, shiftKey: withShiftKey });
+ return inputEventPromise;
+ }
+
+ function cancelEvent(event) {
+ event.preventDefault();
+ }
+
+ // Paste into input and textarea
+ for (let fieldid of ["input", "textarea"]) {
+ const field = document.getElementById(fieldid);
+ field.focus();
+
+ field.addEventListener("paste", cancelEvent);
+ doKey(field, false);
+ is(
+ field.value,
+ "",
+ `Nothing should be pasted into <${field.tagName.toLowerCase()}> when paste event is canceled (shift key is not pressed)`
+ );
+
+ doKey(field, true);
+ is(
+ field.value,
+ "",
+ `Nothing should be pasted into <${field.tagName.toLowerCase()}> when paste event is canceled (shift key is pressed)`
+ );
+ field.removeEventListener("paste", cancelEvent);
+
+ doKey(field, false);
+ is(field.value, expectedText, "paste into " + fieldid);
+
+ doKey(field, true);
+ is(field.value, expectedText + expectedText, "paste unformatted into " + field);
+ }
+
+ const selection = window.getSelection();
+
+ const editable = document.getElementById("editable");
+ const innerHTMLBeforeTest = editable.innerHTML;
+
+ (function test_pasteWithFormatIntoEditableArea() {
+ selection.selectAllChildren(editable);
+ selection.collapseToStart();
+ editable.addEventListener("paste", cancelEvent);
+ doKey(editable, false);
+ is(
+ editable.innerHTML,
+ "",
+ "test_pasteWithFormatIntoEditableArea: Nothing should be pasted when paste event is canceled"
+ );
+ editable.removeEventListener("paste", cancelEvent);
+
+ doKey(editable, false);
+ is(
+ editable.innerHTML,
+ expectedHTML,
+ "test_pasteWithFormatIntoEditableArea: Pasting with format should work as expected"
+ );
+ editable.innerHTML = innerHTMLBeforeTest;
+ }());
+
+ (function test_pasteWithoutFormatIntoEditableArea() {
+ selection.selectAllChildren(editable);
+ selection.collapseToEnd();
+ doKey(editable, true);
+ is(
+ editable.innerHTML,
+ expectedText,
+ "test_pasteWithoutFormatIntoEditableArea: Pasting without format should work as expected",
+ );
+ editable.innerHTML = innerHTMLBeforeTest;
+ })();
+
+ (function test_pasteWithFormatIntoEditableAreaWhenHTMLEditorIsInReadonlyMode() {
+ getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ selection.selectAllChildren(editable);
+ selection.collapseToStart();
+ let beforeInputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ }
+ editable.addEventListener("beforeinput", onBeforeInput);
+ doKey(editable, false);
+ const description = "test_pasteWithFormatIntoEditableAreaWhenHTMLEditorIsInReadonlyMode";
+ is(
+ editable.innerHTML,
+ innerHTMLBeforeTest,
+ `${description}: Pasting with format should not work`
+ );
+ is(
+ beforeInputEvents.length,
+ 0,
+ `${description}: Pasting with format should not cause "beforeinput", but fired "${
+ beforeInputEvents[0]?.inputType
+ }"`
+ );
+ editable.removeEventListener("beforeinput", onBeforeInput);
+ editable.innerHTML = innerHTMLBeforeTest;
+ getEditor().flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ })();
+
+ (function test_pasteWithoutFormatIntoEditableAreaWhenHTMLEditorIsInReadonlyMode() {
+ getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ selection.selectAllChildren(editable);
+ selection.collapseToStart();
+ let beforeInputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ }
+ editable.addEventListener("beforeinput", onBeforeInput);
+ doKey(editable, false);
+ const description = "test_pasteWithoutFormatIntoEditableAreaWhenHTMLEditorIsInReadonlyMode";
+ is(
+ editable.innerHTML,
+ innerHTMLBeforeTest,
+ `${description}: Pasting with format should not work`
+ );
+ is(
+ beforeInputEvents.length,
+ 0,
+ `${description}: Pasting with format should not cause "beforeinput", but fired "${
+ beforeInputEvents[0]?.inputType
+ }"`
+ );
+ editable.removeEventListener("beforeinput", onBeforeInput);
+ editable.innerHTML = innerHTMLBeforeTest;
+ getEditor().flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ })();
+
+ let noneditable = document.getElementById("noneditable");
+ selection.selectAllChildren(noneditable);
+ selection.collapseToStart();
+
+ function getPasteResult() {
+ return new Promise(resolve => {
+ noneditable.addEventListener("paste", event => {
+ resolve({
+ text: event.clipboardData.getData("text/plain"),
+ html: event.clipboardData.getData("text/html"),
+ });
+ }, { once: true});
+ });
+ }
+
+ // Normal paste into non-editable area
+ let pastePromise = getPasteResult();
+ doKey(noneditable, false);
+ is(noneditable.innerHTML, "Text", "paste into non-editable");
+
+ let result = await pastePromise;
+ is(result.text, expectedText, "paste text into non-editable");
+ is(result.html,
+ htmlPrefix + expectedHTML + htmlPostfix,
+ "paste html into non-editable");
+
+ // Unformatted paste into non-editable area
+ pastePromise = getPasteResult();
+ doKey(noneditable, true);
+ is(noneditable.innerHTML, "Text", "paste unformatted into non-editable");
+
+ result = await pastePromise;
+ is(result.text, expectedText, "paste unformatted text into non-editable");
+ // Formatted HTML text should not exist when pasting unformatted.
+ is(result.html, "", "paste unformatted html into non-editable");
+});
+
+function getEditor() {
+ const editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window);
+}
+</script>
+</body>
diff --git a/editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html b/editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html
new file mode 100644
index 0000000000..b82938158e
--- /dev/null
+++ b/editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html
@@ -0,0 +1,143 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing handling "paste" command when a "paste" event listener moves focus</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ info("Waiting for initializing clipboard...");
+ await SimpleTest.promiseClipboardChange(
+ "plain text",
+ () => SpecialPowers.clipboardCopyString("plain text")
+ );
+
+ const transferable =
+ SpecialPowers.Cc["@mozilla.org/widget/transferable;1"].createInstance(SpecialPowers.Ci.nsITransferable);
+ transferable.init(
+ SpecialPowers.wrap(window).docShell.QueryInterface(SpecialPowers.Ci.nsILoadContext)
+ );
+ const supportString =
+ SpecialPowers.Cc["@mozilla.org/supports-string;1"].createInstance(SpecialPowers.Ci.nsISupportsString);
+ supportString.data = "plain text";
+ transferable.setTransferData("text/plain", supportString);
+
+ function getValue(aElement) {
+ if (aElement.tagName.toLowerCase() == "input" ||
+ aElement.tagName.toLowerCase() == "textarea") {
+ return aElement.value;
+ }
+ return aElement.textContent;
+ }
+ function setValue(aElement, aValue) {
+ if (aElement.tagName.toLowerCase() == "input" ||
+ aElement.tagName.toLowerCase() == "textarea") {
+ aElement.value = aValue;
+ return;
+ }
+ aElement.innerHTML = aValue === "" ? "<br>" : aValue;
+ }
+
+ for (const command of [
+ "cmd_paste",
+ "cmd_pasteNoFormatting",
+ "cmd_pasteQuote",
+ "cmd_pasteTransferable"
+ ]) {
+ for (const editableSelector of [
+ "#src > input",
+ "#src > textarea",
+ "#src > div[contenteditable]"
+ ]) {
+ const editableElement = document.querySelector(editableSelector);
+ const editableElementDesc = `<${
+ editableElement.tagName.toLocaleLowerCase()
+ }${editableElement.hasAttribute("contenteditable") ? " contenteditable" : ""}>`;
+ (test_from_editableElement_to_input => {
+ const input = document.querySelector("#dest > input");
+ editableElement.focus();
+ editableElement.addEventListener(
+ "paste",
+ () => input.focus(),
+ {once: true}
+ );
+ SpecialPowers.doCommand(window, command, transferable);
+ is(
+ getValue(editableElement).replace(/\n/g, ""),
+ "",
+ `${command}: ${
+ editableElementDesc
+ } should not have the pasted text because focus is redirected to <input> in a "paste" event listener`
+ );
+ is(
+ input.value.replace("> ", ""),
+ "plain text",
+ `${command}: new focused <input> (moved from ${
+ editableElementDesc
+ }) should have the pasted text`
+ );
+ setValue(editableElement, "");
+ input.value = "";
+ })();
+
+ (test_from_editableElement_to_contenteditable => {
+ const contentEditable = document.querySelector("#dest > div[contenteditable]");
+ editableElement.focus();
+ editableElement.addEventListener(
+ "paste",
+ () => contentEditable.focus(),
+ {once: true}
+ );
+ SpecialPowers.doCommand(window, command, transferable);
+ is(
+ getValue(editableElement).replace(/\n/g, ""),
+ "",
+ `${command}: ${
+ editableElementDesc
+ } should not have the pasted text because focus is redirected to <div contenteditable> in a "paste" event listener`
+ );
+ is(
+ contentEditable.textContent.replace(/\n/g, "").replace("> ", ""),
+ "plain text",
+ `${command}: new focused <div contenteditable> (moved from ${
+ editableElementDesc
+ }) should have the pasted text`
+ );
+ setValue(editableElement, "");
+ contentEditable.innerHTML = "<br>";
+ })();
+
+ (test_from_editableElement_to_non_editable => {
+ const button = document.querySelector("#dest > button");
+ editableElement.focus();
+ editableElement.addEventListener(
+ "paste",
+ () => button.focus(),
+ {once: true}
+ );
+ SpecialPowers.doCommand(window, command, transferable);
+ is(
+ getValue(editableElement).replace(/\n/g, ""),
+ "",
+ `${command}: ${
+ editableElementDesc
+ } should not have the pasted text because focus is redirected to <button> in a "paste" event listener`
+ );
+ setValue(editableElement, "");
+ })();
+ }
+ }
+
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body>
+<div id="src"><input><textarea></textarea><div contenteditable><br></div></div>
+<div id="dest"><input><div contenteditable><br></div><button>button</button></div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_pasting_in_root_element.xhtml b/editor/libeditor/tests/test_pasting_in_root_element.xhtml
new file mode 100644
index 0000000000..d647899ca7
--- /dev/null
+++ b/editor/libeditor/tests/test_pasting_in_root_element.xhtml
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html contenteditable="" xmlns="http://www.w3.org/1999/xhtml"><head>
+ <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1719387 -->
+ <meta charset="utf-8"/>
+ <title>Test to paste plaintext in the clipboard into the html element which does not have body element</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head><span>Text outside body</span><script><![CDATA[
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ info("Waiting for initializing clipboard...");
+ await SimpleTest.promiseClipboardChange(
+ "plain text",
+ () => {
+ SpecialPowers.clipboardCopyString("plain text");
+ }
+ );
+
+ focus();
+
+ function getInnerHTMLOfBody() {
+ return document.documentElement.innerHTML.replace(/\n/g, "")
+ .replace(/ xmlns="http:\/\/www.w3.org\/1999\/xhtml"/g, "")
+ .replace(/<head.+[\/]head>/g, "")
+ .replace(/<script.+[\/]script>/g, "");
+ }
+
+ try {
+ getSelection().collapse(document.documentElement, 1); // collapse to before the <span> element.
+ document.execCommand("insertText", false, "plain text");
+ todo_is(
+ getInnerHTMLOfBody(),
+ "plain text<span>Text outside body</span>", // Chrome's result: "<span>plain textText outside body</span>"
+ "Typing text should insert the text before the <span> element"
+ );
+ } catch (ex) {
+ ok(false, `Failed to typing text due to ${ex}`);
+ } finally {
+ SpecialPowers.doCommand(window, "cmd_undo");
+ }
+
+ try {
+ getSelection().collapse(document.documentElement, 1); // collapse to before the <span> element.
+ SpecialPowers.doCommand(window, "cmd_paste");
+ is(
+ getInnerHTMLOfBody(),
+ "plain text<span>Text outside body</span>", // Chrome's result: "<span>plain textText outside body</span>"
+ "\"cmd_paste\" should insert text in the clipboard before the <span> element"
+ );
+ } catch (ex) {
+ todo(false, `Failed to typing text due to ${ex}`);
+ } finally {
+ SpecialPowers.doCommand(window, "cmd_undo");
+ }
+
+ try {
+ getSelection().collapse(document.documentElement, 1); // collapse to before the <span> element.
+ SpecialPowers.doCommand(window, "cmd_pasteQuote");
+ is(
+ getInnerHTMLOfBody(),
+ "<blockquote type=\"cite\">plain text</blockquote><span>Text outside body</span>",
+ "\"cmd_pasteQuote\" should insert the text wrapping with <blockquote> element before the <span> element"
+ );
+ } catch (ex) {
+ ok(false, `Failed to typing text due to ${ex}`);
+ } finally {
+ SpecialPowers.doCommand(window, "cmd_undo");
+ }
+
+ SimpleTest.finish();
+});
+]]></script></html>
diff --git a/editor/libeditor/tests/test_pasting_in_temporarily_created_div_outside_body.html b/editor/libeditor/tests/test_pasting_in_temporarily_created_div_outside_body.html
new file mode 100644
index 0000000000..05250be049
--- /dev/null
+++ b/editor/libeditor/tests/test_pasting_in_temporarily_created_div_outside_body.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<html>
+<head>
+<title>Test for paste in temporarily created div element outside the body element</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const editor = document.querySelector("div[contenteditable]");
+ const heading = document.querySelector("h1");
+ getSelection().setBaseAndExtent(heading.firstChild, "So".length,
+ heading.firstChild, "Some te".length);
+ try {
+ await SimpleTest.promiseClipboardChange(
+ "me te", () => synthesizeKey("c", {accelKey: true}));
+ } catch (ex) {
+ ok(false, `Failed to copy selected text: ${ex}`);
+ SimpleTest.finish();
+ }
+ editor.focus();
+ editor.addEventListener("paste", () => {
+ const anotherEditor = document.createElement("div");
+ anotherEditor.setAttribute("contenteditable", "true");
+ document.documentElement.appendChild(anotherEditor);
+ anotherEditor.focus();
+ }, {once: true});
+ synthesizeKey("v", {accelKey: true});
+ const tempEditor = document.documentElement.lastChild;
+ is(tempEditor.nodeName.toLocaleLowerCase(), "div",
+ "Paste event handler should've inserted another editor");
+ is(tempEditor.textContent.trim(), "me te");
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body>
+ <h1>Some text</h1>
+ <div contenteditable></div>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_pasting_table_rows.html b/editor/libeditor/tests/test_pasting_table_rows.html
new file mode 100644
index 0000000000..328fb0b297
--- /dev/null
+++ b/editor/libeditor/tests/test_pasting_table_rows.html
@@ -0,0 +1,554 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test pasting table rows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <style>
+ /**
+ * A small font-size, so that the loaded document fits on the screens of all
+ * test devices.
+ */
+ * { font-size: 8px; }
+
+ /**
+ * Helps fitting the tables on the screens of all test devices.
+ */
+ div[class="tableContainer"] {
+ display: inline-block;
+ }
+ </style>
+ <script>
+ const kEditabilityModeContenteditable = "contenteditable";
+ const kEditabilityModeDesignMode = "designMode";
+
+ // All column names of the test-tables used below.
+ const kColumns = ["c1", "c2", "c3"];
+
+ // Ctrl+click on table cells to select them.
+ const kSelectionModeClickSelection = "click-selection";
+ // Click and drag from the first given row to the end of the last given row.
+ const kSelectionModeDragSelection = "drag-selection";
+
+ const kTableTagName = "TABLE";
+ const kTbodyTagName = "TBODY";
+ const kTheadTagName = "THEAD";
+ const kTfootTagName = "TFOOT";
+
+ const kInputEventType = "input";
+ const kInputEventInputTypeInsertFromPaste = "insertFromPaste";
+
+ // Where a table is pasted to in the test.
+ const kTargetElementId = "targetElement";
+
+ /**
+ * @param aTableName see Test::constructor::aTableName.
+ * @param aRowsInTable see Test::constructor::aRowsInTable.
+ * @return an array of elements of aRowsInTable.
+ */
+ function FilterRowsWithParentTag(aTableName, aRowsInTable, aTagName) {
+ return aRowsInTable.filter(rowName => document.getElementById(aTableName +
+ rowName).parentElement.tagName == aTagName);
+ }
+
+ /**
+ * Tables used with this class are required to:
+ * - have ids of the following form for each table cell:
+ <tableName><rowName><column>. Where <column> has to be one of
+ `kColumns`.
+ - have exactly `kColumns.length` columns per row.
+ - have an id of the form <tableName><rowName> for each table row.
+ */
+ class Test {
+ /**
+ * @param aTableName indicates which table to operate on.
+ * @param aRowsInTable an array of row names. Ordered from top to bottom.
+ * @param aEditabilityMode `kEditabilityModeContenteditable` or
+ * `kEditabilityModeDesignMode`.
+ * @param aSelectionMode `kSelectionModeClickSelection` or
+ * `kSelectionModeDragSelection`.
+ */
+ constructor(aTableName, aRowsInTable, aEditabilityMode, aSelectionMode) {
+ ok(aEditabilityMode == kEditabilityModeContenteditable ||
+ aEditabilityMode == kEditabilityModeDesignMode,
+ "Editablity mode is valid.");
+
+ ok(aSelectionMode == kSelectionModeClickSelection ||
+ aSelectionMode == kSelectionModeDragSelection,
+ "Selection mode is valid.");
+
+ this._tableName = aTableName;
+ this._rowsInTable = aRowsInTable;
+ this._editabilityMode = aEditabilityMode;
+ this._selectionMode = aSelectionMode;
+ this._innerHTMLOfTargetBeforeTestRun =
+ document.getElementById(kTargetElementId).innerHTML;
+
+ if (this._editabilityMode == kEditabilityModeDesignMode) {
+ this._removeContenteditableAttributeOfTarget();
+ document.designMode = "on";
+ }
+
+ SimpleTest.info("Constructed the test (" + this._toString() + ").");
+ }
+
+ /**
+ * Call `_restoreStateOfDocumentBeforeRun` afterwards.
+ */
+ async _run() {
+ // Generate the expected pasted HTML before pasting the clipboard's
+ // content, because that may duplicate ids, hence leading to creating
+ // a wrong expectation string.
+ const expectedPastedHTML = this._createExpectedOuterHTMLOfTable();
+
+ if (this._selectionMode == kSelectionModeDragSelection) {
+ this._dragSelectAllCellsInRowsOfTable();
+ } else {
+ this._clickSelectAllCellsInRowsOfTable();
+ }
+
+ await this._copyToClipboard(expectedPastedHTML);
+ this._pasteToTargetElement();
+
+ const targetElement = document.getElementById(kTargetElementId);
+ is(targetElement.children.length, 1,
+ "Target element has exactly one child.");
+ is(targetElement.children[0]?.tagName, kTableTagName,
+ "Target element has a table child.");
+
+ // Linebreaks and whitespace after tags are irrelevant, hence stripping
+ // them.
+ is(SimpleTest.stripLinebreaksAndWhitespaceAfterTags(
+ targetElement.children[0]?.outerHTML), expectedPastedHTML,
+ "Pasted table (" + this._toString() + ") has expected outerHTML.");
+ }
+
+ _restoreStateOfDocumentBeforeRun() {
+ if (this._editabilityMode == kEditabilityModeDesignMode) {
+ document.designMode = "off";
+ this._setContenteditableAttributeOfTarget();
+ }
+
+ const targetElement = document.getElementById(kTargetElementId);
+ targetElement.innerHTML = this._innerHTMLOfTargetBeforeTestRun;
+ targetElement.getBoundingClientRect();
+
+ SimpleTest.info(
+ "Restored the state of the document before the test run.");
+ }
+
+ _toString() {
+ return "table: " + this._tableName + "; row(s): " +
+ this._rowsInTable.toString() + "; editability-mode: " +
+ this._editabilityMode + "; selection-mode: " + this._selectionMode;
+ }
+
+ _removeContenteditableAttributeOfTarget() {
+ const targetElement = document.getElementById(kTargetElementId);
+ SimpleTest.info("Removing target's 'contenteditable' attribute.");
+ targetElement.removeAttribute("contenteditable");
+ }
+
+ _setContenteditableAttributeOfTarget() {
+ const targetElement = document.getElementById(kTargetElementId);
+ SimpleTest.info("Setting 'contenteditable' attribute of target.");
+ targetElement.setAttribute("contenteditable", "");
+ }
+
+ _getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(aElementId) {
+ const outerHTML = document.getElementById(aElementId).outerHTML;
+ return SimpleTest.stripLinebreaksAndWhitespaceAfterTags(outerHTML);
+ }
+
+ _createExpectedOuterHTMLOfTable() {
+ const rowsInTableHead = FilterRowsWithParentTag(this._tableName,
+ this._rowsInTable, kTheadTagName);
+
+ const rowsInTableBody = FilterRowsWithParentTag(this._tableName,
+ this._rowsInTable, kTbodyTagName);
+
+ const rowsInTableFoot = FilterRowsWithParentTag(this._tableName,
+ this._rowsInTable, kTfootTagName);
+
+ let expectedTableOuterHTML = '\
+<table>';
+
+ if (rowsInTableHead.length) {
+ expectedTableOuterHTML += '\
+<thead>';
+ rowsInTableHead.forEach(rowName =>
+ expectedTableOuterHTML +=
+ this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(
+ this._tableName + rowName));
+ expectedTableOuterHTML +='\
+</thead>';
+ }
+
+ if (rowsInTableBody.length) {
+ expectedTableOuterHTML += '\
+<tbody>';
+
+ rowsInTableBody.forEach(rowName =>
+ expectedTableOuterHTML +=
+ this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(
+ this._tableName + rowName));
+
+ expectedTableOuterHTML +='\
+</tbody>';
+ }
+
+ if (rowsInTableFoot.length) {
+ expectedTableOuterHTML += '\
+<tfoot>';
+ rowsInTableFoot.forEach(rowName =>
+ expectedTableOuterHTML +=
+ this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(this._tableName
+ + rowName));
+ expectedTableOuterHTML += '\
+</tfoot>';
+ }
+
+ expectedTableOuterHTML += '\
+</table>';
+
+ return expectedTableOuterHTML;
+ }
+
+ _clickSelectAllCellsInRowsOfTable() {
+ function synthesizeAccelKeyAndClickAt(aElementId) {
+ const element = document.getElementById(aElementId);
+ synthesizeMouseAtCenter(element, { accelKey: true });
+ }
+
+ this._rowsInTable.forEach(rowName => kColumns.forEach(column =>
+ synthesizeAccelKeyAndClickAt(this._tableName + rowName + column)));
+ }
+
+ _dragSelectAllCellsInRowsOfTable() {
+ const firstColumnOfFirstRow = document.getElementById(this._tableName +
+ this._rowsInTable[0] + kColumns[0]);
+ const lastColumnOfLastRow = document.getElementById(this._tableName +
+ this._rowsInTable.slice(-1)[0] + kColumns.slice(-1)[0]);
+
+ synthesizeMouse(firstColumnOfFirstRow, 0 /* aOffsetX */,
+ 0 /* aOffsetY */, { type: "mousedown" } /* aEvent */);
+
+ const rectOfLastColumnOfLastRow =
+ lastColumnOfLastRow.getBoundingClientRect();
+
+ synthesizeMouse(lastColumnOfLastRow, rectOfLastColumnOfLastRow.width
+ /* aOffsetX */, rectOfLastColumnOfLastRow.height /* aOffsetY */,
+ { type: "mousemove" } /* aEvent */);
+
+ synthesizeMouse(lastColumnOfLastRow, rectOfLastColumnOfLastRow.width
+ /* aOffsetX */, rectOfLastColumnOfLastRow.height /* aOffsetY */,
+ { type: "mouseup" } /* aEvent */);
+ }
+
+ /**
+ * @return a promise.
+ */
+ async _copyToClipboard(aExpectedPastedHTML) {
+ const flavor = "text/html";
+
+ const expectedPastedHTML = (() => {
+ if (navigator.platform.includes(kPlatformWindows)) {
+ // TODO: ideally, this should be factored out, see bug 1669963.
+
+ // Windows wraps the pasted HTML, see
+ // https://searchfox.org/mozilla-central/rev/8f7b017a31326515cb467e69eef1f6c965b4f00e/widget/windows/nsDataObj.cpp#1798-1805,1839-1840,1842.
+ return kTextHtmlPrefixClipboardDataWindows +
+ aExpectedPastedHTML + kTextHtmlSuffixClipboardDataWindows;
+ }
+ return aExpectedPastedHTML;
+ })();
+
+ function validatorFn(aData) {
+ // The data's format doesn't specify whether there should be line
+ // breaks or whitspace between tags. Hence, remove them.
+ if (SimpleTest.stripLinebreaksAndWhitespaceAfterTags(aData) ==
+ SimpleTest.stripLinebreaksAndWhitespaceAfterTags(expectedPastedHTML)) {
+ return true;
+ }
+ info(`Waiting clipboard data: expected:\n"${
+ SimpleTest.stripLinebreaksAndWhitespaceAfterTags(expectedPastedHTML)
+ }"\n, but got:\n"${
+ SimpleTest.stripLinebreaksAndWhitespaceAfterTags(aData)
+ }"`);
+ return false;
+ }
+
+ return SimpleTest.promiseClipboardChange(validatorFn,
+ () => synthesizeKey("c", { accelKey: true } /* aEvent*/), flavor);
+ }
+
+ _pasteToTargetElement() {
+ const editingHost = (this._editabilityMode ==
+ kEditabilityModeContenteditable) ?
+ document.getElementById(kTargetElementId) :
+ document;
+
+ let inputEvent;
+ function handleInputEvent(aEvent) {
+ if (aEvent.inputType == kInputEventInputTypeInsertFromPaste) {
+ editingHost.removeEventListener(kInputEventType, handleInputEvent);
+ SimpleTest.info(
+ 'Listened to an "' + kInputEventInputTypeInsertFromPaste + '" "'
+ + kInputEventType + ' event.');
+ inputEvent = aEvent;
+ }
+ }
+ editingHost.addEventListener(kInputEventType, handleInputEvent);
+
+ const targetElement = document.getElementById(kTargetElementId);
+ synthesizeMouseAtCenter(targetElement, {});
+ synthesizeKey("v", { accelKey: true } /* aEvent */);
+
+ ok(
+ inputEvent != undefined,
+ `An ${kInputEventType} whose "inputType" is ${
+ kInputEventInputTypeInsertFromPaste
+ } should've been fired on ${editingHost.localName}`
+ );
+ }
+ }
+
+ function ContainsRowWithParentTag(aTableName, aRowsInTable, aTagName) {
+ return !!FilterRowsWithParentTag(aTableName, aRowsInTable,
+ aTagName).length;
+ }
+
+ function DoesContainRowInTheadAndTbody(aTableName, aRowsInTable) {
+ return ContainsRowWithParentTag(aTableName, aRowsInTable, kTheadTagName) &&
+ ContainsRowWithParentTag(aTableName, aRowsInTable, kTbodyTagName);
+ }
+
+ function DoesContainRowInTbodyAndTfoot(aTableName, aRowsInTable) {
+ return ContainsRowWithParentTag(aTableName, aRowsInTable, kTbodyTagName)
+ && ContainsRowWithParentTag(aTableName, aRowsInTable, kTfootTagName);
+ }
+
+ async function runTests() {
+ const kClickSelectionTests = {
+ selectionMode : kSelectionModeClickSelection,
+ tablesToTest : ["t1", "t2", "t3", "t4", "t5"],
+ rowsToSelect : [
+ ["r1", "r2", "r3", "r4"],
+ ["r1"],
+ ["r2", "r3"],
+ ["r1", "r3"],
+ ["r3", "r4"],
+ ["r4"],
+ ],
+ };
+
+ const kDragSelectionTests = {
+ selectionMode : kSelectionModeDragSelection,
+ tablesToTest : ["t1", "t2", "t3", "t4", "t5"],
+ // Only consecutive rows when drag-selecting.
+ rowsToSelect : [
+ ["r1", "r2", "r3", "r4"],
+ ["r1"],
+ ["r2", "r3"],
+ ["r3", "r4"],
+ ["r4"],
+ ],
+ };
+
+ const kTestGroups = [kClickSelectionTests, kDragSelectionTests];
+
+ const kEditabilityModes = [
+ kEditabilityModeContenteditable,
+ kEditabilityModeDesignMode,
+ ];
+
+ for (const editabilityMode of kEditabilityModes) {
+ for (const testGroup of kTestGroups) {
+ for (const tableName of testGroup.tablesToTest) {
+ for (const rowsToSelect of testGroup.rowsToSelect) {
+ if (DoesContainRowInTheadAndTbody(tableName, rowsToSelect) ||
+ DoesContainRowInTbodyAndTfoot(tableName, rowsToSelect)) {
+ todo(false,
+ 'Rows to select (' + rowsToSelect.toString() + ') contains ' +
+ ' row in <tbody> and <thead> or <tfoot> of table "' +
+ tableName + '", see bug 1667786.');
+ continue;
+ }
+
+ const test = new Test(tableName, rowsToSelect, editabilityMode,
+ testGroup.selectionMode);
+ try {
+ await test._run();
+ } catch (ex) {
+ ok(false, `Aborting the following tests due to unexpected error: ${ex.message}`);
+ SimpleTest.finish();
+ return;
+ }
+ test._restoreStateOfDocumentBeforeRun();
+ }
+ }
+ }
+ }
+
+ SimpleTest.finish();
+ }
+
+ function onLoad() {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(runTests);
+ }
+ </script>
+</head>
+<body onload="onLoad()">
+<p id="display"></p>
+ <h4>Test for <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1639972">bug 1639972</a></h4>
+ <div id="content">
+ <div class="tableContainer">Table with <code>tbody</code> and <code>td</code>:
+ <table>
+ <tbody>
+ <tr id="t1r1">
+ <td id="t1r1c1">r1c1</td>
+ <td id="t1r1c2">r1c2</td>
+ <td id="t1r1c3">r1c3</td>
+ </tr>
+ <tr id="t1r2">
+ <td id="t1r2c1">r2c1</td>
+ <td id="t1r2c2">r2c2</td>
+ <td id="t1r2c3">r2c3</td>
+ </tr>
+ <tr id="t1r3">
+ <td id="t1r3c1">r3c1</td>
+ <td id="t1r3c2">r3c2</td>
+ <td id="t1r3c3">r3c3</td>
+ </tr>
+ <tr id="t1r4">
+ <td id="t1r4c1">r4c1</td>
+ <td id="t1r4c2">r4c2</td>
+ <td id="t1r4c3">r4c3</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="tableContainer">Table with <code>tbody</code>, <code>td</code> and <code>th</code>:
+ <table>
+ <tbody>
+ <tr id="t2r1">
+ <th id="t2r1c1">r1c1</th>
+ <th id="t2r1c2">r1c2</th>
+ <th id="t2r1c3">r1c3</th>
+ </tr>
+ <tr id="t2r2">
+ <td id="t2r2c1">r2c1</td>
+ <td id="t2r2c2">r2c2</td>
+ <td id="t2r2c3">r2c3</td>
+ </tr>
+ <tr id="t2r3">
+ <td id="t2r3c1">r3c1</td>
+ <td id="t2r3c2">r3c2</td>
+ <td id="t2r3c3">r3c3</td>
+ </tr>
+ <tr id="t2r4">
+ <td id="t2r4c1">r4c1</td>
+ <td id="t2r4c2">r4c2</td>
+ <td id="t2r4c3">r4c3</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="tableContainer">Table with <code>thead</code>, <code>tbody</code>, <code>td</code>:
+ <table>
+ <thead>
+ <tr id="t3r1">
+ <td id="t3r1c1">r1c1</td>
+ <td id="t3r1c2">r1c2</td>
+ <td id="t3r1c3">r1c3</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="t3r2">
+ <td id="t3r2c1">r2c1</td>
+ <td id="t3r2c2">r2c2</td>
+ <td id="t3r2c3">r2c3</td>
+ </tr>
+ <tr id="t3r3">
+ <td id="t3r3c1">r3c1</td>
+ <td id="t3r3c2">r3c2</td>
+ <td id="t3r3c3">r3c3</td>
+ </tr>
+ <tr id="t3r4">
+ <td id="t3r4c1">r4c1</td>
+ <td id="t3r4c2">r4c2</td>
+ <td id="t3r4c3">r4c3</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="tableContainer">Table with <code>thead</code>, <code>tbody</code>, <code>td</code> and <code>th</code>:
+ <table>
+ <thead>
+ <tr id="t4r1">
+ <th id="t4r1c1">r1c1</th>
+ <th id="t4r1c2">r1c2</th>
+ <th id="t4r1c3">r1c3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="t4r2">
+ <td id="t4r2c1">r2c1</td>
+ <td id="t4r2c2">r2c2</td>
+ <td id="t4r2c3">r2c3</td>
+ </tr>
+ <tr id="t4r3">
+ <td id="t4r3c1">r3c1</td>
+ <td id="t4r3c2">r3c2</td>
+ <td id="t4r3c3">r3c3</td>
+ </tr>
+ <tr id="t4r4">
+ <td id="t4r4c1">r4c1</td>
+ <td id="t4r4c2">r4c2</td>
+ <td id="t4r4c3">r4c3</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="tableContainer">Table with <code>thead</code>,
+ <code>tbody</code>, <code>tfoot</code>, and <code>td</code>:
+ <table>
+ <thead>
+ <tr id="t5r1">
+ <td id="t5r1c1">r1c1</td>
+ <td id="t5r1c2">r1c2</td>
+ <td id="t5r1c3">r1c3</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="t5r2">
+ <td id="t5r2c1">r2c1</td>
+ <td id="t5r2c2">r2c2</td>
+ <td id="t5r2c3">r2c3</td>
+ </tr>
+ <tr id="t5r3">
+ <td id="t5r3c1">r3c1</td>
+ <td id="t5r3c2">r3c2</td>
+ <td id="t5r3c3">r3c3</td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr id="t5r4">
+ <td id="t5r4c1">r4c1</td>
+ <td id="t5r4c2">r4c2</td>
+ <td id="t5r4c3">r4c3</td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ <p>Target for pasting:
+ <div id="targetElement" contenteditable><!-- Some content so that it can be clicked on. -->X</div>
+ </p>
+ </div>
+</html>
diff --git a/editor/libeditor/tests/test_pasting_text_longer_than_maxlength.html b/editor/libeditor/tests/test_pasting_text_longer_than_maxlength.html
new file mode 100644
index 0000000000..d25875431e
--- /dev/null
+++ b/editor/libeditor/tests/test_pasting_text_longer_than_maxlength.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1320229
+
+Checks if a user can paste a password longer than `maxlength` and if the field
+is then marked as `tooLong`.
+-->
+<head>
+ <title>Test pasting text longer than maxlength</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1320229">Mozilla Bug 1320229</a>
+<p id="display"></p>
+<div id="content">
+ <div id="src">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</div>
+ <input id="form-password" type="password" maxlength="20">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1320229 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ await SpecialPowers.pushPrefEnv({"set": [["editor.truncate_user_pastes", false]]});
+ var src = document.getElementById("src");
+ var pwd = document.getElementById("form-password");
+ SimpleTest.waitForClipboard(src.textContent,
+ function() {
+ getSelection().selectAllChildren(src);
+ synthesizeKey("C", {accelKey: true});
+ },
+ function() {
+ pwd.focus();
+ synthesizeKey("V", {accelKey: true});
+ is(pwd.value, src.textContent,
+ "Pasting should paste the clipboard contents regardless of maxlength");
+ is(pwd.validity.tooLong, true, "Pasting over maxlength should set the tooLong flag")
+ SimpleTest.finish();
+ },
+ function() {
+ SimpleTest.finish();
+ }
+ );
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_resizers_appearance.html b/editor/libeditor/tests/test_resizers_appearance.html
new file mode 100644
index 0000000000..e09c80f530
--- /dev/null
+++ b/editor/libeditor/tests/test_resizers_appearance.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for resizers appearance</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="editor" contenteditable></div>
+<div id="clickaway" style="width: 3px; height: 3px;"></div>
+<img src="green.png"><!-- for ensuring to load the image at first test of <img> case -->
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ async function waitForSelectionChange() {
+ return new Promise(resolve => {
+ document.addEventListener("selectionchange", () => {
+ resolve();
+ }, {once: true});
+ });
+ }
+
+ let editor = document.getElementById("editor");
+ let outOfEditor = document.getElementById("clickaway");
+
+ const kTests = [
+ { description: "<img>",
+ innerHTML: "<img id=\"target\" src=\"green.png\" width=\"100\" height=\"100\">",
+ resizable: true,
+ },
+ { description: "<table>",
+ innerHTML: "<table id=\"target\" border><tr><td>1-1</td><td>1-2</td></tr><tr><td>2-1</td><td>2-2</td></tr></table>",
+ resizable: true,
+ },
+ { description: "absolute positioned <div>",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; top: 50px; left: 50px;\">positioned</div>",
+ resizable() { return document.queryCommandState("enableAbsolutePositionEditing"); },
+ },
+ { description: "fixed positioned <div>",
+ innerHTML: "<div id=\"target\" style=\"position: fixed; top: 50px; left: 50px;\">positioned</div>",
+ resizable: false,
+ },
+ { description: "relative positioned <div>",
+ innerHTML: "<div id=\"target\" style=\"position: relative; top: 50px; left: 50px;\">positioned</div>",
+ resizable: false,
+ },
+ ];
+
+ for (let kEnableAbsolutePositionEditor of [true, false]) {
+ document.execCommand("enableAbsolutePositionEditing", false, kEnableAbsolutePositionEditor);
+ for (const kTest of kTests) {
+ const kDescription = kTest.description +
+ (kEnableAbsolutePositionEditor ? " (enabled absolute position editor)" : "") + ": ";
+ editor.innerHTML = kTest.innerHTML;
+ let target = document.getElementById("target");
+
+ document.execCommand("enableObjectResizing", false, false);
+ ok(!document.queryCommandState("enableObjectResizing"),
+ kDescription + "Object resizer should be disabled by the call of execCommand");
+
+ synthesizeMouseAtCenter(outOfEditor, {});
+ let promiseSelectionChangeEvent1 = waitForSelectionChange();
+ synthesizeMouseAtCenter(target, {});
+ await promiseSelectionChangeEvent1;
+
+ ok(!target.hasAttribute("_moz_resizing"),
+ kDescription + ": While enableObjectResizing is disabled, resizers shouldn't appear");
+
+ document.execCommand("enableObjectResizing", false, true);
+ ok(document.queryCommandState("enableObjectResizing"),
+ kDescription + "Object resizer should be enabled by the call of execCommand");
+
+ synthesizeMouseAtCenter(outOfEditor, {});
+ let promiseSelectionChangeEvent2 = waitForSelectionChange();
+ synthesizeMouseAtCenter(target, {});
+ await promiseSelectionChangeEvent2;
+
+ const kResizable = typeof kTest.resizable === "function" ? kTest.resizable() : kTest.resizable;
+ is(target.hasAttribute("_moz_resizing"), kResizable,
+ kDescription + (kResizable ? "While enableObjectResizing is enabled, resizers should appear" :
+ "Even while enableObjectResizing is enabled, resizers shouldn't appear"));
+
+ document.execCommand("enableObjectResizing", false, false);
+ ok(!target.hasAttribute("_moz_resizing"),
+ kDescription + "enableObjectResizing is disabled even while resizers are visible, resizers should disappear");
+
+ document.execCommand("enableObjectResizing", false, true);
+ is(target.hasAttribute("_moz_resizing"), kResizable,
+ kDescription + (kResizable ? "enableObjectResizing is enabled when resizable object is selected, resizers should appear" :
+ "Even if enableObjectResizing is enabled when non-resizable object is selected, resizers shouldn't appear"));
+ }
+ }
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_resizers_resizing_elements.html b/editor/libeditor/tests/test_resizers_resizing_elements.html
new file mode 100644
index 0000000000..f30b6bb269
--- /dev/null
+++ b/editor/libeditor/tests/test_resizers_resizing_elements.html
@@ -0,0 +1,299 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for resizers of some elements</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #target {
+ background-color: green;
+ }
+ </style>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" contenteditable style="width: 200px; height: 200px;"></div>
+<div id="clickaway" style="width: 10px; height: 10px"></div>
+<img src="green.png"><!-- for ensuring to load the image at first test of <img> case -->
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ document.execCommand("enableObjectResizing", false, true);
+ ok(document.queryCommandState("enableObjectResizing"),
+ "Object resizer should be enabled by the call of execCommand");
+ // Disable inline-table-editing UI for this test.
+ document.execCommand("enableInlineTableEditing", false, false);
+
+ let outOfEditor = document.getElementById("clickaway");
+
+ function cancel(e) { e.stopPropagation(); }
+ let content = document.getElementById("content");
+ content.addEventListener("mousedown", cancel);
+ content.addEventListener("mousemove", cancel);
+ content.addEventListener("mouseup", cancel);
+
+ async function waitForSelectionChange() {
+ return new Promise(resolve => {
+ document.addEventListener("selectionchange", () => {
+ resolve();
+ }, {once: true});
+ });
+ }
+
+ async function doTest(aDescription, aPreserveRatio, aInnerHTML) {
+ let description = aDescription;
+ if (document.queryCommandState("enableAbsolutePositionEditing")) {
+ description += " (absolute position editor is enabled)";
+ }
+ description += ": ";
+ content.innerHTML = aInnerHTML;
+ let target = document.getElementById("target");
+
+ /**
+ * This function is a generic resizer test.
+ * We have 8 resizers that we'd like to test, and each can be moved in 8 different directions.
+ * In specifying baseX, W can be considered to be the width of the image, and for baseY, H
+ * can be considered to be the height of the image. deltaX and deltaY are regular pixel values
+ * which can be positive or negative.
+ * TODO: Should test canceling "beforeinput" events case.
+ */
+ const W = 1;
+ const H = 1;
+ async function testResizer(baseX, baseY, deltaX, deltaY, expectedDeltaX, expectedDeltaY) {
+ ok(true, description + "testResizer(" + [baseX, baseY, deltaX, deltaY, expectedDeltaX, expectedDeltaY].join(", ") + ")");
+
+ // Reset the dimensions of the target.
+ target.style.width = "150px";
+ target.style.height = "150px";
+ let rect = target.getBoundingClientRect();
+ is(rect.width, 150, description + "Sanity check the width");
+ is(rect.height, 150, description + "Sanity check the height");
+
+ // Click on the target to show the resizers
+ ok(true, "waiting selectionchange to select the target element");
+ let promiseSelectionChangeEvent = waitForSelectionChange();
+ synthesizeMouseAtCenter(target, {});
+ await promiseSelectionChangeEvent;
+
+ // Determine which resizer we're dealing with.
+ let basePosX = rect.width * baseX;
+ let basePosY = rect.height * baseY;
+
+ let inputEventExpected = true;
+ function onInput(aEvent) {
+ if (!inputEventExpected) {
+ ok(false, `"${aEvent.type}" event shouldn't be fired after stopping resizing`);
+ return;
+ }
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface`);
+ is(aEvent.cancelable, false,
+ `"${aEvent.type}" event should be never cancelable`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble`);
+ is(aEvent.inputType, "",
+ `inputType of "${aEvent.type}" event should be empty string when an element is resized`);
+ is(aEvent.data, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `data of "${aEvent.type}" event should be null ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aEvent.type === "beforeinput") {
+ let selection = document.getSelection();
+ is(targetRanges.length, selection.rangeCount,
+ `getTargetRanges() of "beforeinput" event for position changing of absolute position should return selection ranges ${aDescription}`);
+ if (targetRanges.length === selection.rangeCount) {
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i);
+ is(targetRanges[i].startContainer, range.startContainer,
+ `startContainer of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match ${aDescription}`);
+ is(targetRanges[i].startOffset, range.startOffset,
+ `startOffset of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match ${aDescription}`);
+ is(targetRanges[i].endContainer, range.endContainer,
+ `endContainer of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match ${aDescription}`);
+ is(targetRanges[i].endOffset, range.endOffset,
+ `endOffset of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match ${aDescription}`);
+ }
+ }
+ } else {
+ is(targetRanges.length, 0,
+ `getTargetRanges() of "${aEvent.type}" event for position changing of absolute position should return empty array ${aDescription}`);
+ }
+ }
+
+ content.addEventListener("beforeinput", onInput);
+ content.addEventListener("input", onInput);
+
+ // Click on the correct resizer
+ synthesizeMouse(target, basePosX, basePosY, {type: "mousedown"});
+ // Drag it delta pixels to the right and bottom (or maybe left and top!)
+ synthesizeMouse(target, basePosX + deltaX, basePosY + deltaY, {type: "mousemove"});
+ // Release the mouse button
+ synthesizeMouse(target, basePosX + deltaX, basePosY + deltaY, {type: "mouseup"});
+
+ inputEventExpected = false;
+
+ // Move the mouse delta more pixels to the same direction to make sure that the
+ // resize operation has stopped.
+ synthesizeMouse(target, basePosX + deltaX * 2, basePosY + deltaY * 2, {type: "mousemove"});
+
+ // Click outside of the editor to hide the resizers
+ ok(true, "waiting selectionchange to select outside the target element");
+ let promiseSelectionExitEvent = waitForSelectionChange();
+ synthesizeMouseAtCenter(outOfEditor, {});
+ await promiseSelectionExitEvent;
+
+ // Get the new dimensions for the target
+ // XXX I don't know why we need 2px margin to check this on Android.
+ // Fortunately, this test checks whether objects are resizable
+ // actually. So, bigger difference is okay.
+ let newRect = target.getBoundingClientRect();
+ isfuzzy(newRect.width, rect.width + expectedDeltaX, 2, description + "The width should be increased by " + expectedDeltaX + " pixels");
+ isfuzzy(newRect.height, rect.height + expectedDeltaY, 2, description + "The height should be increased by " + expectedDeltaY + "pixels");
+
+ content.removeEventListener("beforeinput", onInput);
+ content.removeEventListener("input", onInput);
+ }
+
+ // Account for changes in the resizing behavior when we're trying to preserve
+ // the aspect ration of image.
+ // ignoredGrowth means we don't change the size of a dimension because otherwise
+ // the aspect ratio would change undesirably.
+ // needlessGrowth means that we change the size of a dimension perpendecular to
+ // the mouse movement axis in order to preserve the aspect ratio.
+ // reversedGrowth means that we change the size of a dimension in the opposite
+ // direction to the mouse movement in order to maintain the aspect ratio.
+ const ignoredGrowth = aPreserveRatio ? 0 : 1;
+ const needlessGrowth = aPreserveRatio ? 1 : 0;
+ const reversedGrowth = aPreserveRatio ? -1 : 1;
+
+ /* eslint-disable no-multi-spaces */
+
+ // top resizer
+ await testResizer(W / 2, 0, -10, -10, 0, 10);
+ await testResizer(W / 2, 0, -10, 0, 0, 0);
+ await testResizer(W / 2, 0, -10, 10, 0, -10);
+ await testResizer(W / 2, 0, 0, -10, 0, 10);
+ await testResizer(W / 2, 0, 0, 0, 0, 0);
+ await testResizer(W / 2, 0, 0, 10, 0, -10);
+ await testResizer(W / 2, 0, 10, -10, 0, 10);
+ await testResizer(W / 2, 0, 10, 0, 0, 0);
+ await testResizer(W / 2, 0, 10, 10, 0, -10);
+
+ // top right resizer
+ await testResizer( W, 0, -10, -10, -10 * reversedGrowth, 10);
+ await testResizer( W, 0, -10, 0, -10 * ignoredGrowth, 0);
+ await testResizer( W, 0, -10, 10, -10, -10);
+ await testResizer( W, 0, 0, -10, 10 * needlessGrowth, 10);
+ await testResizer( W, 0, 0, 0, 0, 0);
+ await testResizer( W, 0, 0, 10, 0, -10 * ignoredGrowth);
+ await testResizer( W, 0, 10, -10, 10, 10);
+ await testResizer( W, 0, 10, 0, 10, 10 * needlessGrowth);
+ await testResizer( W, 0, 10, 10, 10, -10 * reversedGrowth);
+
+ // right resizer
+ await testResizer( W, H / 2, -10, -10, -10, 0);
+ await testResizer( W, H / 2, -10, 0, -10, 0);
+ await testResizer( W, H / 2, -10, 10, -10, 0);
+ await testResizer( W, H / 2, 0, -10, 0, 0);
+ await testResizer( W, H / 2, 0, 0, 0, 0);
+ await testResizer( W, H / 2, 0, 10, 0, 0);
+ await testResizer( W, H / 2, 10, -10, 10, 0);
+ await testResizer( W, H / 2, 10, 0, 10, 0);
+ await testResizer( W, H / 2, 10, 10, 10, 0);
+
+ // bottom right resizer
+ await testResizer( W, H, -10, -10, -10, -10);
+ await testResizer( W, H, -10, 0, -10 * ignoredGrowth, 0);
+ await testResizer( W, H, -10, 10, -10 * reversedGrowth, 10);
+ await testResizer( W, H, 0, -10, 0, -10 * ignoredGrowth);
+ await testResizer( W, H, 0, 0, 0, 0);
+ await testResizer( W, H, 0, 10, 10 * needlessGrowth, 10);
+ await testResizer( W, H, 10, -10, 10, -10 * reversedGrowth);
+ await testResizer( W, H, 10, 0, 10, 10 * needlessGrowth);
+ await testResizer( W, H, 10, 10, 10, 10);
+
+ // bottom resizer
+ await testResizer(W / 2, H, -10, -10, 0, -10);
+ await testResizer(W / 2, H, -10, 0, 0, 0);
+ await testResizer(W / 2, H, -10, 10, 0, 10);
+ await testResizer(W / 2, H, 0, -10, 0, -10);
+ await testResizer(W / 2, H, 0, 0, 0, 0);
+ await testResizer(W / 2, H, 0, 10, 0, 10);
+ await testResizer(W / 2, H, 10, -10, 0, -10);
+ await testResizer(W / 2, H, 10, 0, 0, 0);
+ await testResizer(W / 2, H, 10, 10, 0, 10);
+
+ // bottom left resizer
+ await testResizer( 0, H, -10, -10, 10, -10 * reversedGrowth);
+ await testResizer( 0, H, -10, 0, 10, 10 * needlessGrowth);
+ await testResizer( 0, H, -10, 10, 10, 10);
+ await testResizer( 0, H, 0, -10, 0, -10 * ignoredGrowth);
+ await testResizer( 0, H, 0, 0, 0, 0);
+ await testResizer( 0, H, 0, 10, 10 * needlessGrowth, 10);
+ await testResizer( 0, H, 10, -10, -10, -10);
+ await testResizer( 0, H, 10, 0, -10 * ignoredGrowth, 0);
+ await testResizer( 0, H, 10, 10, -10 * reversedGrowth, 10);
+
+ // left resizer
+ await testResizer( 0, H / 2, -10, -10, 10, 0);
+ await testResizer( 0, H / 2, -10, 0, 10, 0);
+ await testResizer( 0, H / 2, -10, 10, 10, 0);
+ await testResizer( 0, H / 2, 0, -10, 0, 0);
+ await testResizer( 0, H / 2, 0, 0, 0, 0);
+ await testResizer( 0, H / 2, 0, 10, 0, 0);
+ await testResizer( 0, H / 2, 10, -10, -10, 0);
+ await testResizer( 0, H / 2, 10, 0, -10, 0);
+ await testResizer( 0, H / 2, 10, 10, -10, 0);
+
+ // top left resizer
+ await testResizer( 0, 0, -10, -10, 10, 10);
+ await testResizer( 0, 0, -10, 0, 10, 10 * needlessGrowth);
+ await testResizer( 0, 0, -10, 10, 10, -10 * reversedGrowth);
+ await testResizer( 0, 0, 0, -10, 10 * needlessGrowth, 10);
+ await testResizer( 0, 0, 0, 0, 0, 0);
+ await testResizer( 0, 0, 0, 10, 0, -10 * ignoredGrowth);
+ await testResizer( 0, 0, 10, -10, -10 * reversedGrowth, 10);
+ await testResizer( 0, 0, 10, 0, -10 * ignoredGrowth, 0);
+ await testResizer( 0, 0, 10, 10, -10, -10);
+
+ /* eslint-enable no-multi-spaces */
+ }
+
+ const kTests = [
+ { description: "Resizers for <img>",
+ innerHTML: "<img id=\"target\" src=\"green.png\">",
+ mayPreserveRatio: true,
+ isAbsolutePosition: false,
+ },
+ { description: "Resizers for <table>",
+ innerHTML: "<table id=\"target\" border><tr><td>cell</td><td>cell</td></tr></table>",
+ mayPreserveRatio: false,
+ isAbsolutePosition: false,
+ },
+ { description: "Resizers for absolute positioned <div>",
+ innerHTML: "<div id=\"target\" style=\"position: absolute; top: 50px; left: 50px;\">positioned</div>",
+ mayPreserveRatio: false,
+ isAbsolutePosition: true,
+ },
+ ];
+
+ // Resizers for absolute positioned element and table element are available
+ // only when enableAbsolutePositionEditing or enableInlineTableEditing is
+ // enabled for each. So, let's enable them during testing resizers for
+ // absolute positioned elements or table elements.
+ for (const kTest of kTests) {
+ document.execCommand("enableAbsolutePositionEditing", false, kTest.isAbsolutePosition);
+ await doTest(kTest.description, kTest.mayPreserveRatio, kTest.innerHTML);
+ }
+ content.innerHTML = "";
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_root_element_replacement.html b/editor/libeditor/tests/test_root_element_replacement.html
new file mode 100644
index 0000000000..45765d9ab2
--- /dev/null
+++ b/editor/libeditor/tests/test_root_element_replacement.html
@@ -0,0 +1,140 @@
+<html>
+<head>
+ <title>Test for root element replacement</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display">
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+function runDesignModeTest(aDoc, aFocus, aNewSource) {
+ aDoc.designMode = "on";
+
+ if (aFocus) {
+ aDoc.documentElement.focus();
+ }
+
+ aDoc.open();
+ aDoc.write(aNewSource);
+ aDoc.close();
+ aDoc.documentElement.focus();
+}
+
+function runContentEditableTest(aDoc, aFocus, aNewSource) {
+ if (aFocus) {
+ aDoc.body.setAttribute("contenteditable", "true");
+ aDoc.body.focus();
+ }
+
+ aDoc.open();
+ aDoc.write(aNewSource);
+ aDoc.close();
+ aDoc.getElementById("focus").focus();
+}
+
+var gTestIndex = 0;
+
+const kTests = [
+ { description: "Replace to '<body></body>', designMode",
+ initializer: runDesignModeTest,
+ args: [ "<body></body>" ] },
+ { description: "Replace to '<html><body></body></html>', designMode",
+ initializer: runDesignModeTest,
+ args: [ "<html><body></body></html>" ] },
+ { description: "Replace to '<html>&nbsp;<body></body></html>', designMode",
+ initializer: runDesignModeTest,
+ args: [ "<html> <body></body></html>" ] },
+ { description: "Replace to '&nbsp;<html>&nbsp;<body></body></html>', designMode",
+ initializer: runDesignModeTest,
+ args: [ " <html> <body></body></html>" ] },
+
+ { description: "Replace to '<html contenteditable='true'><body></body></html>",
+ initializer: runContentEditableTest,
+ args: [ "<html contenteditable='true' id='focus'><body></body></html>" ] },
+ { description: "Replace to '<html><body contenteditable='true'></body></html>",
+ initializer: runContentEditableTest,
+ args: [ "<html><body contenteditable='true' id='focus'></body></html>" ] },
+ { description: "Replace to '<body contenteditable='true'></body>",
+ initializer: runContentEditableTest,
+ args: [ "<body contenteditable='true' id='focus'></body>" ] },
+];
+
+var gIFrame;
+var gSetFocusToIFrame = false;
+
+function onLoadIFrame() {
+ var frameDoc = gIFrame.contentWindow.document;
+
+ var selCon = SpecialPowers.wrap(gIFrame).contentWindow.
+ docShell.
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsISelectionDisplay).
+ QueryInterface(SpecialPowers.Ci.nsISelectionController);
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // move focus to the HTML editor
+ const kTest = kTests[gTestIndex];
+ ok(true, "Running " + kTest.description);
+ if (kTest.args.length == 1) {
+ kTest.initializer(frameDoc, gSetFocusToIFrame, kTest.args[0]);
+ ok(selCon.caretVisible, "caret isn't visible -- " + kTest.description);
+ } else {
+ ok(false, "kTests is broken at index=" + gTestIndex);
+ }
+
+ is(utils.IMEStatus, utils.IME_STATUS_ENABLED,
+ "IME isn't enabled -- " + kTest.description);
+ synthesizeKey("A", { }, gIFrame.contentWindow);
+ synthesizeKey("B", { }, gIFrame.contentWindow);
+ synthesizeKey("C", { }, gIFrame.contentWindow);
+ var content = frameDoc.body.firstChild;
+ ok(content, "body doesn't have contents -- " + kTest.description);
+ if (content) {
+ is(content.nodeType, Node.TEXT_NODE,
+ "the content of body isn't text node -- " + kTest.description);
+ if (content.nodeType == Node.TEXT_NODE) {
+ is(content.data, "ABC",
+ "the content of body text isn't 'ABC' -- " + kTest.description);
+ is(frameDoc.body.innerHTML, "ABC",
+ "the innerHTML of body isn't 'ABC' -- " + kTest.description);
+ }
+ }
+
+ document.getElementById("display").removeChild(gIFrame);
+
+ // Do next test or finish the tests.
+ if (++gTestIndex < kTests.length) {
+ setTimeout(runTest, 0);
+ } else if (!gSetFocusToIFrame) {
+ gSetFocusToIFrame = true;
+ gTestIndex = 0;
+ setTimeout(runTest, 0);
+ } else {
+ SimpleTest.finish();
+ }
+}
+
+function runTest() {
+ gIFrame = document.createElement("iframe");
+ document.getElementById("display").appendChild(gIFrame);
+ gIFrame.src = "about:blank";
+ gIFrame.onload = onLoadIFrame;
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_sanitizer_on_paste.html b/editor/libeditor/tests/test_sanitizer_on_paste.html
new file mode 100644
index 0000000000..09c6ff4fb2
--- /dev/null
+++ b/editor/libeditor/tests/test_sanitizer_on_paste.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test pasting table rows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<textarea></textarea>
+<div contenteditable="true">Paste target</div>
+<script>
+ SimpleTest.waitForExplicitFinish();
+ function fail() {
+ ok(false, "Should not run event handlers.");
+ }
+ document.addEventListener('copy', ev => {
+ dump("IN LISTENER\n");
+ const payload = `<svg><style><image href=file_sanitizer_on_paste.sjs onerror=fail() onload=fail()>`
+
+ ev.preventDefault();
+ ev.clipboardData.setData('text/html', payload);
+ ev.clipboardData.setData('text/plain', payload);
+ });
+
+ document.getElementsByTagName("textarea")[0].focus();
+ synthesizeKey("c", { accelKey: true } /* aEvent*/);
+
+ let div = document.getElementsByTagName("div")[0];
+ div.focus();
+ synthesizeKey("v", { accelKey: true } /* aEvent*/);
+
+ let svg = div.firstChild;
+ is(svg.nodeName, "svg", "Node name should be svg");
+
+ let style = svg.firstChild;
+ if (style) {
+ is(style.firstChild, null, "Style should not have child nodes.");
+ } else {
+ ok(false, "Should have gotten a node.");
+ }
+
+ var s = document.createElement("script");
+ s.src = "file_sanitizer_on_paste.sjs?report=1";
+ document.body.appendChild(s);
+</script>
+</body> \ No newline at end of file
diff --git a/editor/libeditor/tests/test_select_all_without_body.html b/editor/libeditor/tests/test_select_all_without_body.html
new file mode 100644
index 0000000000..0e8dca9f3d
--- /dev/null
+++ b/editor/libeditor/tests/test_select_all_without_body.html
@@ -0,0 +1,26 @@
+<html>
+<head>
+ <title>Test select all in HTML editor without body element</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display">
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+window.open("file_select_all_without_body.html", "_blank",
+ "width=600,height=600");
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_selection_move_commands.html b/editor/libeditor/tests/test_selection_move_commands.html
new file mode 100644
index 0000000000..cb2f769a2a
--- /dev/null
+++ b/editor/libeditor/tests/test_selection_move_commands.html
@@ -0,0 +1,225 @@
+<!doctype html>
+<title>Test for nsSelectionMoveCommands</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=454004">Mozilla Bug 454004</a>
+
+<iframe id="edit" width="200" height="100" src="about:blank"></iframe>
+
+<script>
+SimpleTest.waitForExplicitFinish();
+
+async function setup() {
+ await SpecialPowers.pushPrefEnv({set: [["general.smoothScroll", false]]});
+}
+
+async function* runTests() {
+ var e = document.getElementById("edit");
+ var doc = e.contentDocument;
+ var win = e.contentWindow;
+ var root = doc.documentElement;
+ var body = doc.body;
+
+ var sel = win.getSelection();
+
+ function testScrollCommand(cmd, expectTop) {
+ const { top } = root.getBoundingClientRect();
+
+ // XXX(krosylight): Android scrolls slightly more inside
+ // geckoview-test-verify-e10s CI job
+ const isAndroid = SpecialPowers.Services.appinfo.widgetToolkit == "android";
+ const delta = isAndroid ? .150 : 0;
+ isfuzzy(top, -expectTop, delta, cmd);
+ }
+
+ function testMoveCommand(cmd, expectNode, expectOffset) {
+ SpecialPowers.doCommand(window, cmd);
+ is(sel.isCollapsed, true, "collapsed after " + cmd);
+ is(sel.anchorNode, expectNode, "node after " + cmd);
+ is(sel.anchorOffset, expectOffset, "offset after " + cmd);
+ }
+
+ function findChildNum(element, child) {
+ var i = 0;
+ var n = element.firstChild;
+ while (n && n != child) {
+ n = n.nextSibling;
+ ++i;
+ }
+ if (!n)
+ return -1;
+ return i;
+ }
+
+ function testPageMoveCommand(cmd, expectOffset) {
+ SpecialPowers.doCommand(window, cmd);
+ is(sel.isCollapsed, true, "collapsed after " + cmd);
+ is(sel.anchorOffset, expectOffset, "offset after " + cmd);
+ return findChildNum(body, sel.anchorNode);
+ }
+
+ function testSelectCommand(cmd, expectNode, expectOffset) {
+ var anchorNode = sel.anchorNode;
+ var anchorOffset = sel.anchorOffset;
+ SpecialPowers.doCommand(window, cmd);
+ is(sel.isCollapsed, false, "not collapsed after " + cmd);
+ is(sel.anchorNode, anchorNode, "anchor not moved after " + cmd);
+ is(sel.anchorOffset, anchorOffset, "anchor not moved after " + cmd);
+ is(sel.focusNode, expectNode, "node after " + cmd);
+ is(sel.focusOffset, expectOffset, "offset after " + cmd);
+ }
+
+ function testPageSelectCommand(cmd, expectOffset) {
+ var anchorNode = sel.anchorNode;
+ var anchorOffset = sel.anchorOffset;
+ SpecialPowers.doCommand(window, cmd);
+ is(sel.isCollapsed, false, "not collapsed after " + cmd);
+ is(sel.anchorNode, anchorNode, "anchor not moved after " + cmd);
+ is(sel.anchorOffset, anchorOffset, "anchor not moved after " + cmd);
+ is(sel.focusOffset, expectOffset, "offset after " + cmd);
+ return findChildNum(body, sel.focusNode);
+ }
+
+ function node(i) {
+ var n = body.firstChild;
+ while (i > 0) {
+ n = n.nextSibling;
+ --i;
+ }
+ return n;
+ }
+
+ SpecialPowers.doCommand(window, "cmd_scrollBottom");
+ yield;
+ testScrollCommand("cmd_scrollBottom", root.scrollHeight - 100);
+ SpecialPowers.doCommand(window, "cmd_scrollTop");
+ yield;
+ testScrollCommand("cmd_scrollTop", 0);
+
+ SpecialPowers.doCommand(window, "cmd_scrollPageDown");
+ yield;
+ var pageHeight = -root.getBoundingClientRect().top;
+ ok(pageHeight > 0, "cmd_scrollPageDown works");
+ ok(pageHeight <= 100, "cmd_scrollPageDown doesn't scroll too much");
+ SpecialPowers.doCommand(window, "cmd_scrollBottom");
+ yield;
+ SpecialPowers.doCommand(window, "cmd_scrollPageUp");
+ yield;
+ testScrollCommand("cmd_scrollPageUp", root.scrollHeight - 100 - pageHeight);
+
+ SpecialPowers.doCommand(window, "cmd_scrollTop");
+ yield;
+ SpecialPowers.doCommand(window, "cmd_scrollLineDown");
+ yield;
+ var lineHeight = -root.getBoundingClientRect().top;
+ ok(lineHeight > 0, "Can scroll by lines");
+ SpecialPowers.doCommand(window, "cmd_scrollBottom");
+ yield;
+ SpecialPowers.doCommand(window, "cmd_scrollLineUp");
+ yield;
+ testScrollCommand("cmd_scrollLineUp", root.scrollHeight - 100 - lineHeight);
+
+ var runSelectionTests = function() {
+ testMoveCommand("cmd_moveBottom", body, 23);
+ testMoveCommand("cmd_moveTop", node(0), 0);
+ testSelectCommand("cmd_selectBottom", body, 23);
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testSelectCommand("cmd_selectTop", node(0), 0);
+
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ testMoveCommand("cmd_lineNext", node(2), 0);
+ testMoveCommand("cmd_linePrevious", node(0), 0);
+ testSelectCommand("cmd_selectLineNext", node(2), 0);
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testSelectCommand("cmd_selectLinePrevious", node(20), 2);
+
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testMoveCommand("cmd_charPrevious", node(22), 1);
+ testMoveCommand("cmd_charNext", node(22), 2);
+ testSelectCommand("cmd_selectCharPrevious", node(22), 1);
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ testSelectCommand("cmd_selectCharNext", node(0), 1);
+
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ testMoveCommand("cmd_endLine", node(0), 1);
+ testMoveCommand("cmd_beginLine", node(0), 0);
+ testSelectCommand("cmd_selectEndLine", node(0), 1);
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testSelectCommand("cmd_selectBeginLine", node(22), 0);
+
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ testMoveCommand("cmd_wordPrevious", node(22), 0);
+ testMoveCommand("cmd_wordNext", body, 23);
+ testSelectCommand("cmd_selectWordPrevious", node(22), 0);
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ testSelectCommand("cmd_selectWordNext", body, 1);
+
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ var lineNum = testPageMoveCommand("cmd_movePageDown", 0);
+ ok(lineNum > 0, "cmd_movePageDown works");
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ SpecialPowers.doCommand(window, "cmd_beginLine");
+ is(testPageMoveCommand("cmd_movePageUp", 0), 22 - lineNum, "cmd_movePageUp");
+
+ SpecialPowers.doCommand(window, "cmd_moveTop");
+ is(testPageSelectCommand("cmd_selectPageDown", 0), lineNum, "cmd_selectPageDown");
+ SpecialPowers.doCommand(window, "cmd_moveBottom");
+ SpecialPowers.doCommand(window, "cmd_beginLine");
+ is(testPageSelectCommand("cmd_selectPageUp", 0), 22 - lineNum, "cmd_selectPageUp");
+ };
+
+ await SpecialPowers.pushPrefEnv({set: [["layout.word_select.eat_space_to_next_word", false]]});
+ runSelectionTests();
+ await SpecialPowers.pushPrefEnv({set: [["layout.word_select.eat_space_to_next_word", true]]});
+ runSelectionTests();
+}
+
+function cleanup() {
+ SimpleTest.finish();
+}
+
+async function testRunner() {
+ var e = document.getElementById("edit");
+ var doc = e.contentDocument;
+ var win = e.contentWindow;
+ var body = doc.body;
+
+ body.style.fontSize = "16px";
+ body.style.lineHeight = "16px";
+ body.style.height = "400px";
+ body.style.padding = "0px";
+ body.style.margin = "0px";
+ body.style.borderWidth = "0px";
+
+ doc.designMode = "on";
+ body.innerHTML = "1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>10<br>11<br>12<br>";
+ win.focus();
+ // Flush out layout to make sure that the subdocument will be the size we
+ // expect by the time we try to scroll it.
+ is(body.getBoundingClientRect().height, 400,
+ "Body height should be what we set it to");
+
+ await waitToClearOutAnyPotentialScrolls(win);
+
+ let curTest = runTests();
+ while (true) {
+ let promise = new Promise(resolve => { win.addEventListener("scroll", () => { SimpleTest.executeSoon(resolve); }, {once: true, capture: true}); });
+ if ((await curTest.next()).done) {
+ break;
+ }
+ // wait for the scroll
+ await promise;
+ // clear out any other pending scrolls
+ await waitToClearOutAnyPotentialScrolls(win);
+ }
+}
+
+SimpleTest.waitForFocus(function() {
+ setup()
+ .then(() => testRunner())
+ .then(() => cleanup())
+ .catch(err => ok(false, err));
+}, window);
+
+</script>
diff --git a/editor/libeditor/tests/test_setting_value_longer_than_maxlength_with_setUserInput.html b/editor/libeditor/tests/test_setting_value_longer_than_maxlength_with_setUserInput.html
new file mode 100644
index 0000000000..5c2290c094
--- /dev/null
+++ b/editor/libeditor/tests/test_setting_value_longer_than_maxlength_with_setUserInput.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1320229
+
+Checks if `eReplaceText` has consistent behavior regardless of whether the
+field has an associated editor---this test works by calling `setUserInput()`
+before the element gets focus.)
+
+Inspired by `dom/html/test/forms/test_MozEditableElement_setUserInput.html`.
+-->
+
+<head>
+ <title>Test setting value longer than maxlength with setUserInput</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1320229">Mozilla Bug 1320229</a>
+ <div id="display">
+ </div>
+ <div id="content"></div>
+ <pre id="test">
+</pre>
+
+ <script class="testbody" type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+ // eslint-disable-next-line complexity
+ SimpleTest.waitForFocus(() => {
+ let content = document.getElementById("content");
+ for (let test of [
+ {
+ element: "input",
+ type: "password",
+ maxlength: "9",
+ input: { before: "aaaaaaaa", after: "bbbbbbbb" },
+ result: { before: "aaaaaaaa", after: "bbbbbbbb"}
+ },
+ {
+ element: "input",
+ type: "password",
+ maxlength: "4",
+ input: { before: "aaaaaaaa", after: "bbbbbbbb" },
+ result: { before: "aaaaaaaa", after: "bbbbbbbb"}
+ },
+ ]) {
+ let tag = `<${test.element} type="${test.type}" maxlength="${test.maxlength}">`
+ content.innerHTML = `${tag}`;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+
+ // Before setting focus, editor of the element may have not been created yet.
+ SpecialPowers.wrap(target).setUserInput(test.input.before);
+ is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`);
+
+ // Now we do the same after setting focus.
+ target.focus();
+ SpecialPowers.wrap(target).setUserInput(test.input.after);
+ is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`);
+ }
+
+ SimpleTest.finish();
+ });
+ </script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_spellcheck_pref.html b/editor/libeditor/tests/test_spellcheck_pref.html
new file mode 100644
index 0000000000..8b0ea7f303
--- /dev/null
+++ b/editor/libeditor/tests/test_spellcheck_pref.html
@@ -0,0 +1,22 @@
+<html>
+<head>
+ <title>Test if spellcheck is turned on</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+ is(SpecialPowers.getIntPref("layout.spellcheckDefault"), 1, "Check if the layout.spellcheckDefault pref is turned on");
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_state_change_on_reframe.html b/editor/libeditor/tests/test_state_change_on_reframe.html
new file mode 100644
index 0000000000..c74bf46ea7
--- /dev/null
+++ b/editor/libeditor/tests/test_state_change_on_reframe.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<title>Test for state change not bogusly changing during reframe (bug 1528644)</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<style>
+ .reframe { display: table }
+ input:invalid { color: red; }
+ input:valid { color: green; }
+</style>
+<form>
+ <input type="text" required minlength="4" onkeypress="this.classList.toggle('reframe')">
+</form>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let input = document.querySelector("input");
+ input.focus();
+ requestAnimationFrame(() => {
+ synthesizeKey("a");
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ ok(!input.validity.valid);
+ is(getComputedStyle(input).display, "table");
+ SimpleTest.finish();
+ });
+ });
+ });
+});
+</script>
diff --git a/editor/libeditor/tests/test_textarea_value_not_include_cr.html b/editor/libeditor/tests/test_textarea_value_not_include_cr.html
new file mode 100644
index 0000000000..f687792f8b
--- /dev/null
+++ b/editor/libeditor/tests/test_textarea_value_not_include_cr.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for HTMLTextAreaElement.value not returning value including CR</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<textarea></textarea>
+
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ /**
+ * This test should check only the cases with emulating complicated
+ * key events and using XPCOM methods. If you need to add simple textcases,
+ * use testing/web-platform/tests/html/semantics/forms/the-textarea-element/value-defaultValue-textContent.html
+ * instead
+ */
+ let textarea = document.querySelector("textarea");
+ textarea.focus();
+ // This shouldn't occur because widget handles control characters if they
+ // receive native key event, but for backward compatibility as XUL platform,
+ // let's check this.
+ synthesizeKey("\r");
+ is(textarea.value, "\n", "Inputting \\r from keyboard event should be converted to \\n");
+
+ textarea.value = "";
+ await new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(
+ "ab\ncd\nef",
+ () => { SpecialPowers.clipboardCopyString("ab\r\ncd\ref"); },
+ resolve,
+ () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ }
+ );
+ });
+ synthesizeKey("v", {accelKey: true});
+ is(textarea.value, "ab\ncd\nef", "Pasting \\r from clipboard should be converted to \\n");
+
+ textarea.value = "";
+ SpecialPowers.wrap(textarea).editor.insertText("ab\r\ncd\ref");
+ is(textarea.value, "ab\ncd\nef", "Inserting \\r with nsIEditor.insertText() should be converted to \\n");
+
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab\r\ncd\ref",
+ "clauses":
+ [
+ { "length": 9, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 9, "length": 0 },
+ });
+ is(textarea.value, "ab\ncd\nef", "Inserting \\r with composition should be converted to \\n");
+
+ synthesizeComposition({type: "compositioncommitasis"});
+ is(textarea.value, "ab\ncd\nef", "Inserting \\r with committing composition should be converted to \\n");
+
+ // We don't need to test spellchecker features on Android because of unsupported.
+ if (!navigator.appVersion.includes("Android")) {
+ ok(true, "Waiting to run spellchecker...");
+ let inlineSpellchecker = SpecialPowers.wrap(textarea).editor.getInlineSpellChecker(true);
+ textarea.value = "abx ";
+ await new Promise(resolve => {
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs"
+ );
+ onSpellCheck(textarea, () => {
+ SimpleTest.executeSoon(resolve);
+ });
+ });
+ let anonymousDivElement = SpecialPowers.wrap(textarea).editor.rootElement;
+ let misspelledWord = inlineSpellchecker.getMisspelledWord(anonymousDivElement.firstChild, 0);
+ is(misspelledWord.startOffset, 0, "Misspelled word start should be 0");
+ is(misspelledWord.endOffset, 3, "Misspelled word end should be 3");
+ inlineSpellchecker.replaceWord(anonymousDivElement.firstChild, 0, "ab\r\ncd\ref");
+ is(textarea.value, "ab\ncd\nef ", "Inserting \\r from spellchecker should be converted to \\n");
+ }
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_texteditor_keyevent_handling.html b/editor/libeditor/tests/test_texteditor_keyevent_handling.html
new file mode 100644
index 0000000000..2c80181b3c
--- /dev/null
+++ b/editor/libeditor/tests/test_texteditor_keyevent_handling.html
@@ -0,0 +1,471 @@
+<html>
+<head>
+ <title>Test for key event handler of text editor</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display">
+ <input type="text" id="inputField">
+ <input type="password" id="passwordField">
+ <textarea id="textarea"></textarea>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+var inputField = document.getElementById("inputField");
+var passwordField = document.getElementById("passwordField");
+var textarea = document.getElementById("textarea");
+
+const kIsMac = navigator.platform.includes("Mac");
+const kIsWin = navigator.platform.includes("Win");
+const kIsLinux = navigator.platform.includes("Linux");
+
+async function runTests() {
+ var fm = SpecialPowers.Services.focus;
+
+ var capturingPhase = { fired: false, prevented: false };
+ var bubblingPhase = { fired: false, prevented: false };
+
+ var listener = {
+ handleEvent: function _hv(aEvent) {
+ is(aEvent.type, "keypress", "unexpected event is handled");
+ switch (aEvent.eventPhase) {
+ case aEvent.CAPTURING_PHASE:
+ capturingPhase.fired = true;
+ capturingPhase.prevented = aEvent.defaultPrevented;
+ break;
+ case aEvent.BUBBLING_PHASE:
+ bubblingPhase.fired = true;
+ bubblingPhase.prevented = aEvent.defaultPrevented;
+ aEvent.preventDefault(); // prevent the browser default behavior
+ break;
+ default:
+ ok(false, "event is handled in unexpected phase");
+ }
+ },
+ };
+
+ function check(aDescription,
+ aFiredOnCapture, aFiredOnBubbling, aPreventedOnBubbling) {
+ function getDesciption(aExpected) {
+ return aDescription + (aExpected ? " wasn't " : " was ");
+ }
+
+ is(capturingPhase.fired, aFiredOnCapture,
+ getDesciption(aFiredOnCapture) + "fired on capture phase");
+ is(bubblingPhase.fired, aFiredOnBubbling,
+ getDesciption(aFiredOnBubbling) + "fired on bubbling phase");
+
+ // If the event is fired on bubbling phase and it was already prevented
+ // on capture phase, it must be prevented on bubbling phase too.
+ if (capturingPhase.prevented) {
+ todo(false, aDescription +
+ " was consumed already, so, we cannot test the editor behavior actually");
+ aPreventedOnBubbling = true;
+ }
+
+ is(bubblingPhase.prevented, aPreventedOnBubbling,
+ getDesciption(aPreventedOnBubbling) + "prevented on bubbling phase");
+ }
+
+ var parentElement = document.getElementById("display");
+ SpecialPowers.addSystemEventListener(parentElement, "keypress", listener,
+ true);
+ SpecialPowers.addSystemEventListener(parentElement, "keypress", listener,
+ false);
+
+ async function doTest(aElement, aDescription, aIsSingleLine, aIsReadonly) {
+ function reset(aText) {
+ capturingPhase.fired = false;
+ capturingPhase.prevented = false;
+ bubblingPhase.fired = false;
+ bubblingPhase.prevented = false;
+ aElement.value = aText;
+ }
+
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+
+ aDescription += ": ";
+
+ aElement.focus();
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement, aDescription + "failed to move focus");
+
+ // Backspace key:
+ // If native key bindings map the key combination to something, it's consumed.
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable, it consumes backspace and shift+backspace.
+ // Otherwise, editor doesn't consume the event but the native key
+ // bindings on nsTextControlFrame may consume it.
+ reset("");
+ synthesizeKey("KEY_Backspace");
+ check(aDescription + "Backspace", true, true, true);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {shiftKey: true});
+ check(aDescription + "Shift+Backspace", true, true, true);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {ctrlKey: true});
+ // Win: cmd_deleteWordBackward
+ check(aDescription + "Ctrl+Backspace",
+ true, true, aIsReadonly || kIsWin);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {altKey: true});
+ // Win: cmd_undo
+ // Mac: cmd_deleteWordBackward
+ check(aDescription + "Alt+Backspace",
+ true, true, aIsReadonly || kIsWin || kIsMac);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {metaKey: true});
+ check(aDescription + "Meta+Backspace", true, true, aIsReadonly || kIsMac);
+
+ // Delete key:
+ // If native key bindings map the key combination to something, it's consumed.
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable, delete is consumed.
+ // Otherwise, editor doesn't consume the event but the native key
+ // bindings on nsTextControlFrame may consume it.
+ reset("");
+ synthesizeKey("KEY_Delete");
+ // Linux: native handler
+ // Mac: cmd_deleteCharForward
+ check(aDescription + "Delete",
+ true, true, !aIsReadonly || kIsLinux || kIsMac);
+
+ reset("");
+ // Win: cmd_cutOrDelete
+ // Linux: cmd_cut
+ // Mac: cmd_deleteCharForward
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ check(aDescription + "Shift+Delete",
+ true, true, true);
+
+ reset("");
+ synthesizeKey("KEY_Delete", {ctrlKey: true});
+ // Win: cmd_deleteWordForward
+ // Linux: cmd_copy
+ check(aDescription + "Ctrl+Delete",
+ true, true, kIsWin || kIsLinux);
+
+ reset("");
+ synthesizeKey("KEY_Delete", {altKey: true});
+ // Mac: cmd_deleteWordForward
+ check(aDescription + "Alt+Delete",
+ true, true, kIsMac);
+
+ reset("");
+ synthesizeKey("KEY_Delete", {metaKey: true});
+ // Linux: native handler consumed.
+ check(aDescription + "Meta+Delete",
+ true, true, kIsLinux);
+
+ // XXX input.value returns "\n" when it's empty, so, we should use dummy
+ // value ("a") for the following tests.
+
+ // Return key:
+ // If editor is readonly, it doesn't consume.
+ // If editor is editable and not single line editor, it consumes Return
+ // and Shift+Return.
+ // Otherwise, editor doesn't consume the event.
+ reset("a");
+ synthesizeKey("KEY_Enter");
+ check(aDescription + "Return",
+ true, true, !aIsSingleLine && !aIsReadonly);
+ is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a",
+ aDescription + "Return");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {shiftKey: true});
+ check(aDescription + "Shift+Return",
+ true, true, !aIsSingleLine && !aIsReadonly);
+ is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a",
+ aDescription + "Shift+Return");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {ctrlKey: true});
+ check(aDescription + "Ctrl+Return", true, true, false);
+ is(aElement.value, "a", aDescription + "Ctrl+Return");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {altKey: true});
+ check(aDescription + "Alt+Return", true, true, false);
+ is(aElement.value, "a", aDescription + "Alt+Return");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {metaKey: true});
+ check(aDescription + "Meta+Return", true, true, false);
+ is(aElement.value, "a", aDescription + "Meta+Return");
+
+ // Tab key:
+ // Editor consumes tab key event unless any modifier keys are pressed.
+ reset("a");
+ synthesizeKey("KEY_Tab");
+ check(aDescription + "Tab", true, true, false);
+ is(aElement.value, "a", aDescription + "Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Tab)");
+ aElement.focus();
+
+ reset("a");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ check(aDescription + "Shift+Tab", true, true, false);
+ is(aElement.value, "a", aDescription + "Shift+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Shift+Tab)");
+
+ // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress
+ // event should never be fired.
+ reset("a");
+ synthesizeKey("KEY_Tab", {ctrlKey: true});
+ check(aDescription + "Ctrl+Tab", false, false, false);
+ is(aElement.value, "a", aDescription + "Ctrl+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Ctrl+Tab)");
+
+ reset("a");
+ synthesizeKey("KEY_Tab", {altKey: true});
+ check(aDescription + "Alt+Tab", true, true, false);
+ is(aElement.value, "a", aDescription + "Alt+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Alt+Tab)");
+
+ reset("a");
+ synthesizeKey("KEY_Tab", {metaKey: true});
+ check(aDescription + "Meta+Tab", true, true, false);
+ is(aElement.value, "a", aDescription + "Meta+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (Meta+Tab)");
+
+ // Esc key:
+ // In all cases, esc key events are not consumed
+ reset("abc");
+ synthesizeKey("KEY_Escape");
+ check(aDescription + "Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {shiftKey: true});
+ check(aDescription + "Shift+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {ctrlKey: true});
+ check(aDescription + "Ctrl+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {altKey: true});
+ check(aDescription + "Alt+Esc", true, true, false);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {metaKey: true});
+ check(aDescription + "Meta+Esc", true, true, false);
+
+ // typical typing tests:
+ reset("");
+ sendString("M");
+ check(aDescription + "M", true, true, !aIsReadonly);
+ sendString("o");
+ check(aDescription + "o", true, true, !aIsReadonly);
+ sendString("z");
+ check(aDescription + "z", true, true, !aIsReadonly);
+ sendString("i");
+ check(aDescription + "i", true, true, !aIsReadonly);
+ sendString("l");
+ check(aDescription + "l", true, true, !aIsReadonly);
+ sendString("l");
+ check(aDescription + "l", true, true, !aIsReadonly);
+ sendString("a");
+ check(aDescription + "a", true, true, !aIsReadonly);
+ sendString(" ");
+ check(aDescription + "' '", true, true, !aIsReadonly);
+ is(aElement.value, !aIsReadonly ? "Mozilla " : "",
+ aDescription + "typed \"Mozilla \"");
+
+ // typing non-BMP character:
+ async function test_typing_surrogate_pair(
+ aTestPerSurrogateKeyPress,
+ aTestIllFormedUTF16KeyValue = false
+ ) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress],
+ ["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue],
+ ],
+ });
+ reset("");
+ let events = [];
+ function pushIntoEvents(aEvent) {
+ events.push(aEvent);
+ }
+ function getEventData(aKeyboardEventOrInputEvent) {
+ if (!aKeyboardEventOrInputEvent) {
+ return "{}";
+ }
+ switch (aKeyboardEventOrInputEvent.type) {
+ case "keydown":
+ case "keypress":
+ case "keyup":
+ return `{ type: "${aKeyboardEventOrInputEvent.type}", key="${
+ aKeyboardEventOrInputEvent.key
+ }", charCode=0x${
+ aKeyboardEventOrInputEvent.charCode.toString(16).toUpperCase()
+ } }`;
+ default:
+ return `{ type: "${aKeyboardEventOrInputEvent.type}", inputType="${
+ aKeyboardEventOrInputEvent.inputType
+ }", data="${aKeyboardEventOrInputEvent.data}" }`;
+ }
+ }
+ function getEventArrayData(aEvents) {
+ if (!aEvents.length) {
+ return "[]";
+ }
+ let result = "[\n";
+ for (const e of aEvents) {
+ result += ` ${getEventData(e)}\n`;
+ }
+ return result + "]";
+ }
+ aElement.addEventListener("keydown", pushIntoEvents);
+ aElement.addEventListener("keypress", pushIntoEvents);
+ aElement.addEventListener("keyup", pushIntoEvents);
+ aElement.addEventListener("beforeinput", pushIntoEvents);
+ aElement.addEventListener("input", pushIntoEvents);
+ synthesizeKey("\uD842\uDFB7");
+ aElement.removeEventListener("keydown", pushIntoEvents);
+ aElement.removeEventListener("keypress", pushIntoEvents);
+ aElement.removeEventListener("keyup", pushIntoEvents);
+ aElement.removeEventListener("beforeinput", pushIntoEvents);
+ aElement.removeEventListener("input", pushIntoEvents);
+ const settingDescription =
+ `aTestPerSurrogateKeyPress=${
+ aTestPerSurrogateKeyPress
+ }, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`;
+ const allowIllFormedUTF16 =
+ aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue;
+
+ check(`${aDescription}, ${settingDescription}a surrogate pair`, true, true, !aIsReadonly);
+ is(
+ aElement.value,
+ !aIsReadonly ? "\uD842\uDFB7" : "",
+ `${aDescription}, ${
+ settingDescription
+ }, The typed surrogate pair should've been inserted`
+ );
+ if (aIsReadonly) {
+ is(
+ getEventArrayData(events),
+ getEventArrayData(
+ // eslint-disable-next-line no-nested-ternary
+ aTestPerSurrogateKeyPress
+ ? (
+ allowIllFormedUTF16
+ ? [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842", charCode: 0xD842 },
+ { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ : [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 },
+ { type: "keypress", key: "", charCode: 0xDFB7 },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ )
+ : [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ ),
+ `${aDescription}, ${
+ settingDescription
+ }, Typing a surrogate pair in readonly editor should not cause input events`
+ );
+ } else {
+ is(
+ getEventArrayData(events),
+ getEventArrayData(
+ // eslint-disable-next-line no-nested-ternary
+ aTestPerSurrogateKeyPress
+ ? (
+ allowIllFormedUTF16
+ ? [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842", charCode: 0xD842 },
+ { type: "beforeinput", data: "\uD842", inputType: "insertText" },
+ { type: "input", data: "\uD842", inputType: "insertText" },
+ { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 },
+ { type: "beforeinput", data: "\uDFB7", inputType: "insertText" },
+ { type: "input", data: "\uDFB7", inputType: "insertText" },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ : [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 },
+ { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" },
+ { type: "input", data: "\uD842\uDFB7", inputType: "insertText" },
+ { type: "keypress", key: "", charCode: 0xDFB7 },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ )
+ : [
+ { type: "keydown", key: "\uD842\uDFB7", charCode: 0 },
+ { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 },
+ { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" },
+ { type: "input", data: "\uD842\uDFB7", inputType: "insertText" },
+ { type: "keyup", key: "\uD842\uDFB7", charCode: 0 },
+ ]
+ ),
+ `${aDescription}, ${
+ settingDescription
+ }, Typing a surrogate pair in editor should cause input events`
+ );
+ }
+ }
+ await test_typing_surrogate_pair(true, true);
+ await test_typing_surrogate_pair(true, false);
+ await test_typing_surrogate_pair(false);
+ }
+
+ await doTest(inputField, "<input type=\"text\">", true, false);
+
+ inputField.setAttribute("readonly", "readonly");
+ await doTest(inputField, "<input type=\"text\" readonly>", true, true);
+
+ await doTest(passwordField, "<input type=\"password\">", true, false);
+
+ passwordField.setAttribute("readonly", "readonly");
+ await doTest(passwordField, "<input type=\"password\" readonly>", true, true);
+
+ await doTest(textarea, "<textarea>", false, false);
+
+ textarea.setAttribute("readonly", "readonly");
+ await doTest(textarea, "<textarea readonly>", false, true);
+
+ SpecialPowers.removeSystemEventListener(parentElement, "keypress", listener,
+ true);
+ SpecialPowers.removeSystemEventListener(parentElement, "keypress", listener,
+ false);
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/libeditor/tests/test_texteditor_textnode.html b/editor/libeditor/tests/test_texteditor_textnode.html
new file mode 100644
index 0000000000..d93f714649
--- /dev/null
+++ b/editor/libeditor/tests/test_texteditor_textnode.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for Bug 1713334</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<input id="input">
+<textarea id="textarea"></textarea>
+<script>
+"use strict";
+
+function assertChild(div, content) {
+ const name = div.parentElement.localName;
+ is(div.firstChild.nodeType, Node.TEXT_NODE, `<${name}>: The first node of the root element must be a text node`);
+ is(div.firstChild.textContent, content, `<${name}>: The content of the text node is wrong`);
+}
+
+function test(element) {
+ element.focus();
+
+ const { rootElement } = SpecialPowers.wrap(element).editor;
+ assertChild(rootElement, "");
+
+ element.value = "";
+ assertChild(rootElement, "");
+
+ element.value = "foo"
+ assertChild(rootElement, "foo");
+
+ element.value = "";
+ assertChild(rootElement, "");
+
+ element.value = "foo";
+ const selection =
+ SpecialPowers.wrap(element).
+ editor.
+ selectionController.getSelection(
+ SpecialPowers.Ci.nsISelectionController.SELECTION_NORMAL
+ );
+ selection.setBaseAndExtent(rootElement, 0, rootElement, 1);
+ document.execCommand("delete");
+ assertChild(rootElement, "");
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ test(document.all.input);
+ test(document.all.textarea);
+
+ SimpleTest.finish();
+});
+</script>
diff --git a/editor/libeditor/tests/test_texteditor_tripleclick_setvalue.html b/editor/libeditor/tests/test_texteditor_tripleclick_setvalue.html
new file mode 100644
index 0000000000..65ae2ce7e4
--- /dev/null
+++ b/editor/libeditor/tests/test_texteditor_tripleclick_setvalue.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for TextEditor triple click and SetValue</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<style>
+ body {
+ font: 1em/1 Ahem
+ }
+</style>
+<input id="input" value="foo bar baz">
+<script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(() => {
+ const { input } = document.all;
+ input.focus();
+ synthesizeMouse(input, 5, 5, { clickCount: 3 }, window);
+ is(input.selectionStart, 0, "selectionStart should be 0");
+ is(input.selectionEnd, input.value.length, "selectionEnd should be the end of the value");
+ synthesizeKey("KEY_Backspace");
+ is(input.value, "", ".value should be empty");
+
+ input.value = "hmm";
+ is(input.value, "hmm", ".value must be set");
+ SimpleTest.finish();
+ });
+</script>
diff --git a/editor/libeditor/tests/test_texteditor_wrapping_long_line.html b/editor/libeditor/tests/test_texteditor_wrapping_long_line.html
new file mode 100644
index 0000000000..14a445bbb0
--- /dev/null
+++ b/editor/libeditor/tests/test_texteditor_wrapping_long_line.html
@@ -0,0 +1,45 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1733878
+-->
+<head>
+ <meta charset="UTF-8" />
+ <title>Test for bug 1733878</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script>
+ /** Test for bug 1733878 **/
+ window.addEventListener("DOMContentLoaded", (event) => {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ document.body.textContent = ""; // It would be \n\n otherwise...
+ synthesizeMouseAtCenter(document.body, {});
+
+ var editor = getEditor();
+ is(document.body.textContent, "", "Initial body check");
+ editor.rewrap(false);
+ is(document.body.textContent, "", "Initial body check after rewrap");
+
+ document.body.innerHTML = "> hello world this_is_a_very_long_long_word_which_has_a_length_higher_than_the_max_column";
+ editor.rewrap(true);
+ is(document.body.innerText, "> hello world\n> this_is_a_very_long_long_word_which_has_a_length_higher_than_the_max_column", "Rewrapped");
+
+ SimpleTest.finish();
+ });
+ });
+
+ function getEditor() {
+ var Ci = SpecialPowers.Ci;
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ var editor = editingSession.getEditorForWindow(window);
+ editor.QueryInterface(Ci.nsIHTMLEditor);
+ editor.QueryInterface(Ci.nsIEditorMailSupport);
+ editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask;
+ return editor;
+ }
+ </script>
+</head>
+<body contenteditable></body>
+</html>
diff --git a/editor/libeditor/tests/test_typing_at_edge_of_anchor.html b/editor/libeditor/tests/test_typing_at_edge_of_anchor.html
new file mode 100644
index 0000000000..3cb1a2de2d
--- /dev/null
+++ b/editor/libeditor/tests/test_typing_at_edge_of_anchor.html
@@ -0,0 +1,476 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for typing after a link</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+ <div contenteditable style="padding: 5px;"></div>
+ <iframe srcdoc="<!doctype html><html><body style='padding: 5px;'></body></html>"></iframe>
+</div>
+
+<pre id="test">
+<script>
+
+// Currently, `Home` and `End` key press can be tested only on Windows.
+// Therefore, we need to check the running platform.
+const kCanTestHomeEndKeys =
+ navigator.platform.indexOf("Win") == 0 || navigator.appVersion.includes("Android");
+
+SimpleTest.waitForExplicitFinish();
+
+// When some tests newly fail by your change but the new result is expected by
+// testing/web-platform/tests/editing/other/typing-around-link-element-at-(non-)collapsed-selection.tentative.html,
+// it's okay to delete the test from here.
+function doTest(editor, selection, win) {
+ const kDescription =
+ editor.getAttribute("contenteditable") === null ? "designMode" : "contenteditable";
+
+ // At start
+ editor.innerHTML = "<p>abc<a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 1);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>abc<a href=\"about:blank\">XYdef</a></p>",
+ `${kDescription}: Typing X and YY at start of the <a> element should insert to the start of it when caret is moved from middle of it`);
+
+ editor.innerHTML = "<p>abc<a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("a[href]").previousSibling, 2);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>abcXY<a href=\"about:blank\">def</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element should insert to the end of the preceding text node when caret is moved from middle of the preceding text node`);
+
+ editor.innerHTML = "<p><b>abc</b><a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 1);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b>abc</b><a href=\"about:blank\">XYdef</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element should insert to the start of it when caret is moved from middle of it (following <b>)`);
+
+ editor.innerHTML = "<p><b>abc</b><a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("b").firstChild, 2);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b>abcXY</b><a href=\"about:blank\">def</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element should insert to end of the following <b> when caret is moved from middle of the <b>`);
+
+ editor.innerHTML = "<p>abc</p><p><a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 1);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>abc</p><p><a href=\"about:blank\">XYdef</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element should insert to start of it when caret is moved from middle of it (following <p>)`);
+
+ editor.innerHTML = "<p>abc</p><p><a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("p").firstChild, 3);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>abc</p><p>XY<a href=\"about:blank\">def</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element should insert to start of new text node when caret is moved from the previous paragraph`);
+
+ editor.innerHTML = "<p>abc</p><p><b><a href=\"about:blank\">def</a></b></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 1);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>abc</p><p><b><a href=\"about:blank\">XYdef</a></b></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element should insert to start of it when caret is moved from middle of it (following <p> and wrapped by <b>)`);
+
+ editor.innerHTML = "<p>abc</p><p><b><a href=\"about:blank\">def</a></b></p>";
+ selection.collapse(editor.querySelector("p").firstChild, 3);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>abc</p><p><b>XY<a href=\"about:blank\">def</a></b></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element should insert to start of wrapper <b> when caret is moved from the previous paragraph`);
+
+ editor.innerHTML = "<p>abc</p><p><a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("p").firstChild, 3);
+ synthesizeKey("KEY_Delete");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>abcXY<a href=\"about:blank\">def</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element after joining paragraphs with Delete should insert to end of the preceding text node`);
+
+ editor.innerHTML = "<p><b>abc</b></p><p><a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("b").firstChild, 3);
+ synthesizeKey("KEY_Delete");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b>abcXY</b><a href=\"about:blank\">def</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element after joining paragraphs with Delete should insert to end of the preceding <b>`);
+
+ editor.innerHTML = "<p>abc<br></p><p><a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("p").firstChild, 3);
+ synthesizeKey("KEY_Delete");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>abcXY<a href=\"about:blank\">def</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element after joining paragraphs with Delete should insert to end of the preceding text node (invisible <br>)`);
+
+ editor.innerHTML = "<p><b>abc</b><br></p><p><a href=\"about:blank\">def</a></p>";
+ selection.collapse(editor.querySelector("b").firstChild, 3);
+ synthesizeKey("KEY_Delete");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b>abcXY</b><a href=\"about:blank\">def</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element after joining paragraphs with Delete should insert to start of the preceding <b> (invisible <br>)`);
+
+ if (kCanTestHomeEndKeys) {
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 1);
+ synthesizeKey("KEY_Home");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>XY<a href=\"about:blank\">abc</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element after \`Home\` key press should insert it outside the link`);
+
+ editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 1);
+ synthesizeKey("KEY_Home");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b>XY<a href=\"about:blank\">abc</a></b></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element wrapped by <b> after \`Home\` key press should insert it outside the link but in the <b>`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 0);
+ synthesizeKey("KEY_Home");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>XY<a href=\"about:blank\">abc</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element after \`Home\` key press without selection change should insert it outside the link`);
+
+ editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 0);
+ synthesizeKey("KEY_Home");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b>XY<a href=\"about:blank\">abc</a></b></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element wrapped by <b> after \`Home\` key press without selection change should insert it outside the link but in the <b>`);
+ }
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 0);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p>XY<a href=\"about:blank\">abc</a></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element after \`ArrowLeft\` key press without selection change should insert it outside the link`);
+
+ editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 0);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b>XY<a href=\"about:blank\">abc</a></b></p>",
+ `${kDescription}: Typing X and Y at start of the <a> element wrapped by <b> after \`ArrowLeft\` key press without selection change should insert it outside the link but in the <b>`);
+
+ for (const startPos of [2, 0]) {
+ editor.innerHTML = '<p><a href="about:blank">abc</a></p>';
+ selection.collapse(editor.querySelector("a").firstChild, startPos);
+ synthesizeMouse(editor.querySelector("a"), 2, 2, {}, win);
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, '<p><a href="about:blank">XYabc</a></p>',
+ `${kDescription}: Typing X and Y after clicking left half of the first character of the link ${
+ startPos === 0 ? "(without caret move)" : ""
+ } should insert them to start of the link`);
+
+ editor.innerHTML = '<p><a href="about:blank">abc</a></p>';
+ selection.collapse(editor.querySelector("a").firstChild, startPos);
+ synthesizeMouse(editor.querySelector("a"), -2, 2, {}, win);
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, '<p>XY<a href="about:blank">abc</a></p>',
+ `${kDescription}: Typing X and Y after clicking "before" the first character of the link ${
+ startPos === 0 ? "(without caret move)" : ""
+ } should insert them to before the link`);
+
+ editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>';
+ selection.collapse(editor.querySelector("a").firstChild, startPos);
+ synthesizeMouse(editor.querySelector("a"), 2, 2, {}, win);
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, '<p><b><a href="about:blank">XYabc</a></b></p>',
+ `${kDescription}: Typing X and Y after clicking left half of the first character of the link in <b> ${
+ startPos === 0 ? "(without caret move)" : ""
+ } should insert them to start of the link`);
+
+ editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>';
+ selection.collapse(editor.querySelector("a").firstChild, startPos);
+ synthesizeMouse(editor.querySelector("a"), -2, 2, {}, win);
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, '<p><b>XY<a href="about:blank">abc</a></b></p>',
+ `${kDescription}: Typing X and Y after clicking "before" the first character of the link in <b> ${
+ startPos === 0 ? "(without caret move)" : ""
+ } should insert them to before the link`);
+ }
+
+ // At end
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a>def</p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 2);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a>def</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element should insert to the end of it when caret is moved from middle of it`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a>def</p>";
+ selection.collapse(editor.querySelector("a[href]").nextSibling, 1);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XYdef</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element should insert to the start of the following text node when caret is moved from middle of the following text node`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a><b>def</b></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 2);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a><b>def</b></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element should insert to the end of it when caret is moved from middle of it (followed by <b>)`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a><b>def</b></p>";
+ selection.collapse(editor.querySelector("b").firstChild, 1);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a><b>XYdef</b></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element should insert to start of the following <b> when caret is moved from middle of the following <b> element`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p>def</p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 2);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a></p><p>def</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element should insert to end of it when caret is moved from middle of it (followed by <p>)`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p>def</p>";
+ selection.collapse(editor.querySelector("p + p").firstChild, 0);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY</p><p>def</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element should insert to start of new text node when caret is moved from the following paragraph`);
+
+ editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p><p>def</p>";
+ selection.collapse(editor.querySelector("p + p").firstChild, 0);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a>XY</b></p><p>def</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element should insert to end of wrapper <b> when caret is moved from the following paragraph`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\"><b>abc</b></a></p><p>def</p>";
+ selection.collapse(editor.querySelector("p + p").firstChild, 0);
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\"><b>abc</b></a><b>XY</b></p><p>def</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element should insert to start of new <b> when caret is moved from the following paragraph`);
+
+ // I'm not sure whether this behavior should be changed or not, but inconsistent with the case of Backspace from start of <a href>.
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p>def</p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 3);
+ synthesizeKey("KEY_Delete");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XYdef</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Delete should insert to end of it`);
+ todo_is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a>def</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Delete should insert to end of it`);
+
+ // I'm not sure whether this behavior should be changed or not, but inconsistent with the case of Backspace from start of <a href>.
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p><b>def</b></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 3);
+ synthesizeKey("KEY_Delete");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY<b>def</b></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Delete should insert to end of it (following <p> has <b>)`);
+ todo_is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a><b>def</b></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Delete should insert to end of it (following <p> has <b>)`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p>def</p>";
+ selection.collapse(editor.querySelector("p + p").firstChild, 0);
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XYdef</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Backspace should insert to start of next text node`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p><b>def</b></p>";
+ selection.collapse(editor.querySelector("b").firstChild, 0);
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY<b>def</b></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Backspace should insert before next <b>`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a><br></p><p>def</p>";
+ selection.collapse(editor.querySelector("p + p").firstChild, 0);
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XYdef</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Backspace should insert to start of next text node (invisible <br>)`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a><br></p><p><b>def</b></p>";
+ selection.collapse(editor.querySelector("b").firstChild, 0);
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY<b>def</b></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Backspace should insert before next <b> (invisible <br>)`);
+
+ if (kCanTestHomeEndKeys) {
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 1);
+ synthesizeKey("KEY_End");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after \`End\` key press should insert it outside the link`);
+
+ editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b><br></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 1);
+ synthesizeKey("KEY_End");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ todo_is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a></b>XY<br></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> and followed by an invisible <br> after \`End\` key press should insert it outside the link but in the <b>`);
+
+ editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, 1);
+ synthesizeKey("KEY_End");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY</p>",
+ `${kDescription}: Typing X and Y at end of the <a> element after \`End\` key press without selection change should insert it outside the link`);
+
+ editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, editor.querySelector("a[href]").firstChild.length);
+ synthesizeKey("KEY_End");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a>XY</b></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> after \`End\` key press without selection change should insert it outside the link but in the <b>`);
+
+ editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b><br></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, editor.querySelector("a[href]").firstChild.length);
+ synthesizeKey("KEY_End");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ todo_is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a></b>XY<br></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> and followed by an invisible <br> after \`End\` key press without selection change should insert it outside the link but in the <b>`);
+ }
+
+ editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, editor.querySelector("a[href]").firstChild.length);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a>XY</b></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> after \`ArrowRight\` key press without selection change should insert it outside the link but in the <b>`);
+
+ editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b><br></p>";
+ selection.collapse(editor.querySelector("a[href]").firstChild, editor.querySelector("a[href]").firstChild.length);
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ todo_is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a></b>XY<br></p>",
+ `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> and followed by an invisible <br> after \`ArrowRight\` key press without selection change should insert it outside the link but in the <b>`);
+
+ for (const startPos of [1, 3]) {
+ editor.innerHTML = '<p><a href="about:blank">abc</a></p>';
+ selection.collapse(editor.querySelector("a").firstChild, startPos);
+ synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width - 1, 1, {}, win);
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, '<p><a href="about:blank">abcXY</a></p>',
+ `${kDescription}: Typing X and Y after clicking right half of the last character of the link ${
+ startPos === 3 ? "(without caret move)" : ""
+ } should insert them to end of the link`);
+
+ editor.innerHTML = '<p><a href="about:blank">abc</a></p>';
+ selection.collapse(editor.querySelector("a").firstChild, startPos);
+ synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width + 1, 1, {}, win);
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, '<p><a href="about:blank">abc</a>XY</p>',
+ `${kDescription}: Typing X and Y after clicking "after" the last character of the link ${
+ startPos === 3 ? "(without caret move)" : ""
+ } should insert them to after the link`);
+
+ editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>';
+ selection.collapse(editor.querySelector("a").firstChild, startPos);
+ synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width - 1, 1, {}, win);
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, '<p><b><a href="about:blank">abcXY</a></b></p>',
+ `${kDescription}: Typing X and Y after clicking right half of the last character of the link in <b> ${
+ startPos === 3 ? "(without caret move)" : ""
+ } should insert them to end of the link`);
+
+ editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>';
+ selection.collapse(editor.querySelector("a").firstChild, startPos);
+ synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width + 1, 1, {}, win);
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ is(editor.innerHTML, '<p><b><a href="about:blank">abc</a>XY</b></p>',
+ `${kDescription}: Typing X and Y after clicking "after" the last character of the link in <b> ${
+ startPos === 3 ? "(without caret move)" : ""
+ } should insert them to after the link`);
+ }
+
+ // at middle of link
+ editor.innerHTML = '<p><a href="about:blank">abcde</a></p>';
+ selection.collapse(editor.querySelector("a").firstChild, 0);
+ synthesizeMouseAtCenter(editor.querySelector("a"), {}, win);
+ synthesizeKey("X");
+ synthesizeKey("Y");
+ if (selection.focusOffset == 4) {
+ is(editor.innerHTML, '<p><a href="about:blank">abXYcde</a></p>',
+ `${kDescription}: Typing X and Y after clicking center of the link should insert them to the link`);
+ } else if (selection.focusOffset == 5) {
+ is(editor.innerHTML, '<p><a href="about:blank">abcXYde</a></p>',
+ `${kDescription}: Typing X and Y after clicking center of the link should insert them to the link`);
+ } else {
+ ok(false, `selection is collapsed at unexpected offset got ${selection.focusOffset} but expected 2 or 3`);
+ }
+}
+
+SimpleTest.waitForFocus(() => {
+ let editor = document.querySelector("[contenteditable]");
+ let selection = getSelection();
+ editor.focus();
+ doTest(editor, selection, window);
+
+ let iframe = document.querySelector("iframe");
+ editor = iframe.contentDocument.body;
+ selection = iframe.contentWindow.getSelection();
+ iframe.contentDocument.designMode = "on";
+ iframe.contentWindow.focus();
+ doTest(editor, selection, iframe.contentWindow);
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html b/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html
new file mode 100644
index 0000000000..09a7d63d22
--- /dev/null
+++ b/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display"></div>
+<textarea id="textarea">abc abx abc</textarea>
+<div id="contenteditable" contenteditable>abc abx abc</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(0, 1); // In a11y module
+SimpleTest.waitForFocus(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Even if `beforeinput` events for `setUserInput()` calls are not
+ // allowed to cancel, correcting the spells should be cancelable for
+ // compatibility with the other browsers.
+ ["dom.input_event.allow_to_cancel_set_user_input", false],
+ ],
+ });
+
+ let textarea = document.getElementById("textarea");
+ let textEditor = SpecialPowers.wrap(textarea).editor;
+ let contenteditable = document.getElementById("contenteditable");
+ let htmlEditor = SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
+
+ function doTest(aElement, aRootElement, aEditor, aDescription) {
+ return new Promise(resolve => {
+ let inlineSpellChecker = aEditor.getInlineSpellChecker(true);
+
+ aElement.focus();
+
+ function checkInputEvent(aEvent, aInputType, aData, aDataTransfer, aTargetRanges, aDescriptionInner) {
+ ok(aEvent instanceof InputEvent,
+ `${aDescription}"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescriptionInner}`);
+ is(aEvent.cancelable, aEvent.type === "beforeinput" && aInputType !== "",
+ `${aDescription}"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescriptionInner}`);
+ is(aEvent.bubbles, true,
+ `${aDescription}"${aEvent.type}" event should always bubble ${aDescriptionInner}`);
+ is(aEvent.inputType, aInputType,
+ `${aDescription}inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescriptionInner}`);
+ is(aEvent.data, aData,
+ `${aDescription}data of "${aEvent.type}" event should be ${aData} ${aDescriptionInner}`);
+ if (aDataTransfer === null) {
+ is(aEvent.dataTransfer, null,
+ `${aDescription}dataTransfer of "${aEvent.type}" event should be null ${aDescriptionInner}`);
+ } else {
+ for (let item of aDataTransfer) {
+ is(aEvent.dataTransfer.getData(item.type), item.data,
+ `${aDescription}dataTransfer of "${aEvent.type}" event should have ${item.data} as ${item.type} ${aDescriptionInner}`);
+ }
+ }
+ let targetRanges = aEvent.getTargetRanges();
+ if (aTargetRanges.length === 0) {
+ is(targetRanges.length, 0,
+ `${aDescription}getTargetRange() of "${aEvent.type}" event should return empty array ${aDescriptionInner}`);
+ } else {
+ is(targetRanges.length, aTargetRanges.length,
+ `${aDescription}getTargetRange() of "${aEvent.type}" event should return static range array ${aDescriptionInner}`);
+ 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 ${aDescriptionInner}`);
+ is(targetRanges[i].startOffset, aTargetRanges[i].startOffset,
+ `${aDescription}startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match ${aDescriptionInner}`);
+ is(targetRanges[i].endContainer, aTargetRanges[i].endContainer,
+ `${aDescription}endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match ${aDescriptionInner}`);
+ is(targetRanges[i].endOffset, aTargetRanges[i].endOffset,
+ `${aDescription}endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match ${aDescriptionInner}`);
+ }
+ }
+ }
+ }
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+
+ function getValue() {
+ return aElement === textarea ? aElement.value : aElement.innerHTML;
+ }
+
+ const { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs"
+ );
+ maybeOnSpellCheck(aElement, () => {
+ SimpleTest.executeSoon(() => {
+ aElement.addEventListener("beforeinput", onBeforeInput);
+ aElement.addEventListener("input", onInput);
+
+ let misspelledWord = inlineSpellChecker.getMisspelledWord(aRootElement.firstChild, 5);
+ is(misspelledWord.startOffset, 4,
+ `${aDescription}Misspelled word should start from 4`);
+ is(misspelledWord.endOffset, 7,
+ `${aDescription}Misspelled word should end at 7`);
+ beforeInputEvents = [];
+ inputEvents = [];
+ inlineSpellChecker.replaceWord(aRootElement.firstChild, 5, "aux");
+ is(getValue(), "abc aux abc",
+ `${aDescription}'abx' should be replaced with 'aux'`);
+ is(beforeInputEvents.length, 1,
+ `${aDescription}Only one "beforeinput" event should be fired when replacing a word with spellchecker`);
+ if (aElement === textarea) {
+ checkInputEvent(beforeInputEvents[0], "insertReplacementText", "aux", null, [],
+ "when replacing a word with spellchecker");
+ } else {
+ checkInputEvent(beforeInputEvents[0], "insertReplacementText", null, [{type: "text/plain", data: "aux"}],
+ [{startContainer: aRootElement.firstChild, startOffset: 4,
+ endContainer: aRootElement.firstChild, endOffset: 7}],
+ "when replacing a word with spellchecker");
+ }
+ is(inputEvents.length, 1,
+ `${aDescription}Only one "input" event should be fired when replacing a word with spellchecker`);
+ if (aElement === textarea) {
+ checkInputEvent(inputEvents[0], "insertReplacementText", "aux", null, [],
+ "when replacing a word with spellchecker");
+ } else {
+ checkInputEvent(inputEvents[0], "insertReplacementText", null, [{type: "text/plain", data: "aux"}], [],
+ "when replacing a word with spellchecker");
+ }
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeKey("z", { accelKey: true });
+ is(getValue(), "abc abx abc",
+ `${aDescription}'abx' should be restored by undo`);
+ is(beforeInputEvents.length, 1,
+ `${aDescription}Only one "beforeinput" event should be fired when undoing the replacing word`);
+ checkInputEvent(beforeInputEvents[0], "historyUndo", null, null, [],
+ "when undoing the replacing word");
+ is(inputEvents.length, 1,
+ `${aDescription}Only one "input" event should be fired when undoing the replacing word`);
+ checkInputEvent(inputEvents[0], "historyUndo", null, null, [],
+ "when undoing the replacing word");
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeKey("z", { accelKey: true, shiftKey: true });
+ is(getValue(), "abc aux abc",
+ `${aDescription}'aux' should be restored by redo`);
+ is(beforeInputEvents.length, 1,
+ `${aDescription}Only one "beforeinput" event should be fired when redoing the replacing word`);
+ checkInputEvent(beforeInputEvents[0], "historyRedo", null, null, [],
+ "when redoing the replacing word");
+ is(inputEvents.length, 1,
+ `${aDescription}Only one "input" event should be fired when redoing the replacing word`);
+ checkInputEvent(inputEvents[0], "historyRedo", null, null, [],
+ "when redoing the replacing word");
+
+ aElement.removeEventListener("beforeinput", onBeforeInput);
+ aElement.removeEventListener("input", onInput);
+
+ resolve();
+ });
+ });
+ });
+ }
+
+ await doTest(textarea, textEditor.rootElement, textEditor, "<textarea>: ");
+ await doTest(contenteditable, contenteditable, htmlEditor, "<div contenteditable>: ");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html b/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html
new file mode 100644
index 0000000000..6f33ccaf01
--- /dev/null
+++ b/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1473515
+-->
+<html>
+<head>
+ <title>Test for Bug 1473515</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1473515">Mozilla Bug 1473515</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<input id="input">
+<textarea id="textarea"></textarea>
+
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(0, 1); // In a11y module
+SimpleTest.waitForFocus(() => {
+ let editableElements = [
+ document.getElementById("input"),
+ document.getElementById("textarea"),
+ ];
+ for (let editableElement of editableElements) {
+ function checkInputEvent(aEvent, aInputType, aData, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, aEvent.type === "beforeinput",
+ `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"${aEvent.type}" event should always bubble ${aDescription}`);
+ is(aEvent.inputType, aInputType,
+ `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`);
+ is(aEvent.data, aData,
+ `data of "${aEvent.type}" event should be ${aData} ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "${aEvent.type}" should be null ${aDescription}`);
+ is(aEvent.getTargetRanges().length, 0,
+ `getTargetRanges() of "${aEvent.type}" should return empty array ${aDescription}`);
+ }
+
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ editableElement.addEventListener("beforeinput", onBeforeInput);
+ editableElement.addEventListener("input", onInput);
+
+ editableElement.focus();
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeKey("a");
+ is(beforeInputEvents.length, 1,
+ `Only one "beforeinput" event should be fired when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(beforeInputEvents[0], "insertText", "a", `when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be fired when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(inputEvents[0], "insertText", "a", `when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeKey("c");
+ is(beforeInputEvents.length, 1,
+ `Only one "beforeinput" event should be fired when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(beforeInputEvents[0], "insertText", "c", `when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be fired when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(inputEvents[0], "insertText", "c", `when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeKey("KEY_ArrowLeft");
+ is(beforeInputEvents.length, 0,
+ `No "beforeinput" event should be fired when pressing "ArrowLeft" key on <${editableElement.tagName.toLowerCase()}> element`);
+ is(inputEvents.length, 0,
+ `No "input" event should be fired when pressing "ArrowLeft" key on <${editableElement.tagName.toLowerCase()}> element`);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeKey("b");
+ is(beforeInputEvents.length, 1,
+ `Only one "beforeinput" event should be fired when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(beforeInputEvents[0], "insertText", "b", `when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be fired when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(inputEvents[0], "insertText", "b", `when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
+
+ let editor = SpecialPowers.wrap(editableElement).editor;
+ is(
+ editor.canUndo,
+ true,
+ `${editableElement.tagName}: Initially, the editor should have undo transactions`
+ );
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ editableElement.value = "def";
+ is(beforeInputEvents.length, 0,
+ `No "beforeinput" event should be fired when setting value of <${editableElement.tagName.toLowerCase()}> element`);
+ is(inputEvents.length, 0,
+ `No "input" event should be fired when setting value of <${editableElement.tagName.toLowerCase()}> element`);
+
+ is(
+ editor.canUndo,
+ false,
+ `${editableElement.tagName}: After setting value, the editor should not have undo transactions`
+ );
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeKey("a");
+ is(beforeInputEvents.length, 1,
+ `Only one "beforeinput" event should be fired when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(beforeInputEvents[0], "insertText", "a", `when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be fired when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(inputEvents[0], "insertText", "a", `when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ synthesizeKey("z", { accelKey: true });
+ is(editableElement.value, "def",
+ editableElement.tagName + ": undo should work after setting value");
+ is(beforeInputEvents.length, 1,
+ `Only one "beforeinput" event should be fired when undoing on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(beforeInputEvents[0], "historyUndo", null, `when undoing on <${editableElement.tagName.toLowerCase()}> element`);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be fired when undoing on <${editableElement.tagName.toLowerCase()}> element`);
+ checkInputEvent(inputEvents[0], "historyUndo", null, `when undoing on <${editableElement.tagName.toLowerCase()}> element`);
+
+ // Disable undo/redo.
+ editor.enableUndo(false);
+ is(
+ editor.canUndo,
+ false,
+ `${editableElement.tagName}: After calling enableUndo(false), the editor should not have undo transactions`
+ );
+ editableElement.value = "hij";
+ is(
+ editor.canUndo,
+ false,
+ `${editableElement.tagName}: After calling enableUndo(false), the editor should not create undo transaction for setting value`
+ );
+
+ editableElement.removeEventListener("beforeinput", onBeforeInput);
+ editableElement.removeEventListener("input", onInput);
+ }
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/libeditor/tests/test_undo_with_editingui.html b/editor/libeditor/tests/test_undo_with_editingui.html
new file mode 100644
index 0000000000..fe3565880e
--- /dev/null
+++ b/editor/libeditor/tests/test_undo_with_editingui.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for undo with editing UI</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="editable1" contenteditable="true">
+<table id="table1" border="1">
+<tr id="tr1"><td>ABCDEFG</td><td>HIJKLMN</td></tr>
+<tr id="tr2"><td>ABCDEFG</td><td>HIJKLMN</td></tr>
+</table>
+<div id="edit1">test</div>
+<img id="img1" src="green.png">
+<div id="abs1" style="position: absolute; top: 100px; left: 300px; width: 100px; height: 100px; background-color: green;"></div>
+</div>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+add_task(async function testAbsPosUI() {
+ await new Promise((resolve) => {
+ SimpleTest.waitForFocus(() => {
+ SimpleTest.executeSoon(resolve);
+ }, window);
+ });
+
+ document.execCommand("enableAbsolutePositionEditing", false, "true");
+ ok(document.queryCommandState("enableAbsolutePositionEditing"),
+ "Enable absolute positioned editor");
+
+ let edit1 = document.getElementById("edit1");
+ edit1.innerText = "test";
+ synthesizeMouseAtCenter(edit1, {});
+ synthesizeKey("a");
+ isnot(edit1.firstChild.textContent, "test", "Text is modified");
+ let abs1 = document.getElementById("abs1");
+ ok(!abs1.hasAttribute("_moz_abspos"), "_moz_abspos attribute should be false yet");
+
+ let promiseForAbsEditor = new Promise((resolve) => {
+ document.addEventListener("selectionchange", () => {
+ resolve();
+ }, {once: true});
+ });
+ synthesizeMouseAtCenter(abs1, {});
+ await promiseForAbsEditor;
+ ok(abs1.hasAttribute("_moz_abspos"), "_moz_abspos attribute should be true");
+
+ synthesizeKey("z", { accelKey: true });
+ is(edit1.firstChild.textContent, "test", "Text is restored by undo");
+
+ // TODO: no good way to move absolute position grab.
+
+ document.execCommand("enableAbsolutePositionEditing", false, "false");
+});
+
+add_task(function testResizerUI() {
+ document.execCommand("enableObjectResizing", false, "true");
+ ok(document.queryCommandState("enableObjectResizing"),
+ "Enable object resizing editor");
+
+ let edit1 = document.getElementById("edit1");
+ edit1.innerText = "test";
+ synthesizeMouseAtCenter(edit1, {});
+ synthesizeKey("h");
+ isnot(edit1.firstChild.textContent, "test", "Text is modified");
+
+ let img1 = document.getElementById("img1");
+ synthesizeMouseAtCenter(img1, {});
+ ok(img1.hasAttribute("_moz_resizing"),
+ "_moz_resizing attribute should be true");
+
+ synthesizeKey("z", { accelKey: true });
+ is(edit1.firstChild.textContent, "test", "Text is restored by undo");
+
+ // Resizer
+
+ synthesizeMouseAtCenter(edit1, {});
+ synthesizeKey("j");
+ isnot(edit1.firstChild.textContent, "test", "Text is modified");
+
+ synthesizeMouseAtCenter(img1, {});
+ ok(img1.hasAttribute("_moz_resizing"),
+ "_moz_resizing attribute should be true");
+
+ // Emulate drag & drop
+ let origWidth = img1.width;
+ let posX = img1.clientWidth;
+ let posY = img1.clientHeight - (img1.height / 2);
+ synthesizeMouse(img1, posX, posY, {type: "mousedown"});
+ synthesizeMouse(img1, posX + 100, posY, {type: "mousemove"});
+ synthesizeMouse(img1, posX + 100, posY, {type: "mouseup"});
+
+ isnot(img1.width, origWidth, "Image is resized");
+ synthesizeKey("z", { accelKey: true });
+ is(img1.width, origWidth, "Image width is restored by undo");
+
+ synthesizeKey("z", { accelKey: true });
+ is(edit1.firstChild.textContent, "test", "Text is restored by undo");
+
+ document.execCommand("enableObjectResizing", false, "false");
+});
+
+add_task(async function testInlineTableUI() {
+ document.execCommand("enableInlineTableEditing", false, "true");
+ ok(document.queryCommandState("enableInlineTableEditing"),
+ "Enable Inline Table editor");
+
+ let tr1 = document.getElementById("tr1");
+ synthesizeMouseAtCenter(tr1, {});
+ synthesizeKey("o");
+ isnot(tr1.firstChild.firstChild.textContent, "ABCDEFG",
+ "Text is modified");
+
+ let tr2 = document.getElementById("tr2");
+ synthesizeMouseAtCenter(tr2, {});
+ synthesizeKey("y");
+ isnot(tr2.firstChild.firstChild.textContent, "ABCDEFG",
+ "Text is modified");
+
+ synthesizeKey("z", { accelKey: true });
+ is(tr2.firstChild.firstChild.textContent, "ABCDEFG",
+ "Text is restored by undo");
+
+ synthesizeKey("z", { accelKey: true });
+ is(tr1.firstChild.firstChild.textContent, "ABCDEFG",
+ "Text is restored by undo");
+
+ synthesizeMouseAtCenter(tr1, {});
+ synthesizeKey("p");
+ isnot(tr1.firstChild.firstChild.textContent, "ABCDEFG",
+ "Text is modified");
+
+ // Inline table editing UI
+
+ synthesizeMouseAtCenter(tr2, {});
+ synthesizeMouse(tr2, 0, tr2.clientHeight / 2, {});
+ ok(!document.getElementById("tr2"),
+ "id=tr2 should be removed by a click in the row");
+
+ synthesizeKey("z", { accelKey: true });
+ ok(document.getElementById("tr2"), "id=tr2 should be restored by undo");
+
+ synthesizeKey("z", { accelKey: true });
+ is(tr1.firstChild.firstChild.textContent, "ABCDEFG",
+ "Text is restored by undo");
+
+ document.execCommand("enableInlineTableEditing", false, "false");
+});
+
+</script>
+</pre>
+</body>
+</html>