summaryrefslogtreecommitdiffstats
path: root/editor
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /editor
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'editor')
-rw-r--r--editor/AsyncSpellCheckTestHelper.sys.mjs125
-rw-r--r--editor/composer/ComposerCommandsUpdater.cpp199
-rw-r--r--editor/composer/ComposerCommandsUpdater.h132
-rw-r--r--editor/composer/crashtests/351236-1.html37
-rw-r--r--editor/composer/crashtests/407062-1.html20
-rw-r--r--editor/composer/crashtests/419563-1.xhtml20
-rw-r--r--editor/composer/crashtests/428844-1-inner.xhtml4
-rw-r--r--editor/composer/crashtests/428844-1.html17
-rw-r--r--editor/composer/crashtests/461049-1.html25
-rw-r--r--editor/composer/crashtests/crashtests.list6
-rw-r--r--editor/composer/crashtests/removing-editable-xslt-inner.xhtml4
-rw-r--r--editor/composer/crashtests/removing-editable-xslt.html17
-rw-r--r--editor/composer/moz.build60
-rw-r--r--editor/composer/nsEditingSession.cpp1301
-rw-r--r--editor/composer/nsEditingSession.h172
-rw-r--r--editor/composer/nsIEditingSession.idl84
-rw-r--r--editor/composer/res/EditorOverride.css322
-rw-r--r--editor/composer/res/grabber.gifbin0 -> 858 bytes
-rw-r--r--editor/composer/res/table-add-column-after-active.gifbin0 -> 58 bytes
-rw-r--r--editor/composer/res/table-add-column-after-hover.gifbin0 -> 826 bytes
-rw-r--r--editor/composer/res/table-add-column-after.gifbin0 -> 826 bytes
-rw-r--r--editor/composer/res/table-add-column-before-active.gifbin0 -> 57 bytes
-rw-r--r--editor/composer/res/table-add-column-before-hover.gifbin0 -> 825 bytes
-rw-r--r--editor/composer/res/table-add-column-before.gifbin0 -> 825 bytes
-rw-r--r--editor/composer/res/table-add-row-after-active.gifbin0 -> 57 bytes
-rw-r--r--editor/composer/res/table-add-row-after-hover.gifbin0 -> 826 bytes
-rw-r--r--editor/composer/res/table-add-row-after.gifbin0 -> 826 bytes
-rw-r--r--editor/composer/res/table-add-row-before-active.gifbin0 -> 57 bytes
-rw-r--r--editor/composer/res/table-add-row-before-hover.gifbin0 -> 825 bytes
-rw-r--r--editor/composer/res/table-add-row-before.gifbin0 -> 825 bytes
-rw-r--r--editor/composer/res/table-remove-column-active.gifbin0 -> 835 bytes
-rw-r--r--editor/composer/res/table-remove-column-hover.gifbin0 -> 841 bytes
-rw-r--r--editor/composer/res/table-remove-column.gifbin0 -> 841 bytes
-rw-r--r--editor/composer/res/table-remove-row-active.gifbin0 -> 835 bytes
-rw-r--r--editor/composer/res/table-remove-row-hover.gifbin0 -> 841 bytes
-rw-r--r--editor/composer/res/table-remove-row.gifbin0 -> 841 bytes
-rw-r--r--editor/composer/test/chrome.ini5
-rw-r--r--editor/composer/test/file_bug1453190.html12
-rw-r--r--editor/composer/test/mochitest.ini9
-rw-r--r--editor/composer/test/test_bug1266815.html97
-rw-r--r--editor/composer/test/test_bug1453190.html36
-rw-r--r--editor/composer/test/test_bug348497.html36
-rw-r--r--editor/composer/test/test_bug384147.html203
-rw-r--r--editor/composer/test/test_bug389350.html33
-rw-r--r--editor/composer/test/test_bug434998.xhtml106
-rw-r--r--editor/composer/test/test_bug519928.html119
-rw-r--r--editor/composer/test/test_bug738440.html37
-rw-r--r--editor/docs/ChangJie.pngbin0 -> 544 bytes
-rw-r--r--editor/docs/EditorModuleSpecificRules.rst215
-rw-r--r--editor/docs/EditorModuleStructure.rst219
-rw-r--r--editor/docs/IMEHandlingGuide.rst1092
-rw-r--r--editor/docs/candidatewindow.pngbin0 -> 5667 bytes
-rw-r--r--editor/docs/converted_composition_string.pngbin0 -> 1260 bytes
-rw-r--r--editor/docs/index.rst12
-rw-r--r--editor/docs/inputting_composition_string.pngbin0 -> 1303 bytes
-rw-r--r--editor/docs/raw_composition_string.pngbin0 -> 1302 bytes
-rw-r--r--editor/libeditor/AutoRangeArray.cpp1195
-rw-r--r--editor/libeditor/AutoRangeArray.h469
-rw-r--r--editor/libeditor/CSSEditUtils.cpp1320
-rw-r--r--editor/libeditor/CSSEditUtils.h389
-rw-r--r--editor/libeditor/ChangeAttributeTransaction.cpp143
-rw-r--r--editor/libeditor/ChangeAttributeTransaction.h91
-rw-r--r--editor/libeditor/ChangeStyleTransaction.cpp342
-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.h862
-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.cpp7065
-rw-r--r--editor/libeditor/EditorBase.h2963
-rw-r--r--editor/libeditor/EditorCommands.cpp981
-rw-r--r--editor/libeditor/EditorCommands.h906
-rw-r--r--editor/libeditor/EditorController.cpp140
-rw-r--r--editor/libeditor/EditorController.h30
-rw-r--r--editor/libeditor/EditorDOMPoint.h1586
-rw-r--r--editor/libeditor/EditorEventListener.cpp1187
-rw-r--r--editor/libeditor/EditorEventListener.h128
-rw-r--r--editor/libeditor/EditorForwards.h169
-rw-r--r--editor/libeditor/EditorUtils.cpp474
-rw-r--r--editor/libeditor/EditorUtils.h483
-rw-r--r--editor/libeditor/HTMLAbsPositionEditor.cpp993
-rw-r--r--editor/libeditor/HTMLAnonymousNodeEditor.cpp611
-rw-r--r--editor/libeditor/HTMLEditHelpers.cpp176
-rw-r--r--editor/libeditor/HTMLEditHelpers.h1209
-rw-r--r--editor/libeditor/HTMLEditSubActionHandler.cpp11794
-rw-r--r--editor/libeditor/HTMLEditUtils.cpp2514
-rw-r--r--editor/libeditor/HTMLEditUtils.h2559
-rw-r--r--editor/libeditor/HTMLEditor.cpp7199
-rw-r--r--editor/libeditor/HTMLEditor.h4696
-rw-r--r--editor/libeditor/HTMLEditorCommands.cpp1302
-rw-r--r--editor/libeditor/HTMLEditorController.cpp142
-rw-r--r--editor/libeditor/HTMLEditorController.h26
-rw-r--r--editor/libeditor/HTMLEditorDataTransfer.cpp4325
-rw-r--r--editor/libeditor/HTMLEditorDeleteHandler.cpp6854
-rw-r--r--editor/libeditor/HTMLEditorDocumentCommands.cpp482
-rw-r--r--editor/libeditor/HTMLEditorEventListener.cpp442
-rw-r--r--editor/libeditor/HTMLEditorEventListener.h100
-rw-r--r--editor/libeditor/HTMLEditorInlines.h144
-rw-r--r--editor/libeditor/HTMLEditorNestedClasses.h522
-rw-r--r--editor/libeditor/HTMLEditorObjectResizer.cpp1495
-rw-r--r--editor/libeditor/HTMLEditorState.cpp656
-rw-r--r--editor/libeditor/HTMLInlineTableEditor.cpp493
-rw-r--r--editor/libeditor/HTMLStyleEditor.cpp4430
-rw-r--r--editor/libeditor/HTMLTableEditor.cpp4600
-rw-r--r--editor/libeditor/InsertNodeTransaction.cpp184
-rw-r--r--editor/libeditor/InsertNodeTransaction.h91
-rw-r--r--editor/libeditor/InsertTextTransaction.cpp182
-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.cpp212
-rw-r--r--editor/libeditor/JoinNodesTransaction.h112
-rw-r--r--editor/libeditor/JoinSplitNodeDirection.h51
-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.cpp621
-rw-r--r--editor/libeditor/SelectionState.h591
-rw-r--r--editor/libeditor/SplitNodeTransaction.cpp241
-rw-r--r--editor/libeditor/SplitNodeTransaction.h97
-rw-r--r--editor/libeditor/TextEditSubActionHandler.cpp817
-rw-r--r--editor/libeditor/TextEditor.cpp1140
-rw-r--r--editor/libeditor/TextEditor.h614
-rw-r--r--editor/libeditor/TextEditorDataTransfer.cpp273
-rw-r--r--editor/libeditor/WSRunObject.cpp4390
-rw-r--r--editor/libeditor/WSRunObject.h1633
-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/.eslintrc.js9
-rw-r--r--editor/libeditor/tests/browser.ini8
-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/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.js1867
-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.ini59
-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.ini20
-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.html8
-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.ini353
-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_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.html620
-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_bug620906.html50
-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_focus.html335
-rw-r--r--editor/libeditor/tests/test_contenteditable_text_input_handling.html315
-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_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.html676
-rw-r--r--editor/libeditor/tests/test_htmleditor_tab_key_handling.html112
-rw-r--r--editor/libeditor/tests/test_initial_selection_and_caret_of_designMode.html57
-rw-r--r--editor/libeditor/tests/test_inlineTableEditing.html45
-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.html172
-rw-r--r--editor/libeditor/tests/test_insertParagraph_in_inline_editing_host.html67
-rw-r--r--editor/libeditor/tests/test_join_split_node_direction_change_command.html197
-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_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_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_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.html218
-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.html349
-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
-rw-r--r--editor/moz.build41
-rw-r--r--editor/nsIDocumentStateListener.idl32
-rw-r--r--editor/nsIEditActionListener.idl91
-rw-r--r--editor/nsIEditor.idl678
-rw-r--r--editor/nsIEditorMailSupport.idl48
-rw-r--r--editor/nsIEditorSpellCheck.idl165
-rw-r--r--editor/nsIHTMLAbsPosEditor.idl31
-rw-r--r--editor/nsIHTMLEditor.idl342
-rw-r--r--editor/nsIHTMLInlineTableEditor.idl20
-rw-r--r--editor/nsIHTMLObjectResizer.idl37
-rw-r--r--editor/nsITableEditor.idl460
-rw-r--r--editor/reftests/1088158-ref.html2
-rw-r--r--editor/reftests/1088158.html8
-rw-r--r--editor/reftests/1443902-1-ref.html15
-rw-r--r--editor/reftests/1443902-1.html16
-rw-r--r--editor/reftests/1443902-2-ref.html17
-rw-r--r--editor/reftests/1443902-2.html18
-rw-r--r--editor/reftests/1443902-3-ref.html17
-rw-r--r--editor/reftests/1443902-3.html18
-rw-r--r--editor/reftests/1443902-4-ref.html18
-rw-r--r--editor/reftests/1443902-4.html19
-rw-r--r--editor/reftests/338427-1-ref.html7
-rw-r--r--editor/reftests/338427-1.html7
-rw-r--r--editor/reftests/338427-2-ref.html19
-rw-r--r--editor/reftests/338427-2.html18
-rw-r--r--editor/reftests/338427-3-ref.html19
-rw-r--r--editor/reftests/338427-3.html19
-rw-r--r--editor/reftests/388980-1-ref.html25
-rw-r--r--editor/reftests/388980-1.html43
-rw-r--r--editor/reftests/462758-grabbers-resizers-ref.html34
-rw-r--r--editor/reftests/462758-grabbers-resizers.html33
-rw-r--r--editor/reftests/642800-iframe.html29
-rw-r--r--editor/reftests/642800-ref.html7
-rw-r--r--editor/reftests/642800.html18
-rw-r--r--editor/reftests/672709-ref.html22
-rw-r--r--editor/reftests/672709.html12
-rw-r--r--editor/reftests/674212-spellcheck-ref.html20
-rw-r--r--editor/reftests/674212-spellcheck.html20
-rw-r--r--editor/reftests/694880-1.html10
-rw-r--r--editor/reftests/694880-2.html11
-rw-r--r--editor/reftests/694880-3.html10
-rw-r--r--editor/reftests/694880-ref.html9
-rw-r--r--editor/reftests/824080-1-ref.html17
-rw-r--r--editor/reftests/824080-1.html19
-rw-r--r--editor/reftests/824080-2-ref.html22
-rw-r--r--editor/reftests/824080-2.html22
-rw-r--r--editor/reftests/824080-3-ref.html21
-rw-r--r--editor/reftests/824080-3.html21
-rw-r--r--editor/reftests/824080-4-ref.html21
-rw-r--r--editor/reftests/824080-4.html26
-rw-r--r--editor/reftests/824080-5-ref.html22
-rw-r--r--editor/reftests/824080-5.html25
-rw-r--r--editor/reftests/824080-6-ref.html18
-rw-r--r--editor/reftests/824080-6.html20
-rw-r--r--editor/reftests/824080-7-ref.html19
-rw-r--r--editor/reftests/824080-7.html22
-rw-r--r--editor/reftests/911201-ref.html2
-rw-r--r--editor/reftests/911201.html2
-rw-r--r--editor/reftests/969773-ref.html24
-rw-r--r--editor/reftests/969773.html29
-rw-r--r--editor/reftests/997805-ref.html2
-rw-r--r--editor/reftests/997805.html16
-rw-r--r--editor/reftests/caret_after_reframe-ref.html6
-rw-r--r--editor/reftests/caret_after_reframe.html14
-rw-r--r--editor/reftests/caret_on_focus-ref.html11
-rw-r--r--editor/reftests/caret_on_focus.html11
-rw-r--r--editor/reftests/caret_on_positioned-ref.html8
-rw-r--r--editor/reftests/caret_on_positioned.html8
-rw-r--r--editor/reftests/caret_on_presshell_reinit-2.html27
-rw-r--r--editor/reftests/caret_on_presshell_reinit-ref.html19
-rw-r--r--editor/reftests/caret_on_presshell_reinit.html22
-rw-r--r--editor/reftests/caret_on_textarea_lastline-ref.html13
-rw-r--r--editor/reftests/caret_on_textarea_lastline.html14
-rw-r--r--editor/reftests/dynamic-1.html9
-rw-r--r--editor/reftests/dynamic-overflow-change-ref.html13
-rw-r--r--editor/reftests/dynamic-overflow-change.html13
-rw-r--r--editor/reftests/dynamic-ref.html6
-rw-r--r--editor/reftests/dynamic-type-1.html11
-rw-r--r--editor/reftests/dynamic-type-2.html11
-rw-r--r--editor/reftests/dynamic-type-3.html11
-rw-r--r--editor/reftests/dynamic-type-4.html11
-rw-r--r--editor/reftests/emptypasswd-1.html6
-rw-r--r--editor/reftests/emptypasswd-2.html9
-rw-r--r--editor/reftests/emptypasswd-ref.html6
-rw-r--r--editor/reftests/exec-command-indent-ws-ref.html61
-rw-r--r--editor/reftests/exec-command-indent-ws.html81
-rw-r--r--editor/reftests/inline-table-editor-position-after-updating-table-size-from-input-event-listener-ref.html26
-rw-r--r--editor/reftests/inline-table-editor-position-after-updating-table-size-from-input-event-listener.html33
-rw-r--r--editor/reftests/input-text-notheme-onfocus-reframe-ref.html28
-rw-r--r--editor/reftests/input-text-notheme-onfocus-reframe.html32
-rw-r--r--editor/reftests/input-text-onfocus-reframe-ref.html25
-rw-r--r--editor/reftests/input-text-onfocus-reframe.html29
-rw-r--r--editor/reftests/newline-1.html6
-rw-r--r--editor/reftests/newline-2.html6
-rw-r--r--editor/reftests/newline-3.html6
-rw-r--r--editor/reftests/newline-4.html6
-rw-r--r--editor/reftests/newline-ref.html6
-rw-r--r--editor/reftests/nobogusnode-1.html6
-rw-r--r--editor/reftests/nobogusnode-2.html5
-rw-r--r--editor/reftests/nobogusnode-ref.html6
-rw-r--r--editor/reftests/passwd-1.html6
-rw-r--r--editor/reftests/passwd-2.html6
-rw-r--r--editor/reftests/passwd-3.html9
-rw-r--r--editor/reftests/passwd-4.html19
-rw-r--r--editor/reftests/passwd-5-with-Preview.html6
-rw-r--r--editor/reftests/passwd-5-with-TextEditor.html8
-rw-r--r--editor/reftests/passwd-6-ref.html6
-rw-r--r--editor/reftests/passwd-6-with-Preview.html10
-rw-r--r--editor/reftests/passwd-6-with-TextEditor.html12
-rw-r--r--editor/reftests/passwd-7-with-Preview.html6
-rw-r--r--editor/reftests/passwd-7-with-TextEditor.html8
-rw-r--r--editor/reftests/passwd-8-with-Preview.html7
-rw-r--r--editor/reftests/passwd-8-with-TextEditor.html9
-rw-r--r--editor/reftests/passwd-9-with-Preview.html9
-rw-r--r--editor/reftests/passwd-9-with-TextEditor.html7
-rw-r--r--editor/reftests/passwd-ref.html6
-rw-r--r--editor/reftests/readonly-editable-ref.html13
-rw-r--r--editor/reftests/readonly-editable.html24
-rw-r--r--editor/reftests/readonly-non-editable-ref.html21
-rw-r--r--editor/reftests/readonly-non-editable.html24
-rw-r--r--editor/reftests/readwrite-editable-ref.html13
-rw-r--r--editor/reftests/readwrite-editable.html24
-rw-r--r--editor/reftests/readwrite-non-editable-ref.html21
-rw-r--r--editor/reftests/readwrite-non-editable.html24
-rw-r--r--editor/reftests/reftest.list157
-rw-r--r--editor/reftests/selection_visibility_after_reframe-2.html12
-rw-r--r--editor/reftests/selection_visibility_after_reframe-3.html15
-rw-r--r--editor/reftests/selection_visibility_after_reframe-ref.html6
-rw-r--r--editor/reftests/selection_visibility_after_reframe.html15
-rw-r--r--editor/reftests/spellcheck-comma-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-comma-valid.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr-inherit.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-attr.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-disabled-partial-ref.html12
-rw-r--r--editor/reftests/spellcheck-contenteditable-disabled-partial.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-disabled-ref.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-disabled.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-focused-reframe.html18
-rw-r--r--editor/reftests/spellcheck-contenteditable-focused.html13
-rw-r--r--editor/reftests/spellcheck-contenteditable-nofocus-1.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-nofocus-2.html2
-rw-r--r--editor/reftests/spellcheck-contenteditable-nofocus-ref.html6
-rw-r--r--editor/reftests/spellcheck-contenteditable-property-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-property-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-property-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-property-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-contenteditable-ref.html11
-rw-r--r--editor/reftests/spellcheck-dotafterquote-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-dotafterquote-valid.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-invalid-ref.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-invalid.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-multiple-invalid-ref.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-multiple-invalid.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-multiple-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-multiple-valid.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-hyphen-valid.html6
-rw-r--r--editor/reftests/spellcheck-input-attr-after.html6
-rw-r--r--editor/reftests/spellcheck-input-attr-before.html6
-rw-r--r--editor/reftests/spellcheck-input-attr-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-input-attr-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-input-attr-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-input-attr-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-input-attr-inherit.html6
-rw-r--r--editor/reftests/spellcheck-input-disabled.html6
-rw-r--r--editor/reftests/spellcheck-input-nofocus-ref.html6
-rw-r--r--editor/reftests/spellcheck-input-property-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-input-property-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-input-property-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-input-property-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-input-ref.html22
-rw-r--r--editor/reftests/spellcheck-non-latin-arabic-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-arabic.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-chinese-simplified-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-chinese-simplified.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-chinese-traditional-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-chinese-traditional.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-hebrew-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-hebrew.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-japanese-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-japanese.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-korean-ref.html9
-rw-r--r--editor/reftests/spellcheck-non-latin-korean.html9
-rw-r--r--editor/reftests/spellcheck-period-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-period-valid.html6
-rw-r--r--editor/reftests/spellcheck-slash-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-slash-valid.html6
-rw-r--r--editor/reftests/spellcheck-space-valid-ref.html6
-rw-r--r--editor/reftests/spellcheck-space-valid.html6
-rw-r--r--editor/reftests/spellcheck-superscript-1-ref.html3
-rw-r--r--editor/reftests/spellcheck-superscript-1.html3
-rw-r--r--editor/reftests/spellcheck-superscript-2-ref.html3
-rw-r--r--editor/reftests/spellcheck-superscript-2.html3
-rw-r--r--editor/reftests/spellcheck-textarea-attr-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-textarea-attr-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-textarea-attr-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-textarea-attr-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-textarea-attr-inherit.html6
-rw-r--r--editor/reftests/spellcheck-textarea-attr.html6
-rw-r--r--editor/reftests/spellcheck-textarea-disabled.html6
-rw-r--r--editor/reftests/spellcheck-textarea-focused-notreadonly.html19
-rw-r--r--editor/reftests/spellcheck-textarea-focused-reframe.html18
-rw-r--r--editor/reftests/spellcheck-textarea-focused.html13
-rw-r--r--editor/reftests/spellcheck-textarea-nofocus-ref.html6
-rw-r--r--editor/reftests/spellcheck-textarea-nofocus.html6
-rw-r--r--editor/reftests/spellcheck-textarea-property-dynamic-inherit.html11
-rw-r--r--editor/reftests/spellcheck-textarea-property-dynamic-override-inherit.html11
-rw-r--r--editor/reftests/spellcheck-textarea-property-dynamic-override.html11
-rw-r--r--editor/reftests/spellcheck-textarea-property-dynamic.html11
-rw-r--r--editor/reftests/spellcheck-textarea-ref.html11
-rw-r--r--editor/reftests/spellcheck-textarea-ref2.html11
-rw-r--r--editor/reftests/spellcheck-url-valid-ref.html14
-rw-r--r--editor/reftests/spellcheck-url-valid.html14
-rw-r--r--editor/reftests/unneeded_scroll-ref.html16
-rw-r--r--editor/reftests/unneeded_scroll.html24
-rw-r--r--editor/reftests/xul/empty-ref.xhtml13
-rw-r--r--editor/reftests/xul/emptytextbox-4.xhtml12
-rw-r--r--editor/reftests/xul/emptytextbox-ref.xhtml13
-rw-r--r--editor/reftests/xul/input.css36
-rw-r--r--editor/reftests/xul/placeholder-reset.css8
-rw-r--r--editor/reftests/xul/platform.js28
-rw-r--r--editor/reftests/xul/reftest.list1
-rw-r--r--editor/spellchecker/EditorSpellCheck.cpp1179
-rw-r--r--editor/spellchecker/EditorSpellCheck.h99
-rw-r--r--editor/spellchecker/FilteredContentIterator.cpp398
-rw-r--r--editor/spellchecker/FilteredContentIterator.h82
-rw-r--r--editor/spellchecker/TextServicesDocument.cpp2809
-rw-r--r--editor/spellchecker/TextServicesDocument.h438
-rw-r--r--editor/spellchecker/moz.build34
-rw-r--r--editor/spellchecker/nsComposeTxtSrvFilter.cpp64
-rw-r--r--editor/spellchecker/nsComposeTxtSrvFilter.h45
-rw-r--r--editor/spellchecker/nsIInlineSpellChecker.idl40
-rw-r--r--editor/spellchecker/tests/bug1200533_subframe.html15
-rw-r--r--editor/spellchecker/tests/bug1204147_subframe.html11
-rw-r--r--editor/spellchecker/tests/bug1204147_subframe2.html9
-rw-r--r--editor/spellchecker/tests/bug678842_subframe.html8
-rw-r--r--editor/spellchecker/tests/bug717433_subframe.html8
-rw-r--r--editor/spellchecker/tests/chrome.ini9
-rw-r--r--editor/spellchecker/tests/de-DE/de_DE.aff2
-rw-r--r--editor/spellchecker/tests/de-DE/de_DE.dic6
-rw-r--r--editor/spellchecker/tests/en-AU/en_AU.aff2
-rw-r--r--editor/spellchecker/tests/en-AU/en_AU.dic4
-rw-r--r--editor/spellchecker/tests/en-GB/en_GB.aff2
-rw-r--r--editor/spellchecker/tests/en-GB/en_GB.dic4
-rw-r--r--editor/spellchecker/tests/mochitest.ini68
-rw-r--r--editor/spellchecker/tests/multiple_content_languages_subframe.html13
-rw-r--r--editor/spellchecker/tests/ru-RU/ru_RU.aff1
-rw-r--r--editor/spellchecker/tests/ru-RU/ru_RU.dic2
-rw-r--r--editor/spellchecker/tests/spellcheck.js36
-rw-r--r--editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html60
-rw-r--r--editor/spellchecker/tests/test_bug1100966.html74
-rw-r--r--editor/spellchecker/tests/test_bug1154791.html74
-rw-r--r--editor/spellchecker/tests/test_bug1200533.html163
-rw-r--r--editor/spellchecker/tests/test_bug1204147.html115
-rw-r--r--editor/spellchecker/tests/test_bug1205983.html134
-rw-r--r--editor/spellchecker/tests/test_bug1209414.html144
-rw-r--r--editor/spellchecker/tests/test_bug1219928.html69
-rw-r--r--editor/spellchecker/tests/test_bug1365383.html46
-rw-r--r--editor/spellchecker/tests/test_bug1368544.html91
-rw-r--r--editor/spellchecker/tests/test_bug1402822.html114
-rw-r--r--editor/spellchecker/tests/test_bug1418629.html210
-rw-r--r--editor/spellchecker/tests/test_bug1497480.html94
-rw-r--r--editor/spellchecker/tests/test_bug1602526.html57
-rw-r--r--editor/spellchecker/tests/test_bug1761273.html96
-rw-r--r--editor/spellchecker/tests/test_bug1773802.html96
-rw-r--r--editor/spellchecker/tests/test_bug1837268.html79
-rw-r--r--editor/spellchecker/tests/test_bug338427.html61
-rw-r--r--editor/spellchecker/tests/test_bug366682.html65
-rw-r--r--editor/spellchecker/tests/test_bug432225.html72
-rw-r--r--editor/spellchecker/tests/test_bug484181.html73
-rw-r--r--editor/spellchecker/tests/test_bug596333.html135
-rw-r--r--editor/spellchecker/tests/test_bug636465.html54
-rw-r--r--editor/spellchecker/tests/test_bug678842.html106
-rw-r--r--editor/spellchecker/tests/test_bug697981.html137
-rw-r--r--editor/spellchecker/tests/test_bug717433.html111
-rw-r--r--editor/spellchecker/tests/test_multiple_content_languages.html176
-rw-r--r--editor/spellchecker/tests/test_nsIEditorSpellCheck_ReplaceWord.html64
-rw-r--r--editor/spellchecker/tests/test_spellcheck_after_edit.html198
-rw-r--r--editor/spellchecker/tests/test_spellcheck_after_pressing_navigation_key.html77
-rw-r--r--editor/spellchecker/tests/test_spellcheck_selection.html36
-rw-r--r--editor/spellchecker/tests/test_suggest.html42
-rw-r--r--editor/txmgr/TransactionItem.cpp257
-rw-r--r--editor/txmgr/TransactionItem.h74
-rw-r--r--editor/txmgr/TransactionManager.cpp509
-rw-r--r--editor/txmgr/TransactionManager.h93
-rw-r--r--editor/txmgr/TransactionStack.cpp75
-rw-r--r--editor/txmgr/TransactionStack.h44
-rw-r--r--editor/txmgr/moz.build31
-rw-r--r--editor/txmgr/nsITransaction.idl82
-rw-r--r--editor/txmgr/nsITransactionManager.idl148
-rw-r--r--editor/txmgr/nsTransactionManagerCID.h7
-rw-r--r--editor/txmgr/tests/TestTXMgr.cpp2013
-rw-r--r--editor/txmgr/tests/crashtests/407072-1.html22
-rw-r--r--editor/txmgr/tests/crashtests/449006-1.html28
-rw-r--r--editor/txmgr/tests/crashtests/crashtests.list2
-rw-r--r--editor/txmgr/tests/moz.build10
925 files changed, 179216 insertions, 0 deletions
diff --git a/editor/AsyncSpellCheckTestHelper.sys.mjs b/editor/AsyncSpellCheckTestHelper.sys.mjs
new file mode 100644
index 0000000000..6ceb2a86b2
--- /dev/null
+++ b/editor/AsyncSpellCheckTestHelper.sys.mjs
@@ -0,0 +1,125 @@
+/* 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/. */
+
+const SPELL_CHECK_ENDED_TOPIC = "inlineSpellChecker-spellCheck-ended";
+const SPELL_CHECK_STARTED_TOPIC = "inlineSpellChecker-spellCheck-started";
+
+const CP = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+);
+
+/**
+ * Waits until spell checking has stopped on the given element.
+ *
+ * When a spell check is pending, this waits indefinitely until the spell check
+ * ends. When a spell check is not pending, it waits a small number of turns of
+ * the event loop: if a spell check begins, it resumes waiting indefinitely for
+ * the end, and otherwise it stops waiting and calls the callback.
+ *
+ * This this can therefore trap spell checks that have not started at the time
+ * of calling, spell checks that have already started, multiple consecutive
+ * spell checks, and the absence of spell checks altogether.
+ *
+ * @param editableElement The element being spell checked.
+ * @param callback Called when spell check has completed or enough turns
+ * of the event loop have passed to determine it has not
+ * started.
+ */
+export function maybeOnSpellCheck(editableElement, callback) {
+ let editor = editableElement.editor;
+ if (!editor) {
+ let win = editableElement.ownerGlobal;
+ editor = win.docShell.editingSession.getEditorForWindow(win);
+ }
+ if (!editor) {
+ throw new Error("Unable to find editor for element " + editableElement);
+ }
+
+ try {
+ // False is important here. Pass false so that the inline spell checker
+ // isn't created if it doesn't already exist.
+ var isc = editor.getInlineSpellChecker(false);
+ } catch (err) {
+ // getInlineSpellChecker throws if spell checking is not enabled instead of
+ // just returning null, which seems kind of lame. (Spell checking is not
+ // enabled on Android.) The point here is only to determine whether spell
+ // check is pending, and if getInlineSpellChecker throws, then it's not
+ // pending.
+ }
+ let waitingForEnded = isc && isc.spellCheckPending;
+ let count = 0;
+
+ function observe(subj, topic, data) {
+ if (subj != editor) {
+ return;
+ }
+ count = 0;
+ let expectedTopic = waitingForEnded
+ ? SPELL_CHECK_ENDED_TOPIC
+ : SPELL_CHECK_STARTED_TOPIC;
+ if (topic != expectedTopic) {
+ console.error("Expected " + expectedTopic + " but got " + topic + "!");
+ }
+ waitingForEnded = !waitingForEnded;
+ }
+
+ // eslint-disable-next-line mozilla/use-services
+ let os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(observe, SPELL_CHECK_STARTED_TOPIC);
+ os.addObserver(observe, SPELL_CHECK_ENDED_TOPIC);
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(
+ function tick() {
+ // Wait an arbitrarily large number -- 100 -- turns of the event loop before
+ // declaring that no spell checks will start.
+ if (waitingForEnded || ++count < 100) {
+ return;
+ }
+ timer.cancel();
+ os.removeObserver(observe, SPELL_CHECK_STARTED_TOPIC);
+ os.removeObserver(observe, SPELL_CHECK_ENDED_TOPIC);
+ callback();
+ },
+ 0,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+}
+
+/**
+ * Waits until spell checking has stopped on the given element.
+ *
+ * @param editableElement The element being spell checked.
+ * @param callback Called when spell check has completed or enough turns
+ * of the event loop have passed to determine it has not
+ * started.
+ */
+export function onSpellCheck(editableElement, callback) {
+ const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+ );
+
+ let editor = editableElement.editor;
+ TestUtils.topicObserved(SPELL_CHECK_ENDED_TOPIC, s => s == editor).then(
+ callback
+ );
+}
+
+export async function getDictionaryContentPref() {
+ let dictionaries = await new Promise(resolve => {
+ let value = "";
+ CP.getByDomainAndName("mochi.test", "spellcheck.lang", null, {
+ handleResult(pref) {
+ value = pref.value;
+ },
+ handleCompletion() {
+ resolve(value);
+ },
+ });
+ });
+
+ return dictionaries;
+}
diff --git a/editor/composer/ComposerCommandsUpdater.cpp b/editor/composer/ComposerCommandsUpdater.cpp
new file mode 100644
index 0000000000..efeb9d9544
--- /dev/null
+++ b/editor/composer/ComposerCommandsUpdater.cpp
@@ -0,0 +1,199 @@
+/* -*- 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/ComposerCommandsUpdater.h"
+
+#include "mozilla/mozalloc.h" // for operator new
+#include "mozilla/TransactionManager.h" // for TransactionManager
+#include "mozilla/dom/Selection.h"
+#include "nsCommandManager.h" // for nsCommandManager
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsDocShell.h" // for nsIDocShell
+#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc
+#include "nsID.h" // for NS_GET_IID, etc
+#include "nsIInterfaceRequestorUtils.h" // for do_GetInterface
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsPIDOMWindow.h" // for nsPIDOMWindow
+
+class nsITransaction;
+
+namespace mozilla {
+
+ComposerCommandsUpdater::ComposerCommandsUpdater()
+ : mDirtyState(eStateUninitialized),
+ mSelectionCollapsed(eStateUninitialized),
+ mFirstDoOfFirstUndo(true) {}
+
+ComposerCommandsUpdater::~ComposerCommandsUpdater() {
+ // cancel any outstanding update timer
+ if (mUpdateTimer) {
+ mUpdateTimer->Cancel();
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ComposerCommandsUpdater)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ComposerCommandsUpdater)
+
+NS_INTERFACE_MAP_BEGIN(ComposerCommandsUpdater)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(ComposerCommandsUpdater)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(ComposerCommandsUpdater, mUpdateTimer, mDOMWindow,
+ mDocShell)
+
+#if 0
+# pragma mark -
+#endif
+
+void ComposerCommandsUpdater::DidDoTransaction(
+ TransactionManager& aTransactionManager) {
+ // only need to update if the status of the Undo menu item changes.
+ if (aTransactionManager.NumberOfUndoItems() == 1) {
+ if (mFirstDoOfFirstUndo) {
+ UpdateCommandGroup(CommandGroup::Undo);
+ }
+ mFirstDoOfFirstUndo = false;
+ }
+}
+
+void ComposerCommandsUpdater::DidUndoTransaction(
+ TransactionManager& aTransactionManager) {
+ if (!aTransactionManager.NumberOfUndoItems()) {
+ mFirstDoOfFirstUndo = true; // reset the state for the next do
+ }
+ UpdateCommandGroup(CommandGroup::Undo);
+}
+
+void ComposerCommandsUpdater::DidRedoTransaction(
+ TransactionManager& aTransactionManager) {
+ UpdateCommandGroup(CommandGroup::Undo);
+}
+
+#if 0
+# pragma mark -
+#endif
+
+void ComposerCommandsUpdater::Init(nsPIDOMWindowOuter& aDOMWindow) {
+ mDOMWindow = &aDOMWindow;
+ mDocShell = aDOMWindow.GetDocShell();
+}
+
+nsresult ComposerCommandsUpdater::PrimeUpdateTimer() {
+ if (!mUpdateTimer) {
+ mUpdateTimer = NS_NewTimer();
+ }
+ const uint32_t kUpdateTimerDelay = 150;
+ return mUpdateTimer->InitWithCallback(static_cast<nsITimerCallback*>(this),
+ kUpdateTimerDelay,
+ nsITimer::TYPE_ONE_SHOT);
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY
+void ComposerCommandsUpdater::TimerCallback() {
+ mSelectionCollapsed = SelectionIsCollapsed();
+ UpdateCommandGroup(CommandGroup::Style);
+}
+
+void ComposerCommandsUpdater::UpdateCommandGroup(CommandGroup aCommandGroup) {
+ RefPtr<nsCommandManager> commandManager = GetCommandManager();
+ if (NS_WARN_IF(!commandManager)) {
+ return;
+ }
+
+ switch (aCommandGroup) {
+ case CommandGroup::Undo:
+ commandManager->CommandStatusChanged("cmd_undo");
+ commandManager->CommandStatusChanged("cmd_redo");
+ return;
+ case CommandGroup::Style:
+ commandManager->CommandStatusChanged("cmd_bold");
+ commandManager->CommandStatusChanged("cmd_italic");
+ commandManager->CommandStatusChanged("cmd_underline");
+ commandManager->CommandStatusChanged("cmd_tt");
+
+ commandManager->CommandStatusChanged("cmd_strikethrough");
+ commandManager->CommandStatusChanged("cmd_superscript");
+ commandManager->CommandStatusChanged("cmd_subscript");
+ commandManager->CommandStatusChanged("cmd_nobreak");
+
+ commandManager->CommandStatusChanged("cmd_em");
+ commandManager->CommandStatusChanged("cmd_strong");
+ commandManager->CommandStatusChanged("cmd_cite");
+ commandManager->CommandStatusChanged("cmd_abbr");
+ commandManager->CommandStatusChanged("cmd_acronym");
+ commandManager->CommandStatusChanged("cmd_code");
+ commandManager->CommandStatusChanged("cmd_samp");
+ commandManager->CommandStatusChanged("cmd_var");
+
+ commandManager->CommandStatusChanged("cmd_increaseFont");
+ commandManager->CommandStatusChanged("cmd_decreaseFont");
+
+ commandManager->CommandStatusChanged("cmd_paragraphState");
+ commandManager->CommandStatusChanged("cmd_fontFace");
+ commandManager->CommandStatusChanged("cmd_fontColor");
+ commandManager->CommandStatusChanged("cmd_backgroundColor");
+ commandManager->CommandStatusChanged("cmd_highlight");
+ return;
+ case CommandGroup::Save:
+ commandManager->CommandStatusChanged("cmd_setDocumentModified");
+ commandManager->CommandStatusChanged("cmd_save");
+ return;
+ default:
+ MOZ_ASSERT_UNREACHABLE("New command group hasn't been implemented yet");
+ }
+}
+
+nsresult ComposerCommandsUpdater::UpdateOneCommand(const char* aCommand) {
+ RefPtr<nsCommandManager> commandManager = GetCommandManager();
+ NS_ENSURE_TRUE(commandManager, NS_ERROR_FAILURE);
+ commandManager->CommandStatusChanged(aCommand);
+ return NS_OK;
+}
+
+bool ComposerCommandsUpdater::SelectionIsCollapsed() {
+ if (NS_WARN_IF(!mDOMWindow)) {
+ return true;
+ }
+
+ RefPtr<dom::Selection> domSelection = mDOMWindow->GetSelection();
+ if (NS_WARN_IF(!domSelection)) {
+ return false;
+ }
+
+ return domSelection->IsCollapsed();
+}
+
+nsCommandManager* ComposerCommandsUpdater::GetCommandManager() {
+ if (NS_WARN_IF(!mDocShell)) {
+ return nullptr;
+ }
+ return mDocShell->GetCommandManager();
+}
+
+NS_IMETHODIMP ComposerCommandsUpdater::GetName(nsACString& aName) {
+ aName.AssignLiteral("ComposerCommandsUpdater");
+ return NS_OK;
+}
+
+#if 0
+# pragma mark -
+#endif
+
+nsresult ComposerCommandsUpdater::Notify(nsITimer* aTimer) {
+ NS_ASSERTION(aTimer == mUpdateTimer.get(), "Hey, this ain't my timer!");
+ TimerCallback();
+ return NS_OK;
+}
+
+#if 0
+# pragma mark -
+#endif
+
+} // namespace mozilla
diff --git a/editor/composer/ComposerCommandsUpdater.h b/editor/composer/ComposerCommandsUpdater.h
new file mode 100644
index 0000000000..6cf0886c57
--- /dev/null
+++ b/editor/composer/ComposerCommandsUpdater.h
@@ -0,0 +1,132 @@
+/* -*- 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_ComposerCommandsUpdater_h
+#define mozilla_ComposerCommandsUpdater_h
+
+#include "nsCOMPtr.h" // for already_AddRefed, nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsINamed.h"
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS
+#include "nsITimer.h" // for NS_DECL_NSITIMERCALLBACK, etc
+#include "nscore.h" // for NS_IMETHOD, nsresult, etc
+
+class nsCommandManager;
+class nsIDocShell;
+class nsITransaction;
+class nsITransactionManager;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+
+class TransactionManager;
+
+class ComposerCommandsUpdater final : public nsITimerCallback, public nsINamed {
+ public:
+ ComposerCommandsUpdater();
+
+ // nsISupports
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(ComposerCommandsUpdater,
+ nsITimerCallback)
+
+ // nsITimerCallback
+ NS_DECL_NSITIMERCALLBACK
+
+ // nsINamed
+ NS_DECL_NSINAMED
+
+ void Init(nsPIDOMWindowOuter& aDOMWindow);
+
+ /**
+ * OnSelectionChange() is called when selection is changed in the editor.
+ */
+ void OnSelectionChange() { PrimeUpdateTimer(); }
+
+ /**
+ * OnHTMLEditorCreated() is called when `HTMLEditor` is created and
+ * initialized.
+ */
+ MOZ_CAN_RUN_SCRIPT void OnHTMLEditorCreated() {
+ UpdateOneCommand("obs_documentCreated");
+ }
+
+ /**
+ * OnBeforeHTMLEditorDestroyed() is called when `HTMLEditor` is being
+ * destroyed.
+ */
+ MOZ_CAN_RUN_SCRIPT void OnBeforeHTMLEditorDestroyed() {
+ // cancel any outstanding update timer
+ if (mUpdateTimer) {
+ mUpdateTimer->Cancel();
+ mUpdateTimer = nullptr;
+ }
+
+ // We can't notify the command manager of this right now; it is too late in
+ // some cases and the window is already partially destructed (e.g. JS
+ // objects may be gone).
+ }
+
+ /**
+ * OnHTMLEditorDirtyStateChanged() is called when dirty state of `HTMLEditor`
+ * is changed form or to "dirty".
+ */
+ MOZ_CAN_RUN_SCRIPT void OnHTMLEditorDirtyStateChanged(bool aNowDirty) {
+ if (mDirtyState == static_cast<int8_t>(aNowDirty)) {
+ return;
+ }
+ UpdateCommandGroup(CommandGroup::Save);
+ UpdateCommandGroup(CommandGroup::Undo);
+ mDirtyState = aNowDirty;
+ }
+
+ /**
+ * The following methods are called when aTransactionManager did
+ * `DoTransaction`, `UndoTransaction` or `RedoTransaction` of a transaction
+ * instance.
+ */
+ MOZ_CAN_RUN_SCRIPT void DidDoTransaction(
+ TransactionManager& aTransactionManager);
+ MOZ_CAN_RUN_SCRIPT void DidUndoTransaction(
+ TransactionManager& aTransactionManager);
+ MOZ_CAN_RUN_SCRIPT void DidRedoTransaction(
+ TransactionManager& aTransactionManager);
+
+ protected:
+ virtual ~ComposerCommandsUpdater();
+
+ enum {
+ eStateUninitialized = -1,
+ eStateOff = 0,
+ eStateOn = 1,
+ };
+
+ bool SelectionIsCollapsed();
+ MOZ_CAN_RUN_SCRIPT nsresult UpdateOneCommand(const char* aCommand);
+ enum class CommandGroup {
+ Save,
+ Style,
+ Undo,
+ };
+ MOZ_CAN_RUN_SCRIPT void UpdateCommandGroup(CommandGroup aCommandGroup);
+
+ nsCommandManager* GetCommandManager();
+
+ nsresult PrimeUpdateTimer();
+ void TimerCallback();
+
+ nsCOMPtr<nsITimer> mUpdateTimer;
+ nsCOMPtr<nsPIDOMWindowOuter> mDOMWindow;
+ nsCOMPtr<nsIDocShell> mDocShell;
+
+ int8_t mDirtyState;
+ int8_t mSelectionCollapsed;
+ bool mFirstDoOfFirstUndo;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_ComposerCommandsUpdater_h
diff --git a/editor/composer/crashtests/351236-1.html b/editor/composer/crashtests/351236-1.html
new file mode 100644
index 0000000000..99674f1814
--- /dev/null
+++ b/editor/composer/crashtests/351236-1.html
@@ -0,0 +1,37 @@
+<html><head>
+<title>Testcase bug 351236 - Crash [@ nsGetInterface::operator()] with designMode iframes, removing styles, removing iframes, reloading, etc</title>
+<script>
+function designmodes(i){
+try {
+window.frames[0].document.designMode='on';
+window.frames[0].focus();
+window.frames[0].getSelection().collapse(window.frames[0].document.body.childNodes[0],window.frames[0].document.body.childNodes[0].length-2)
+window.frames[0].document.execCommand('inserthtml', false, 'tesxt ');
+} catch(e) {}
+
+setTimeout(designmodes,50);
+}
+
+function removestyles(){
+document.getElementsByTagName('iframe')[0].removeAttribute('style');
+document.getElementsByTagName('q')[0].removeAttribute('style');
+}
+
+function doe() {
+setTimeout(designmodes,200);
+setTimeout(removestyles,500);
+setTimeout(function() {document.removeChild(document.documentElement);}, 1000);
+setTimeout(function() {window.location.reload();}, 1500);
+}
+window.onload=doe;
+</script>
+
+</head>
+<body>
+This page should not crash Mozilla within 2 seconds<br>
+<q style="display: table-row;">
+<iframe style="display: table-row;"></iframe>
+<iframe></iframe>
+</q>
+</body>
+</html>
diff --git a/editor/composer/crashtests/407062-1.html b/editor/composer/crashtests/407062-1.html
new file mode 100644
index 0000000000..9bd5d02e59
--- /dev/null
+++ b/editor/composer/crashtests/407062-1.html
@@ -0,0 +1,20 @@
+<html contentEditable="true">
+<head>
+
+<script type="text/javascript">
+
+function boom()
+{
+ var r = document.documentElement;
+ while(r.firstChild)
+ r.firstChild.remove();
+
+ document.execCommand("contentReadOnly", false, "");
+ document.documentElement.focus();
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/editor/composer/crashtests/419563-1.xhtml b/editor/composer/crashtests/419563-1.xhtml
new file mode 100644
index 0000000000..417530c13a
--- /dev/null
+++ b/editor/composer/crashtests/419563-1.xhtml
@@ -0,0 +1,20 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.documentElement.appendChild(document.body);
+ document.getElementById("s").contentEditable = "true";
+ document.getElementById("v").focus();
+ document.body.focus();
+ document.execCommand("delete", false, null);
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+<span id="s">thesewords arenot realwords</span><body contenteditable="true" onload="setTimeout(boom, 0);"><span contenteditable="false"><div id="v" contenteditable="true"></div>Five</span></body>
+
+</html>
diff --git a/editor/composer/crashtests/428844-1-inner.xhtml b/editor/composer/crashtests/428844-1-inner.xhtml
new file mode 100644
index 0000000000..1cc72d0856
--- /dev/null
+++ b/editor/composer/crashtests/428844-1-inner.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet type="text/xsl" href="#a"?>
+<html xmlns="http://www.w3.org/1999/xhtml" onload="dump('Inner onload\n'); window.location.reload()" contenteditable="true">
+<xsl:stylesheet version="1.0" id="a" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>
+</html>
diff --git a/editor/composer/crashtests/428844-1.html b/editor/composer/crashtests/428844-1.html
new file mode 100644
index 0000000000..793aababd4
--- /dev/null
+++ b/editor/composer/crashtests/428844-1.html
@@ -0,0 +1,17 @@
+<html class="reftest-wait">
+<head>
+<script>
+function boom() {
+ var iframe = document.getElementById('inner');
+ iframe.addEventListener("load", function() {
+ document.documentElement.removeAttribute("class");
+ });
+ iframe.src = "data:text/html,";
+ dump("Outer onload\n");
+}
+</script>
+</head>
+<body onload="boom()">
+<iframe src="428844-1-inner.xhtml" id="inner"></iframe>
+</body>
+</html>
diff --git a/editor/composer/crashtests/461049-1.html b/editor/composer/crashtests/461049-1.html
new file mode 100644
index 0000000000..fea188e646
--- /dev/null
+++ b/editor/composer/crashtests/461049-1.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function uu()
+{
+ document.removeEventListener("DOMSubtreeModified", uu);
+ document.execCommand("undo", false, null);
+}
+
+function boom()
+{
+ document.execCommand("selectAll", false, null);
+ document.execCommand("strikethrough", false, null);
+ document.addEventListener("DOMSubtreeModified", uu);
+ document.execCommand("undo", false, null);
+}
+
+</script>
+</head>
+
+<body contenteditable="true" onload="boom();"><div></div></body>
+
+</html>
diff --git a/editor/composer/crashtests/crashtests.list b/editor/composer/crashtests/crashtests.list
new file mode 100644
index 0000000000..db84e0e5b1
--- /dev/null
+++ b/editor/composer/crashtests/crashtests.list
@@ -0,0 +1,6 @@
+load 351236-1.html
+load 407062-1.html
+load 419563-1.xhtml
+load 428844-1.html
+load 461049-1.html
+load removing-editable-xslt.html
diff --git a/editor/composer/crashtests/removing-editable-xslt-inner.xhtml b/editor/composer/crashtests/removing-editable-xslt-inner.xhtml
new file mode 100644
index 0000000000..cbf206d7ed
--- /dev/null
+++ b/editor/composer/crashtests/removing-editable-xslt-inner.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet type="text/xsl" href="#a"?>
+<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true">
+<xsl:stylesheet version="1.0" id="a" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>
+</html>
diff --git a/editor/composer/crashtests/removing-editable-xslt.html b/editor/composer/crashtests/removing-editable-xslt.html
new file mode 100644
index 0000000000..cbf104ac99
--- /dev/null
+++ b/editor/composer/crashtests/removing-editable-xslt.html
@@ -0,0 +1,17 @@
+<html class="reftest-wait">
+<head>
+<script>
+function boom()
+{
+ document.getElementById("i").src = "removing-editable-xslt-inner.xhtml";
+ setTimeout(function() {
+ document.body.removeChild(document.getElementById("i"));
+ document.documentElement.removeAttribute("class");
+ }, 0);
+}
+</script>
+</head>
+<body onload="boom();">
+<iframe id="i"></iframe>
+</body>
+</html>
diff --git a/editor/composer/moz.build b/editor/composer/moz.build
new file mode 100644
index 0000000000..38f0506665
--- /dev/null
+++ b/editor/composer/moz.build
@@ -0,0 +1,60 @@
+# -*- 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 += ["test/mochitest.ini"]
+
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"]
+
+XPIDL_SOURCES += [
+ "nsIEditingSession.idl",
+]
+
+XPIDL_MODULE = "composer"
+
+UNIFIED_SOURCES += [
+ "ComposerCommandsUpdater.cpp",
+ "nsEditingSession.cpp",
+]
+
+EXPORTS += [
+ "nsEditingSession.h",
+]
+
+EXPORTS.mozilla += [
+ "ComposerCommandsUpdater.h",
+]
+
+# Needed because we include HTMLEditor.h which indirectly includes Document.h
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/dom/html", # For nsHTMLDocument
+ "/editor/spellchecker", # nsComposeTxtSrvFilter.h
+ "/layout/style", # For things nsHTMLDocument includes.
+]
+
+FINAL_LIBRARY = "xul"
+RESOURCE_FILES += [
+ "res/EditorOverride.css",
+ "res/grabber.gif",
+ "res/table-add-column-after-active.gif",
+ "res/table-add-column-after-hover.gif",
+ "res/table-add-column-after.gif",
+ "res/table-add-column-before-active.gif",
+ "res/table-add-column-before-hover.gif",
+ "res/table-add-column-before.gif",
+ "res/table-add-row-after-active.gif",
+ "res/table-add-row-after-hover.gif",
+ "res/table-add-row-after.gif",
+ "res/table-add-row-before-active.gif",
+ "res/table-add-row-before-hover.gif",
+ "res/table-add-row-before.gif",
+ "res/table-remove-column-active.gif",
+ "res/table-remove-column-hover.gif",
+ "res/table-remove-column.gif",
+ "res/table-remove-row-active.gif",
+ "res/table-remove-row-hover.gif",
+ "res/table-remove-row.gif",
+]
diff --git a/editor/composer/nsEditingSession.cpp b/editor/composer/nsEditingSession.cpp
new file mode 100644
index 0000000000..72435ded45
--- /dev/null
+++ b/editor/composer/nsEditingSession.cpp
@@ -0,0 +1,1301 @@
+/* -*- 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 <string.h> // for nullptr, strcmp
+
+#include "imgIContainer.h" // for imgIContainer, etc
+#include "mozilla/ComposerCommandsUpdater.h" // for ComposerCommandsUpdater
+#include "mozilla/FlushType.h" // for FlushType::Frames
+#include "mozilla/HTMLEditor.h" // for HTMLEditor
+#include "mozilla/mozalloc.h" // for operator new
+#include "mozilla/PresShell.h" // for PresShell
+#include "nsAString.h"
+#include "nsBaseCommandController.h" // for nsBaseCommandController
+#include "nsCommandManager.h" // for nsCommandManager
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsContentUtils.h"
+#include "nsDebug.h" // for NS_ENSURE_SUCCESS, etc
+#include "nsDocShell.h" // for nsDocShell
+#include "nsEditingSession.h"
+#include "nsError.h" // for NS_ERROR_FAILURE, NS_OK, etc
+#include "nsIChannel.h" // for nsIChannel
+#include "nsIContentViewer.h" // for nsIContentViewer
+#include "nsIControllers.h" // for nsIControllers
+#include "nsID.h" // for NS_GET_IID, etc
+#include "nsHTMLDocument.h" // for nsHTMLDocument
+#include "nsIDocShell.h" // for nsIDocShell
+#include "mozilla/dom/Document.h" // for Document
+#include "nsIEditor.h" // for nsIEditor
+#include "nsIInterfaceRequestorUtils.h" // for do_GetInterface
+#include "nsIRefreshURI.h" // for nsIRefreshURI
+#include "nsIRequest.h" // for nsIRequest
+#include "nsITimer.h" // for nsITimer, etc
+#include "nsIWeakReference.h" // for nsISupportsWeakReference, etc
+#include "nsIWebNavigation.h" // for nsIWebNavigation
+#include "nsIWebProgress.h" // for nsIWebProgress, etc
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsPIDOMWindow.h" // for nsPIDOMWindow
+#include "nsPresContext.h" // for nsPresContext
+#include "nsReadableUtils.h" // for AppendUTF16toUTF8
+#include "nsStringFwd.h" // for nsString
+#include "mozilla/dom/BrowsingContext.h" // for BrowsingContext
+#include "mozilla/dom/Selection.h" // for AutoHideSelectionChanges, etc
+#include "mozilla/dom/WindowContext.h" // for WindowContext
+#include "nsFrameSelection.h" // for nsFrameSelection
+#include "nsBaseCommandController.h" // for nsBaseCommandController
+#include "mozilla/dom/LoadURIOptionsBinding.h"
+
+class nsISupports;
+class nsIURI;
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+/*---------------------------------------------------------------------------
+
+ nsEditingSession
+
+----------------------------------------------------------------------------*/
+nsEditingSession::nsEditingSession()
+ : mDoneSetup(false),
+ mCanCreateEditor(false),
+ mInteractive(false),
+ mMakeWholeDocumentEditable(true),
+ mDisabledJSAndPlugins(false),
+ mScriptsEnabled(true),
+ mPluginsEnabled(true),
+ mProgressListenerRegistered(false),
+ mImageAnimationMode(0),
+ mEditorFlags(0),
+ mEditorStatus(eEditorOK),
+ mBaseCommandControllerId(0),
+ mDocStateControllerId(0),
+ mHTMLCommandControllerId(0) {}
+
+/*---------------------------------------------------------------------------
+
+ ~nsEditingSession
+
+----------------------------------------------------------------------------*/
+nsEditingSession::~nsEditingSession() {
+ // Must cancel previous timer?
+ if (mLoadBlankDocTimer) mLoadBlankDocTimer->Cancel();
+}
+
+NS_IMPL_ISUPPORTS(nsEditingSession, nsIEditingSession, nsIWebProgressListener,
+ nsISupportsWeakReference)
+
+/*---------------------------------------------------------------------------
+
+ MakeWindowEditable
+
+ aEditorType string, "html" "htmlsimple" "text" "textsimple"
+ void makeWindowEditable(in nsIDOMWindow aWindow, in string aEditorType,
+ in boolean aDoAfterUriLoad,
+ in boolean aMakeWholeDocumentEditable,
+ in boolean aInteractive);
+----------------------------------------------------------------------------*/
+#define DEFAULT_EDITOR_TYPE "html"
+
+NS_IMETHODIMP
+nsEditingSession::MakeWindowEditable(mozIDOMWindowProxy* aWindow,
+ const char* aEditorType,
+ bool aDoAfterUriLoad,
+ bool aMakeWholeDocumentEditable,
+ bool aInteractive) {
+ mEditorType.Truncate();
+ mEditorFlags = 0;
+
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_FAILURE);
+ auto* window = nsPIDOMWindowOuter::From(aWindow);
+
+ // disable plugins
+ nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+ mDocShell = do_GetWeakReference(docShell);
+
+ mInteractive = aInteractive;
+ mMakeWholeDocumentEditable = aMakeWholeDocumentEditable;
+
+ nsresult rv;
+ if (!mInteractive) {
+ rv = DisableJSAndPlugins(window->GetCurrentInnerWindow());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Always remove existing editor
+ TearDownEditorOnWindow(aWindow);
+
+ // Tells embedder that startup is in progress
+ mEditorStatus = eEditorCreationInProgress;
+
+ // temporary to set editor type here. we will need different classes soon.
+ if (!aEditorType) aEditorType = DEFAULT_EDITOR_TYPE;
+ mEditorType = aEditorType;
+
+ // if all this does is setup listeners and I don't need listeners,
+ // can't this step be ignored?? (based on aDoAfterURILoad)
+ rv = PrepareForEditing(window);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // set the flag on the docShell to say that it's editable
+ rv = docShell->MakeEditable(aDoAfterUriLoad);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Setup commands common to plaintext and html editors,
+ // including the document creation observers
+ // the first is an editing controller
+ rv = SetupEditorCommandController(
+ nsBaseCommandController::CreateEditingController, aWindow,
+ static_cast<nsIEditingSession*>(this), &mBaseCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The second is a controller to monitor doc state,
+ // such as creation and "dirty flag"
+ rv = SetupEditorCommandController(
+ nsBaseCommandController::CreateHTMLEditorDocStateController, aWindow,
+ static_cast<nsIEditingSession*>(this), &mDocStateControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // aDoAfterUriLoad can be false only when making an existing window editable
+ if (!aDoAfterUriLoad) {
+ rv = SetupEditorOnWindow(MOZ_KnownLive(*window));
+
+ // mEditorStatus is set to the error reason
+ // Since this is used only when editing an existing page,
+ // it IS ok to destroy current editor
+ if (NS_FAILED(rv)) {
+ TearDownEditorOnWindow(aWindow);
+ }
+ }
+ return rv;
+}
+
+nsresult nsEditingSession::DisableJSAndPlugins(nsPIDOMWindowInner* aWindow) {
+ WindowContext* wc = aWindow->GetWindowContext();
+ BrowsingContext* bc = wc->GetBrowsingContext();
+
+ mScriptsEnabled = wc->GetAllowJavascript();
+
+ MOZ_TRY(wc->SetAllowJavascript(false));
+
+ // Disable plugins in this document:
+ mPluginsEnabled = bc->GetAllowPlugins();
+
+ MOZ_TRY(bc->SetAllowPlugins(false));
+
+ mDisabledJSAndPlugins = true;
+
+ return NS_OK;
+}
+
+nsresult nsEditingSession::RestoreJSAndPlugins(nsPIDOMWindowInner* aWindow) {
+ if (!mDisabledJSAndPlugins) {
+ return NS_OK;
+ }
+
+ mDisabledJSAndPlugins = false;
+
+ if (NS_WARN_IF(!aWindow)) {
+ // DetachFromWindow may call this method with nullptr.
+ return NS_ERROR_FAILURE;
+ }
+
+ WindowContext* wc = aWindow->GetWindowContext();
+ BrowsingContext* bc = wc->GetBrowsingContext();
+
+ MOZ_TRY(wc->SetAllowJavascript(mScriptsEnabled));
+
+ // Disable plugins in this document:
+
+ return bc->SetAllowPlugins(mPluginsEnabled);
+}
+
+/*---------------------------------------------------------------------------
+
+ WindowIsEditable
+
+ boolean windowIsEditable (in nsIDOMWindow aWindow);
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::WindowIsEditable(mozIDOMWindowProxy* aWindow,
+ bool* outIsEditable) {
+ NS_ENSURE_STATE(aWindow);
+ nsCOMPtr<nsIDocShell> docShell =
+ nsPIDOMWindowOuter::From(aWindow)->GetDocShell();
+ NS_ENSURE_STATE(docShell);
+
+ return docShell->GetEditable(outIsEditable);
+}
+
+bool IsSupportedTextType(const nsAString& aMIMEType) {
+ // These are MIME types that are automatically parsed as "text/plain"
+ // and thus we can edit them as plaintext
+ // Note: in older versions, we attempted to convert the mimetype of
+ // the network channel for these and "text/xml" to "text/plain",
+ // but further investigation reveals that strategy doesn't work
+ static constexpr nsLiteralString sSupportedTextTypes[] = {
+ u"text/plain"_ns,
+ u"text/css"_ns,
+ u"text/rdf"_ns,
+ u"text/xsl"_ns,
+ u"text/javascript"_ns, // obsolete type
+ u"text/ecmascript"_ns, // obsolete type
+ u"application/javascript"_ns,
+ u"application/ecmascript"_ns,
+ u"application/x-javascript"_ns, // obsolete type
+ u"text/xul"_ns // obsolete type
+ };
+
+ for (const nsLiteralString& supportedTextType : sSupportedTextTypes) {
+ if (aMIMEType.Equals(supportedTextType)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+nsresult nsEditingSession::SetupEditorOnWindow(nsPIDOMWindowOuter& aWindow) {
+ mDoneSetup = true;
+
+ // MIME CHECKING
+ // must get the content type
+ // Note: the doc gets this from the network channel during StartPageLoad,
+ // so we don't have to get it from there ourselves
+ nsAutoString mimeType;
+
+ // then lets check the mime type
+ if (RefPtr<Document> doc = aWindow.GetDoc()) {
+ doc->GetContentType(mimeType);
+
+ if (IsSupportedTextType(mimeType)) {
+ mEditorType.AssignLiteral("text");
+ mimeType.AssignLiteral("text/plain");
+ } else if (!doc->IsHTMLOrXHTML()) {
+ // Neither an acceptable text or html type.
+ mEditorStatus = eEditorErrorCantEditMimeType;
+
+ // Turn editor into HTML -- we will load blank page later
+ mEditorType.AssignLiteral("html");
+ mimeType.AssignLiteral("text/html");
+ }
+
+ // Flush out frame construction to make sure that the subframe's
+ // presshell is set up if it needs to be.
+ doc->FlushPendingNotifications(mozilla::FlushType::Frames);
+ if (mMakeWholeDocumentEditable) {
+ doc->SetEditableFlag(true);
+ // Enable usage of the execCommand API
+ doc->SetEditingState(Document::EditingState::eDesignMode);
+ }
+ }
+ bool needHTMLController = false;
+
+ if (mEditorType.EqualsLiteral("textmail")) {
+ mEditorFlags = nsIEditor::eEditorPlaintextMask |
+ nsIEditor::eEditorEnableWrapHackMask |
+ nsIEditor::eEditorMailMask;
+ } else if (mEditorType.EqualsLiteral("text")) {
+ mEditorFlags =
+ nsIEditor::eEditorPlaintextMask | nsIEditor::eEditorEnableWrapHackMask;
+ } else if (mEditorType.EqualsLiteral("htmlmail")) {
+ if (mimeType.EqualsLiteral("text/html")) {
+ needHTMLController = true;
+ mEditorFlags = nsIEditor::eEditorMailMask;
+ } else {
+ // Set the flags back to textplain.
+ mEditorFlags = nsIEditor::eEditorPlaintextMask |
+ nsIEditor::eEditorEnableWrapHackMask;
+ }
+ } else {
+ // Defaulted to html
+ needHTMLController = true;
+ }
+
+ if (mInteractive) {
+ mEditorFlags |= nsIEditor::eEditorAllowInteraction;
+ }
+
+ // make the UI state maintainer
+ RefPtr<ComposerCommandsUpdater> commandsUpdater =
+ new ComposerCommandsUpdater();
+ mComposerCommandsUpdater = commandsUpdater;
+
+ // now init the state maintainer
+ // This allows notification of error state
+ // even if we don't create an editor
+ commandsUpdater->Init(aWindow);
+
+ if (mEditorStatus != eEditorCreationInProgress) {
+ commandsUpdater->OnHTMLEditorCreated();
+
+ // At this point we have made a final decision that we don't support
+ // editing the current document. This is an internal failure state, but
+ // we return NS_OK to avoid throwing an exception from the designMode
+ // setter for web compatibility. The document editing APIs will tell the
+ // developer if editing has been disabled because we're in a document type
+ // that doesn't support editing.
+ return NS_OK;
+ }
+
+ // Create editor and do other things
+ // only if we haven't found some error above,
+ const RefPtr<nsDocShell> docShell = nsDocShell::Cast(aWindow.GetDocShell());
+ if (NS_WARN_IF(!docShell)) {
+ return NS_ERROR_FAILURE;
+ }
+ const RefPtr<PresShell> presShell = docShell->GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mInteractive) {
+ // Disable animation of images in this document:
+ nsPresContext* presContext = presShell->GetPresContext();
+ NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE);
+
+ mImageAnimationMode = presContext->ImageAnimationMode();
+ presContext->SetImageAnimationMode(imgIContainer::kDontAnimMode);
+ }
+
+ // Hide selection changes during initialization, in order to hide this
+ // from web pages.
+ RefPtr<nsFrameSelection> fs = presShell->FrameSelection();
+ NS_ENSURE_TRUE(fs, NS_ERROR_FAILURE);
+ AutoHideSelectionChanges hideSelectionChanges(fs);
+
+ nsCOMPtr<nsIContentViewer> contentViewer;
+ nsresult rv = docShell->GetContentViewer(getter_AddRefs(contentViewer));
+ if (NS_FAILED(rv) || NS_WARN_IF(!contentViewer)) {
+ NS_WARNING("nsDocShell::GetContentViewer() failed");
+ return rv;
+ }
+
+ const RefPtr<Document> doc = contentViewer->GetDocument();
+ if (NS_WARN_IF(!doc)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // create and set editor
+ // Try to reuse an existing editor
+ nsCOMPtr<nsIEditor> editor = do_QueryReferent(mExistingEditor);
+ RefPtr<HTMLEditor> htmlEditor = HTMLEditor::GetFrom(editor);
+ MOZ_ASSERT_IF(editor, htmlEditor);
+ if (htmlEditor) {
+ htmlEditor->PreDestroy();
+ } else {
+ htmlEditor = new HTMLEditor(*doc);
+ mExistingEditor =
+ do_GetWeakReference(static_cast<nsIEditor*>(htmlEditor.get()));
+ }
+ // set the editor on the docShell. The docShell now owns it.
+ rv = docShell->SetHTMLEditor(htmlEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // setup the HTML editor command controller
+ if (needHTMLController) {
+ // The third controller takes an nsIEditor as the context
+ rv = SetupEditorCommandController(
+ nsBaseCommandController::CreateHTMLEditorController, &aWindow,
+ static_cast<nsIEditor*>(htmlEditor), &mHTMLCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Set mimetype on editor
+ rv = htmlEditor->SetContentsMIMEType(mimeType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_ASSERT(docShell->HasContentViewer());
+ MOZ_ASSERT(contentViewer->GetDocument());
+
+ MOZ_DIAGNOSTIC_ASSERT(commandsUpdater == mComposerCommandsUpdater);
+ if (MOZ_UNLIKELY(commandsUpdater != mComposerCommandsUpdater)) {
+ commandsUpdater = mComposerCommandsUpdater;
+ }
+ rv = htmlEditor->Init(*doc, *commandsUpdater, mEditorFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<Selection> selection = htmlEditor->GetSelection();
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Set context on all controllers to be the editor
+ rv = SetEditorOnControllers(aWindow, htmlEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Everything went fine!
+ mEditorStatus = eEditorOK;
+
+ // This will trigger documentCreation notification
+ return htmlEditor->PostCreate();
+}
+
+// Removes all listeners and controllers from aWindow and aEditor.
+void nsEditingSession::RemoveListenersAndControllers(
+ nsPIDOMWindowOuter* aWindow, HTMLEditor* aHTMLEditor) {
+ if (!mComposerCommandsUpdater || !aHTMLEditor) {
+ return;
+ }
+
+ // Remove all the listeners
+ RefPtr<ComposerCommandsUpdater> composertCommandsUpdater =
+ std::move(mComposerCommandsUpdater);
+ MOZ_ASSERT(!mComposerCommandsUpdater);
+ aHTMLEditor->Detach(*composertCommandsUpdater);
+
+ // Remove editor controllers from the window now that we're not
+ // editing in that window any more.
+ RemoveEditorControllers(aWindow);
+}
+
+/*---------------------------------------------------------------------------
+
+ TearDownEditorOnWindow
+
+ void tearDownEditorOnWindow (in nsIDOMWindow aWindow);
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::TearDownEditorOnWindow(mozIDOMWindowProxy* aWindow) {
+ if (!mDoneSetup) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_NULL_POINTER);
+
+ // Kill any existing reload timer
+ if (mLoadBlankDocTimer) {
+ mLoadBlankDocTimer->Cancel();
+ mLoadBlankDocTimer = nullptr;
+ }
+
+ mDoneSetup = false;
+
+ // Check if we're turning off editing (from contentEditable or designMode).
+ auto* window = nsPIDOMWindowOuter::From(aWindow);
+
+ RefPtr<Document> doc = window->GetDoc();
+ bool stopEditing = doc && doc->IsEditingOn();
+ if (stopEditing) {
+ RemoveWebProgressListener(window);
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
+ NS_ENSURE_STATE(docShell);
+
+ RefPtr<HTMLEditor> htmlEditor = docShell->GetHTMLEditor();
+ if (stopEditing) {
+ doc->TearingDownEditor();
+ }
+
+ if (mComposerCommandsUpdater && htmlEditor) {
+ // Null out the editor on the controllers first to prevent their weak
+ // references from pointing to a destroyed editor.
+ SetEditorOnControllers(*window, nullptr);
+ }
+
+ // Null out the editor on the docShell to trigger PreDestroy which
+ // needs to happen before document state listeners are removed below.
+ docShell->SetEditor(nullptr);
+
+ RemoveListenersAndControllers(window, htmlEditor);
+
+ if (stopEditing) {
+ // Make things the way they were before we started editing.
+ RestoreJSAndPlugins(window->GetCurrentInnerWindow());
+ RestoreAnimationMode(window);
+
+ if (mMakeWholeDocumentEditable) {
+ doc->SetEditableFlag(false);
+ doc->SetEditingState(Document::EditingState::eOff);
+ }
+ }
+
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ GetEditorForFrame
+
+ nsIEditor getEditorForFrame (in nsIDOMWindow aWindow);
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::GetEditorForWindow(mozIDOMWindowProxy* aWindow,
+ nsIEditor** outEditor) {
+ if (NS_WARN_IF(!aWindow)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<nsIEditor> editor = GetHTMLEditorForWindow(aWindow);
+ editor.forget(outEditor);
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ OnStateChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aStateFlags,
+ nsresult aStatus) {
+#ifdef NOISY_DOC_LOADING
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ if (channel) {
+ nsAutoCString contentType;
+ channel->GetContentType(contentType);
+ if (!contentType.IsEmpty()) {
+ printf(" ++++++ MIMETYPE = %s\n", contentType.get());
+ }
+ }
+#endif
+
+ //
+ // A Request has started...
+ //
+ if (aStateFlags & nsIWebProgressListener::STATE_START) {
+#ifdef NOISY_DOC_LOADING
+ {
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ nsCString spec;
+ uri->GetSpec(spec);
+ printf(" **** STATE_START: CHANNEL URI=%s, flags=%x\n", spec.get(),
+ aStateFlags);
+ }
+ } else {
+ printf(" STATE_START: NO CHANNEL flags=%x\n", aStateFlags);
+ }
+ }
+#endif
+ // Page level notification...
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) {
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ StartPageLoad(channel);
+#ifdef NOISY_DOC_LOADING
+ printf("STATE_START & STATE_IS_NETWORK flags=%x\n", aStateFlags);
+#endif
+ }
+
+ // Document level notification...
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT &&
+ !(aStateFlags & nsIWebProgressListener::STATE_RESTORING)) {
+#ifdef NOISY_DOC_LOADING
+ printf("STATE_START & STATE_IS_DOCUMENT flags=%x\n", aStateFlags);
+#endif
+
+ bool progressIsForTargetDocument =
+ IsProgressForTargetDocument(aWebProgress);
+
+ if (progressIsForTargetDocument) {
+ nsCOMPtr<mozIDOMWindowProxy> window;
+ aWebProgress->GetDOMWindow(getter_AddRefs(window));
+
+ auto* piWindow = nsPIDOMWindowOuter::From(window);
+ RefPtr<Document> doc = piWindow->GetDoc();
+ nsHTMLDocument* htmlDoc =
+ doc && doc->IsHTMLOrXHTML() ? doc->AsHTMLDocument() : nullptr;
+ if (htmlDoc && doc->IsWriting()) {
+ nsAutoString designMode;
+ htmlDoc->GetDesignMode(designMode);
+
+ if (designMode.EqualsLiteral("on")) {
+ // This notification is for data coming in through
+ // document.open/write/close(), ignore it.
+
+ return NS_OK;
+ }
+ }
+
+ mCanCreateEditor = true;
+ StartDocumentLoad(aWebProgress, progressIsForTargetDocument);
+ }
+ }
+ }
+ //
+ // A Request is being processed
+ //
+ else if (aStateFlags & nsIWebProgressListener::STATE_TRANSFERRING) {
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) {
+ // document transfer started
+ }
+ }
+ //
+ // Got a redirection
+ //
+ else if (aStateFlags & nsIWebProgressListener::STATE_REDIRECTING) {
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) {
+ // got a redirect
+ }
+ }
+ //
+ // A network or document Request has finished...
+ //
+ else if (aStateFlags & nsIWebProgressListener::STATE_STOP) {
+#ifdef NOISY_DOC_LOADING
+ {
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ nsCString spec;
+ uri->GetSpec(spec);
+ printf(" **** STATE_STOP: CHANNEL URI=%s, flags=%x\n", spec.get(),
+ aStateFlags);
+ }
+ } else {
+ printf(" STATE_STOP: NO CHANNEL flags=%x\n", aStateFlags);
+ }
+ }
+#endif
+
+ // Document level notification...
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ EndDocumentLoad(aWebProgress, channel, aStatus,
+ IsProgressForTargetDocument(aWebProgress));
+#ifdef NOISY_DOC_LOADING
+ printf("STATE_STOP & STATE_IS_DOCUMENT flags=%x\n", aStateFlags);
+#endif
+ }
+
+ // Page level notification...
+ if (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ (void)EndPageLoad(aWebProgress, channel, aStatus);
+#ifdef NOISY_DOC_LOADING
+ printf("STATE_STOP & STATE_IS_NETWORK flags=%x\n", aStateFlags);
+#endif
+ }
+ }
+
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ OnProgressChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ OnLocationChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsIURI* aURI,
+ uint32_t aFlags) {
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ nsresult rv = aWebProgress->GetDOMWindow(getter_AddRefs(domWindow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto* piWindow = nsPIDOMWindowOuter::From(domWindow);
+
+ RefPtr<Document> doc = piWindow->GetDoc();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ doc->SetDocumentURI(aURI);
+
+ // Notify the location-changed observer that
+ // the document URL has changed
+ nsIDocShell* docShell = piWindow->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ RefPtr<nsCommandManager> commandManager = docShell->GetCommandManager();
+ commandManager->CommandStatusChanged("obs_documentLocationChanged");
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ OnStatusChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ OnSecurityChange
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aState) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ OnContentBlockingEvent
+
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ IsProgressForTargetDocument
+
+ Check that this notification is for our document.
+----------------------------------------------------------------------------*/
+
+bool nsEditingSession::IsProgressForTargetDocument(
+ nsIWebProgress* aWebProgress) {
+ nsCOMPtr<nsIWebProgress> editedWebProgress = do_QueryReferent(mDocShell);
+ return editedWebProgress == aWebProgress;
+}
+
+/*---------------------------------------------------------------------------
+
+ GetEditorStatus
+
+ Called during GetCommandStateParams("obs_documentCreated"...)
+ to determine if editor was created and document
+ was loaded successfully
+----------------------------------------------------------------------------*/
+NS_IMETHODIMP
+nsEditingSession::GetEditorStatus(uint32_t* aStatus) {
+ NS_ENSURE_ARG_POINTER(aStatus);
+ *aStatus = mEditorStatus;
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ StartDocumentLoad
+
+ Called on start of load in a single frame
+----------------------------------------------------------------------------*/
+nsresult nsEditingSession::StartDocumentLoad(nsIWebProgress* aWebProgress,
+ bool aIsToBeMadeEditable) {
+#ifdef NOISY_DOC_LOADING
+ printf("======= StartDocumentLoad ========\n");
+#endif
+
+ NS_ENSURE_ARG_POINTER(aWebProgress);
+
+ if (aIsToBeMadeEditable) {
+ mEditorStatus = eEditorCreationInProgress;
+ }
+
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ EndDocumentLoad
+
+ Called on end of load in a single frame
+----------------------------------------------------------------------------*/
+nsresult nsEditingSession::EndDocumentLoad(nsIWebProgress* aWebProgress,
+ nsIChannel* aChannel,
+ nsresult aStatus,
+ bool aIsToBeMadeEditable) {
+ NS_ENSURE_ARG_POINTER(aWebProgress);
+
+#ifdef NOISY_DOC_LOADING
+ printf("======= EndDocumentLoad ========\n");
+ printf("with status %d, ", aStatus);
+ nsCOMPtr<nsIURI> uri;
+ nsCString spec;
+ if (NS_SUCCEEDED(aChannel->GetURI(getter_AddRefs(uri)))) {
+ uri->GetSpec(spec);
+ printf(" uri %s\n", spec.get());
+ }
+#endif
+
+ // We want to call the base class EndDocumentLoad,
+ // but avoid some of the stuff
+ // that nsDocShell does (need to refactor).
+
+ // OK, time to make an editor on this document
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ aWebProgress->GetDOMWindow(getter_AddRefs(domWindow));
+ NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE);
+
+ // Set the error state -- we will create an editor
+ // anyway and load empty doc later
+ if (aIsToBeMadeEditable && aStatus == NS_ERROR_FILE_NOT_FOUND) {
+ mEditorStatus = eEditorErrorFileNotFound;
+ }
+
+ auto* window = nsPIDOMWindowOuter::From(domWindow);
+ nsIDocShell* docShell = window->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); // better error handling?
+
+ // cancel refresh from meta tags
+ // we need to make sure that all pages in editor (whether editable or not)
+ // can't refresh contents being edited
+ nsCOMPtr<nsIRefreshURI> refreshURI = do_QueryInterface(docShell);
+ if (refreshURI) {
+ refreshURI->CancelRefreshURITimers();
+ }
+
+ nsresult rv = NS_OK;
+
+ // did someone set the flag to make this shell editable?
+ if (aIsToBeMadeEditable && mCanCreateEditor) {
+ bool makeEditable;
+ docShell->GetEditable(&makeEditable);
+
+ if (makeEditable) {
+ // To keep pre Gecko 1.9 behavior, setup editor always when
+ // mMakeWholeDocumentEditable.
+ bool needsSetup = false;
+ if (mMakeWholeDocumentEditable) {
+ needsSetup = true;
+ } else {
+ // do we already have an editor here?
+ needsSetup = !docShell->GetHTMLEditor();
+ }
+
+ if (needsSetup) {
+ mCanCreateEditor = false;
+ rv = SetupEditorOnWindow(MOZ_KnownLive(*window));
+ if (NS_FAILED(rv)) {
+ // If we had an error, setup timer to load a blank page later
+ if (mLoadBlankDocTimer) {
+ // Must cancel previous timer?
+ mLoadBlankDocTimer->Cancel();
+ mLoadBlankDocTimer = nullptr;
+ }
+
+ rv = NS_NewTimerWithFuncCallback(getter_AddRefs(mLoadBlankDocTimer),
+ nsEditingSession::TimerCallback,
+ static_cast<void*>(mDocShell.get()),
+ 10, nsITimer::TYPE_ONE_SHOT,
+ "nsEditingSession::EndDocumentLoad");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mEditorStatus = eEditorCreationInProgress;
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+void nsEditingSession::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ nsCOMPtr<nsIDocShell> docShell =
+ do_QueryReferent(static_cast<nsIWeakReference*>(aClosure));
+ if (docShell) {
+ nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(docShell));
+ if (webNav) {
+ LoadURIOptions loadURIOptions;
+ loadURIOptions.mTriggeringPrincipal =
+ nsContentUtils::GetSystemPrincipal();
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), "about:blank"_ns));
+ webNav->LoadURI(uri, loadURIOptions);
+ }
+ }
+}
+
+/*---------------------------------------------------------------------------
+
+ StartPageLoad
+
+ Called on start load of the entire page (incl. subframes)
+----------------------------------------------------------------------------*/
+nsresult nsEditingSession::StartPageLoad(nsIChannel* aChannel) {
+#ifdef NOISY_DOC_LOADING
+ printf("======= StartPageLoad ========\n");
+#endif
+ return NS_OK;
+}
+
+/*---------------------------------------------------------------------------
+
+ EndPageLoad
+
+ Called on end load of the entire page (incl. subframes)
+----------------------------------------------------------------------------*/
+nsresult nsEditingSession::EndPageLoad(nsIWebProgress* aWebProgress,
+ nsIChannel* aChannel, nsresult aStatus) {
+#ifdef NOISY_DOC_LOADING
+ printf("======= EndPageLoad ========\n");
+ printf(" with status %d, ", aStatus);
+ nsCOMPtr<nsIURI> uri;
+ nsCString spec;
+ if (NS_SUCCEEDED(aChannel->GetURI(getter_AddRefs(uri)))) {
+ uri->GetSpec(spec);
+ printf("uri %s\n", spec.get());
+ }
+
+ nsAutoCString contentType;
+ aChannel->GetContentType(contentType);
+ if (!contentType.IsEmpty()) {
+ printf(" flags = %d, status = %d, MIMETYPE = %s\n", mEditorFlags,
+ mEditorStatus, contentType.get());
+ }
+#endif
+
+ // Set the error state -- we will create an editor anyway
+ // and load empty doc later
+ if (aStatus == NS_ERROR_FILE_NOT_FOUND) {
+ mEditorStatus = eEditorErrorFileNotFound;
+ }
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ aWebProgress->GetDOMWindow(getter_AddRefs(domWindow));
+
+ nsIDocShell* docShell =
+ domWindow ? nsPIDOMWindowOuter::From(domWindow)->GetDocShell() : nullptr;
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ // cancel refresh from meta tags
+ // we need to make sure that all pages in editor (whether editable or not)
+ // can't refresh contents being edited
+ nsCOMPtr<nsIRefreshURI> refreshURI = do_QueryInterface(docShell);
+ if (refreshURI) {
+ refreshURI->CancelRefreshURITimers();
+ }
+
+#if 0
+ // Shouldn't we do this when we want to edit sub-frames?
+ return MakeWindowEditable(domWindow, "html", false, mInteractive);
+#else
+ return NS_OK;
+#endif
+}
+
+/*---------------------------------------------------------------------------
+
+ PrepareForEditing
+
+ Set up this editing session for one or more editors
+----------------------------------------------------------------------------*/
+nsresult nsEditingSession::PrepareForEditing(nsPIDOMWindowOuter* aWindow) {
+ if (mProgressListenerRegistered) {
+ return NS_OK;
+ }
+
+ nsIDocShell* docShell = aWindow ? aWindow->GetDocShell() : nullptr;
+
+ // register callback
+ nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell);
+ NS_ENSURE_TRUE(webProgress, NS_ERROR_FAILURE);
+
+ nsresult rv = webProgress->AddProgressListener(
+ this, (nsIWebProgress::NOTIFY_STATE_NETWORK |
+ nsIWebProgress::NOTIFY_STATE_DOCUMENT |
+ nsIWebProgress::NOTIFY_LOCATION));
+
+ mProgressListenerRegistered = NS_SUCCEEDED(rv);
+
+ return rv;
+}
+
+/*---------------------------------------------------------------------------
+
+ SetupEditorCommandController
+
+ Create a command controller, append to controllers,
+ get and return the controller ID, and set the context
+----------------------------------------------------------------------------*/
+nsresult nsEditingSession::SetupEditorCommandController(
+ nsEditingSession::ControllerCreatorFn aControllerCreatorFn,
+ mozIDOMWindowProxy* aWindow, nsISupports* aContext,
+ uint32_t* aControllerId) {
+ NS_ENSURE_ARG_POINTER(aControllerCreatorFn);
+ NS_ENSURE_ARG_POINTER(aWindow);
+ NS_ENSURE_ARG_POINTER(aContext);
+ NS_ENSURE_ARG_POINTER(aControllerId);
+
+ auto* piWindow = nsPIDOMWindowOuter::From(aWindow);
+ MOZ_ASSERT(piWindow);
+
+ nsCOMPtr<nsIControllers> controllers;
+ nsresult rv = piWindow->GetControllers(getter_AddRefs(controllers));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We only have to create each singleton controller once
+ // We know this has happened once we have a controllerId value
+ if (!*aControllerId) {
+ RefPtr<nsBaseCommandController> commandController = aControllerCreatorFn();
+ NS_ENSURE_TRUE(commandController, NS_ERROR_FAILURE);
+
+ // We must insert at head of the list to be sure our
+ // controller is found before other implementations
+ // (e.g., not-implemented versions by browser)
+ rv = controllers->InsertControllerAt(0, commandController);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remember the ID for the controller
+ rv = controllers->GetControllerId(commandController, aControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Set the context
+ return SetContextOnControllerById(controllers, aContext, *aControllerId);
+}
+
+nsresult nsEditingSession::SetEditorOnControllers(nsPIDOMWindowOuter& aWindow,
+ HTMLEditor* aEditor) {
+ nsCOMPtr<nsIControllers> controllers;
+ nsresult rv = aWindow.GetControllers(getter_AddRefs(controllers));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> editorAsISupports = static_cast<nsIEditor*>(aEditor);
+ if (mBaseCommandControllerId) {
+ rv = SetContextOnControllerById(controllers, editorAsISupports,
+ mBaseCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mDocStateControllerId) {
+ rv = SetContextOnControllerById(controllers, editorAsISupports,
+ mDocStateControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mHTMLCommandControllerId) {
+ rv = SetContextOnControllerById(controllers, editorAsISupports,
+ mHTMLCommandControllerId);
+ }
+
+ return rv;
+}
+
+nsresult nsEditingSession::SetContextOnControllerById(
+ nsIControllers* aControllers, nsISupports* aContext, uint32_t aID) {
+ NS_ENSURE_ARG_POINTER(aControllers);
+
+ // aContext can be null (when destroying editor)
+ nsCOMPtr<nsIController> controller;
+ aControllers->GetControllerById(aID, getter_AddRefs(controller));
+
+ // ok with nil controller
+ nsCOMPtr<nsIControllerContext> editorController =
+ do_QueryInterface(controller);
+ NS_ENSURE_TRUE(editorController, NS_ERROR_FAILURE);
+
+ return editorController->SetCommandContext(aContext);
+}
+
+void nsEditingSession::RemoveEditorControllers(nsPIDOMWindowOuter* aWindow) {
+ // Remove editor controllers from the aWindow, call when we're
+ // tearing down/detaching editor.
+
+ nsCOMPtr<nsIControllers> controllers;
+ if (aWindow) {
+ aWindow->GetControllers(getter_AddRefs(controllers));
+ }
+
+ if (controllers) {
+ nsCOMPtr<nsIController> controller;
+ if (mBaseCommandControllerId) {
+ controllers->GetControllerById(mBaseCommandControllerId,
+ getter_AddRefs(controller));
+ if (controller) {
+ controllers->RemoveController(controller);
+ }
+ }
+
+ if (mDocStateControllerId) {
+ controllers->GetControllerById(mDocStateControllerId,
+ getter_AddRefs(controller));
+ if (controller) {
+ controllers->RemoveController(controller);
+ }
+ }
+
+ if (mHTMLCommandControllerId) {
+ controllers->GetControllerById(mHTMLCommandControllerId,
+ getter_AddRefs(controller));
+ if (controller) {
+ controllers->RemoveController(controller);
+ }
+ }
+ }
+
+ // Clear IDs to trigger creation of new controllers.
+ mBaseCommandControllerId = 0;
+ mDocStateControllerId = 0;
+ mHTMLCommandControllerId = 0;
+}
+
+void nsEditingSession::RemoveWebProgressListener(nsPIDOMWindowOuter* aWindow) {
+ nsIDocShell* docShell = aWindow ? aWindow->GetDocShell() : nullptr;
+ nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell);
+ if (webProgress) {
+ webProgress->RemoveProgressListener(this);
+ mProgressListenerRegistered = false;
+ }
+}
+
+void nsEditingSession::RestoreAnimationMode(nsPIDOMWindowOuter* aWindow) {
+ if (mInteractive) {
+ return;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = aWindow ? aWindow->GetDocShell() : nullptr;
+ NS_ENSURE_TRUE_VOID(docShell);
+ RefPtr<PresShell> presShell = docShell->GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return;
+ }
+ nsPresContext* presContext = presShell->GetPresContext();
+ NS_ENSURE_TRUE_VOID(presContext);
+
+ presContext->SetImageAnimationMode(mImageAnimationMode);
+}
+
+nsresult nsEditingSession::DetachFromWindow(nsPIDOMWindowOuter* aWindow) {
+ NS_ENSURE_TRUE(mDoneSetup, NS_OK);
+
+ NS_ASSERTION(mComposerCommandsUpdater,
+ "mComposerCommandsUpdater should exist.");
+
+ // Kill any existing reload timer
+ if (mLoadBlankDocTimer) {
+ mLoadBlankDocTimer->Cancel();
+ mLoadBlankDocTimer = nullptr;
+ }
+
+ // Remove controllers, webprogress listener, and otherwise
+ // make things the way they were before we started editing.
+ RemoveEditorControllers(aWindow);
+ RemoveWebProgressListener(aWindow);
+ RestoreJSAndPlugins(aWindow->GetCurrentInnerWindow());
+ RestoreAnimationMode(aWindow);
+
+ // Kill our weak reference to our original window, in case
+ // it changes on restore, or otherwise dies.
+ mDocShell = nullptr;
+
+ return NS_OK;
+}
+
+nsresult nsEditingSession::ReattachToWindow(nsPIDOMWindowOuter* aWindow) {
+ NS_ENSURE_TRUE(mDoneSetup, NS_OK);
+ NS_ENSURE_TRUE(aWindow, NS_ERROR_FAILURE);
+
+ NS_ASSERTION(mComposerCommandsUpdater,
+ "mComposerCommandsUpdater should exist.");
+
+ // Imitate nsEditorDocShell::MakeEditable() to reattach the
+ // old editor to the window.
+ nsresult rv;
+
+ nsIDocShell* docShell = aWindow->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+ mDocShell = do_GetWeakReference(docShell);
+
+ // Disable plugins.
+ if (!mInteractive) {
+ rv = DisableJSAndPlugins(aWindow->GetCurrentInnerWindow());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Tells embedder that startup is in progress.
+ mEditorStatus = eEditorCreationInProgress;
+
+ // Adds back web progress listener.
+ rv = PrepareForEditing(aWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Setup the command controllers again.
+ rv = SetupEditorCommandController(
+ nsBaseCommandController::CreateEditingController, aWindow,
+ static_cast<nsIEditingSession*>(this), &mBaseCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetupEditorCommandController(
+ nsBaseCommandController::CreateHTMLEditorDocStateController, aWindow,
+ static_cast<nsIEditingSession*>(this), &mDocStateControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mComposerCommandsUpdater) {
+ mComposerCommandsUpdater->Init(*aWindow);
+ }
+
+ // Get editor
+ RefPtr<HTMLEditor> htmlEditor = GetHTMLEditorForWindow(aWindow);
+ if (NS_WARN_IF(!htmlEditor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mInteractive) {
+ // Disable animation of images in this document:
+ RefPtr<PresShell> presShell = docShell->GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsPresContext* presContext = presShell->GetPresContext();
+ NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE);
+
+ mImageAnimationMode = presContext->ImageAnimationMode();
+ presContext->SetImageAnimationMode(imgIContainer::kDontAnimMode);
+ }
+
+ // The third controller takes an nsIEditor as the context
+ rv = SetupEditorCommandController(
+ nsBaseCommandController::CreateHTMLEditorController, aWindow,
+ static_cast<nsIEditor*>(htmlEditor.get()), &mHTMLCommandControllerId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set context on all controllers to be the editor
+ rv = SetEditorOnControllers(*aWindow, htmlEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef DEBUG
+ {
+ bool isEditable;
+ rv = WindowIsEditable(aWindow, &isEditable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(isEditable,
+ "Window is not editable after reattaching editor.");
+ }
+#endif // DEBUG
+
+ return NS_OK;
+}
+
+HTMLEditor* nsIEditingSession::GetHTMLEditorForWindow(
+ mozIDOMWindowProxy* aWindow) {
+ if (NS_WARN_IF(!aWindow)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell =
+ nsPIDOMWindowOuter::From(aWindow)->GetDocShell();
+ if (NS_WARN_IF(!docShell)) {
+ return nullptr;
+ }
+
+ return docShell->GetHTMLEditor();
+}
diff --git a/editor/composer/nsEditingSession.h b/editor/composer/nsEditingSession.h
new file mode 100644
index 0000000000..c808c71c79
--- /dev/null
+++ b/editor/composer/nsEditingSession.h
@@ -0,0 +1,172 @@
+/* -*- 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 nsEditingSession_h__
+#define nsEditingSession_h__
+
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS
+#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
+#include "nsWeakReference.h" // for nsSupportsWeakReference, etc
+#include "nscore.h" // for nsresult
+
+#ifndef __gen_nsIWebProgressListener_h__
+# include "nsIWebProgressListener.h"
+#endif
+
+#ifndef __gen_nsIEditingSession_h__
+# include "nsIEditingSession.h" // for NS_DECL_NSIEDITINGSESSION, etc
+#endif
+
+#include "nsString.h" // for nsCString
+
+class mozIDOMWindowProxy;
+class nsBaseCommandController;
+class nsIDOMWindow;
+class nsISupports;
+class nsITimer;
+class nsIChannel;
+class nsIControllers;
+class nsIDocShell;
+class nsIWebProgress;
+class nsIPIDOMWindowOuter;
+class nsIPIDOMWindowInner;
+
+namespace mozilla {
+class ComposerCommandsUpdater;
+class HTMLEditor;
+} // namespace mozilla
+
+class nsEditingSession final : public nsIEditingSession,
+ public nsIWebProgressListener,
+ public nsSupportsWeakReference {
+ public:
+ nsEditingSession();
+
+ // nsISupports
+ NS_DECL_ISUPPORTS
+
+ // nsIWebProgressListener
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+ // nsIEditingSession
+ NS_DECL_NSIEDITINGSESSION
+
+ /**
+ * Removes all the editor's controllers/listeners etc and makes the window
+ * uneditable.
+ */
+ nsresult DetachFromWindow(nsPIDOMWindowOuter* aWindow);
+
+ /**
+ * Undos DetachFromWindow(), reattaches this editing session/editor
+ * to the window.
+ */
+ nsresult ReattachToWindow(nsPIDOMWindowOuter* aWindow);
+
+ protected:
+ virtual ~nsEditingSession();
+
+ typedef already_AddRefed<nsBaseCommandController> (*ControllerCreatorFn)();
+
+ nsresult SetupEditorCommandController(
+ ControllerCreatorFn aControllerCreatorFn, mozIDOMWindowProxy* aWindow,
+ nsISupports* aContext, uint32_t* aControllerId);
+
+ nsresult SetContextOnControllerById(nsIControllers* aControllers,
+ nsISupports* aContext, uint32_t aID);
+
+ /**
+ * Set the editor on the controller(s) for this window
+ */
+ nsresult SetEditorOnControllers(nsPIDOMWindowOuter& aWindow,
+ mozilla::HTMLEditor* aEditor);
+
+ /**
+ * Setup editor and related support objects
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetupEditorOnWindow(nsPIDOMWindowOuter& aWindow);
+
+ nsresult PrepareForEditing(nsPIDOMWindowOuter* aWindow);
+
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+ nsCOMPtr<nsITimer> mLoadBlankDocTimer;
+
+ // progress load stuff
+ nsresult StartDocumentLoad(nsIWebProgress* aWebProgress,
+ bool isToBeMadeEditable);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult EndDocumentLoad(nsIWebProgress* aWebProgress, nsIChannel* aChannel,
+ nsresult aStatus, bool isToBeMadeEditable);
+ nsresult StartPageLoad(nsIChannel* aChannel);
+ nsresult EndPageLoad(nsIWebProgress* aWebProgress, nsIChannel* aChannel,
+ nsresult aStatus);
+
+ bool IsProgressForTargetDocument(nsIWebProgress* aWebProgress);
+
+ void RemoveEditorControllers(nsPIDOMWindowOuter* aWindow);
+ void RemoveWebProgressListener(nsPIDOMWindowOuter* aWindow);
+ void RestoreAnimationMode(nsPIDOMWindowOuter* aWindow);
+ void RemoveListenersAndControllers(nsPIDOMWindowOuter* aWindow,
+ mozilla::HTMLEditor* aHTMLEditor);
+
+ /**
+ * Disable scripts and plugins in aDocShell.
+ */
+ nsresult DisableJSAndPlugins(nsPIDOMWindowInner* aWindow);
+
+ /**
+ * Restore JS and plugins (enable/disable them) according to the state they
+ * were before the last call to disableJSAndPlugins.
+ */
+ nsresult RestoreJSAndPlugins(nsPIDOMWindowInner* aWindow);
+
+ protected:
+ bool mDoneSetup; // have we prepared for editing yet?
+
+ // Used to prevent double creation of editor because nsIWebProgressListener
+ // receives a STATE_STOP notification before the STATE_START
+ // for our document, so we wait for the STATE_START, then STATE_STOP
+ // before creating an editor
+ bool mCanCreateEditor;
+
+ bool mInteractive;
+ bool mMakeWholeDocumentEditable;
+
+ bool mDisabledJSAndPlugins;
+
+ // True if scripts were enabled before the editor turned scripts
+ // off, otherwise false.
+ bool mScriptsEnabled;
+
+ // True if plugins were enabled before the editor turned plugins
+ // off, otherwise false.
+ bool mPluginsEnabled;
+
+ bool mProgressListenerRegistered;
+
+ // The image animation mode before it was turned off.
+ uint16_t mImageAnimationMode;
+
+ // THE REMAINING MEMBER VARIABLES WILL BECOME A SET WHEN WE EDIT
+ // MORE THAN ONE EDITOR PER EDITING SESSION
+ RefPtr<mozilla::ComposerCommandsUpdater> mComposerCommandsUpdater;
+
+ // Save the editor type so we can create the editor after loading uri
+ nsCString mEditorType;
+ uint32_t mEditorFlags;
+ uint32_t mEditorStatus;
+ uint32_t mBaseCommandControllerId;
+ uint32_t mDocStateControllerId;
+ uint32_t mHTMLCommandControllerId;
+
+ // Make sure the docshell we use is safe
+ nsWeakPtr mDocShell;
+
+ // See if we can reuse an existing editor
+ nsWeakPtr mExistingEditor;
+};
+
+#endif // nsEditingSession_h__
diff --git a/editor/composer/nsIEditingSession.idl b/editor/composer/nsIEditingSession.idl
new file mode 100644
index 0000000000..9691092ce8
--- /dev/null
+++ b/editor/composer/nsIEditingSession.idl
@@ -0,0 +1,84 @@
+/* -*- 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 "nsISupports.idl"
+#include "domstubs.idl"
+
+interface mozIDOMWindowProxy;
+interface nsIEditor;
+
+%{ C++
+class mozIDOMWindowProxy;
+namespace mozilla {
+class HTMLEditor;
+} // namespace mozilla
+%}
+
+[scriptable, builtinclass, uuid(24f963d1-e6fc-43ea-a206-99ac5fcc5265)]
+interface nsIEditingSession : nsISupports
+{
+ /**
+ * Error codes when we fail to create an editor
+ * is placed in attribute editorStatus
+ */
+ const long eEditorOK = 0;
+ const long eEditorCreationInProgress = 1;
+ const long eEditorErrorCantEditMimeType = 2;
+ const long eEditorErrorFileNotFound = 3;
+ const long eEditorErrorCantEditFramesets = 8;
+ const long eEditorErrorUnknown = 9;
+
+ /**
+ * Status after editor creation and document loading
+ * Value is one of the above error codes
+ */
+ readonly attribute unsigned long editorStatus;
+
+ /**
+ * Make this window editable
+ * @param aWindow nsIDOMWindow, the window the embedder needs to make editable
+ * @param aEditorType string, "html" "htmlsimple" "text" "textsimple"
+ * @param aMakeWholeDocumentEditable if PR_TRUE make the whole document in
+ * aWindow editable, otherwise it's the
+ * embedder who should make the document
+ * (or part of it) editable.
+ * @param aInteractive if PR_FALSE turn off scripting and plugins
+ */
+ [can_run_script]
+ void makeWindowEditable(in mozIDOMWindowProxy window,
+ in string aEditorType,
+ in boolean doAfterUriLoad,
+ in boolean aMakeWholeDocumentEditable,
+ in boolean aInteractive);
+
+ /**
+ * Test whether a specific window has had its editable flag set; it may have an editor
+ * now, or will get one after the uri load.
+ *
+ * Use this, passing the content root window, to test if we've set up editing
+ * for this content.
+ */
+ boolean windowIsEditable(in mozIDOMWindowProxy window);
+
+ /**
+ * Get the editor for this window. May return null
+ */
+ nsIEditor getEditorForWindow(in mozIDOMWindowProxy window);
+
+ /**
+ * Destroy editor and related support objects
+ */
+ [noscript] void tearDownEditorOnWindow(in mozIDOMWindowProxy window);
+
+%{C++
+ /**
+ * This method is implemented with nsIDocShell::GetHTMLEditor(). I.e.,
+ * This method doesn't depend on nsEditingSession. Therefore, even if
+ * there were some implementation of nsIEditingSession interface, this
+ * would be safe to use.
+ */
+ mozilla::HTMLEditor* GetHTMLEditorForWindow(mozIDOMWindowProxy* aWindow);
+%}
+};
diff --git a/editor/composer/res/EditorOverride.css b/editor/composer/res/EditorOverride.css
new file mode 100644
index 0000000000..373ed869a1
--- /dev/null
+++ b/editor/composer/res/EditorOverride.css
@@ -0,0 +1,322 @@
+/* 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/. */
+
+*|* {
+ -moz-user-modify: read-write;
+}
+
+/* Styles to alter look of things in the Editor content window
+ * that should NOT be removed when we display in completely WYSIWYG
+ * "Browser Preview" mode.
+ * Anything that should change, like appearance of table borders
+ * and Named Anchors, should be placed in EditorContent.css instead of here.
+*/
+
+/* Primary cursor is text I-beam */
+
+::-moz-canvas, a:link {
+ cursor: text;
+}
+
+/* Use default arrow over objects with size that
+ are selected when clicked on.
+ Override the browser's pointer cursor over links
+*/
+
+img, img[usemap], area,
+object, object[usemap],
+applet, hr, button, input, textarea, select,
+a:link img, a:visited img, a:active img,
+a[name]:-moz-only-whitespace {
+ cursor: default;
+}
+
+a:visited, a:active {
+ cursor: text;
+}
+
+/* Prevent clicking on links from going to link */
+a:link img, a:visited img {
+ -moz-user-input: none;
+}
+
+/* We suppress user/author's prefs for link underline,
+ so we must set explicitly. This isn't good!
+*/
+a:link {
+ color: -moz-hyperlinktext;
+}
+
+/* Allow double-clicks on these widgets to open properties dialogs
+ XXX except when the widget has disabled attribute */
+input, button, textarea {
+ user-select: all !important;
+ -moz-user-input: auto !important;
+ -moz-user-focus: none !important;
+}
+
+/* XXX Still need a better way of blocking other events to these widgets */
+select, input[disabled], input[type="checkbox"], input[type="radio"], input[type="file"] {
+ user-select: all !important;
+ -moz-user-input: none !important;
+ -moz-user-focus: none !important;
+}
+
+input[type="hidden"] {
+ border: 1px solid black !important;
+ visibility: visible !important;
+}
+
+label {
+ user-select: all !important;
+}
+
+::-moz-display-comboboxcontrol-frame {
+ user-select: text !important;
+}
+
+option {
+ user-select: text !important;
+}
+
+#mozToc.readonly {
+ user-select: all !important;
+ -moz-user-input: none !important;
+}
+
+/* the following rules are for Image Resizing */
+
+span[\_moz_anonclass="mozResizer"] {
+ width: 5px;
+ height: 5px;
+ position: absolute;
+ border: 1px black solid;
+ background-color: white;
+ user-select: none;
+ z-index: 2147483646; /* max value -1 for this property */
+}
+
+/* we can't use :active below */
+span[\_moz_anonclass="mozResizer"][\_moz_activated],
+span[\_moz_anonclass="mozResizer"]:hover {
+ background-color: black;
+}
+
+span[\_moz_anonclass="mozResizer"].hidden,
+span[\_moz_anonclass="mozResizingShadow"].hidden,
+img[\_moz_anonclass="mozResizingShadow"].hidden,
+span[\_moz_anonclass="mozGrabber"].hidden,
+span[\_moz_anonclass="mozResizingInfo"].hidden,
+a[\_moz_anonclass="mozTableRemoveRow"].hidden,
+a[\_moz_anonclass="mozTableRemoveColumn"].hidden {
+ display: none !important;
+}
+
+span[\_moz_anonclass="mozResizer"][anonlocation="nw"] {
+ cursor: nw-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="n"] {
+ cursor: n-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="ne"] {
+ cursor: ne-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="w"] {
+ cursor: w-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="e"] {
+ cursor: e-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="sw"] {
+ cursor: sw-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="s"] {
+ cursor: s-resize;
+}
+span[\_moz_anonclass="mozResizer"][anonlocation="se"] {
+ cursor: se-resize;
+}
+
+span[\_moz_anonclass="mozResizingShadow"],
+img[\_moz_anonclass="mozResizingShadow"] {
+ outline: thin dashed black;
+ user-select: none;
+ opacity: 0.5;
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+}
+
+span[\_moz_anonclass="mozResizingInfo"] {
+ font-family: sans-serif;
+ font-size: x-small;
+ color: black;
+ background-color: #d0d0d0;
+ border: ridge 2px #d0d0d0;
+ padding: 2px;
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+}
+
+img[\_moz_resizing] {
+ outline: thin solid black;
+}
+
+*[\_moz_abspos] {
+ outline: silver ridge 2px;
+ z-index: 2147483645 !important; /* max value -2 for this property */
+}
+*[\_moz_abspos="white"] {
+ background-color: white !important;
+}
+*[\_moz_abspos="black"] {
+ background-color: black !important;
+}
+
+span[\_moz_anonclass="mozGrabber"] {
+ outline: ridge 2px silver;
+ padding: 2px;
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ background-image: url("resource://gre/res/grabber.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ user-select: none;
+ cursor: move;
+ z-index: 2147483647; /* max value for this property */
+}
+
+/* INLINE TABLE EDITING */
+
+a[\_moz_anonclass="mozTableAddColumnBefore"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 4px;
+ height: 8px;
+ background-image: url("resource://gre/res/table-add-column-before.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableAddColumnBefore"]:hover {
+ background-image: url("resource://gre/res/table-add-column-before-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableAddColumnBefore"]:active {
+ background-image: url("resource://gre/res/table-add-column-before-active.gif");
+}
+
+a[\_moz_anonclass="mozTableAddColumnAfter"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 4px;
+ height: 8px;
+ background-image: url("resource://gre/res/table-add-column-after.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableAddColumnAfter"]:hover {
+ background-image: url("resource://gre/res/table-add-column-after-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableAddColumnAfter"]:active {
+ background-image: url("resource://gre/res/table-add-column-after-active.gif");
+}
+
+a[\_moz_anonclass="mozTableRemoveColumn"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 8px;
+ height: 8px;
+ background-image: url("resource://gre/res/table-remove-column.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableRemoveColumn"]:hover {
+ background-image: url("resource://gre/res/table-remove-column-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableRemoveColumn"]:active {
+ background-image: url("resource://gre/res/table-remove-column-active.gif");
+}
+
+a[\_moz_anonclass="mozTableAddRowBefore"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 8px;
+ height: 4px;
+ background-image: url("resource://gre/res/table-add-row-before.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableAddRowBefore"]:hover {
+ background-image: url("resource://gre/res/table-add-row-before-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableAddRowBefore"]:active {
+ background-image: url("resource://gre/res/table-add-row-before-active.gif");
+}
+
+a[\_moz_anonclass="mozTableAddRowAfter"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 8px;
+ height: 4px;
+ background-image: url("resource://gre/res/table-add-row-after.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableAddRowAfter"]:hover {
+ background-image: url("resource://gre/res/table-add-row-after-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableAddRowAfter"]:active {
+ background-image: url("resource://gre/res/table-add-row-after-active.gif");
+}
+
+a[\_moz_anonclass="mozTableRemoveRow"] {
+ position: absolute;
+ z-index: 2147483647; /* max value for this property */
+ text-decoration: none !important;
+ border: none 0px !important;
+ width: 8px;
+ height: 8px;
+ background-image: url("resource://gre/res/table-remove-row.gif");
+ background-repeat: no-repeat;
+ background-position: center center;
+ user-select: none !important;
+ -moz-user-focus: none !important;
+}
+
+a[\_moz_anonclass="mozTableRemoveRow"]:hover {
+ background-image: url("resource://gre/res/table-remove-row-hover.gif");
+}
+
+a[\_moz_anonclass="mozTableRemoveRow"]:active {
+ background-image: url("resource://gre/res/table-remove-row-active.gif");
+}
diff --git a/editor/composer/res/grabber.gif b/editor/composer/res/grabber.gif
new file mode 100644
index 0000000000..06749a64fc
--- /dev/null
+++ b/editor/composer/res/grabber.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-after-active.gif b/editor/composer/res/table-add-column-after-active.gif
new file mode 100644
index 0000000000..3ec50b82ee
--- /dev/null
+++ b/editor/composer/res/table-add-column-after-active.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-after-hover.gif b/editor/composer/res/table-add-column-after-hover.gif
new file mode 100644
index 0000000000..29679f9812
--- /dev/null
+++ b/editor/composer/res/table-add-column-after-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-after.gif b/editor/composer/res/table-add-column-after.gif
new file mode 100644
index 0000000000..8891be969c
--- /dev/null
+++ b/editor/composer/res/table-add-column-after.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-before-active.gif b/editor/composer/res/table-add-column-before-active.gif
new file mode 100644
index 0000000000..1e205291e1
--- /dev/null
+++ b/editor/composer/res/table-add-column-before-active.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-before-hover.gif b/editor/composer/res/table-add-column-before-hover.gif
new file mode 100644
index 0000000000..7b54537e40
--- /dev/null
+++ b/editor/composer/res/table-add-column-before-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-add-column-before.gif b/editor/composer/res/table-add-column-before.gif
new file mode 100644
index 0000000000..d4a3ffe5e9
--- /dev/null
+++ b/editor/composer/res/table-add-column-before.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-after-active.gif b/editor/composer/res/table-add-row-after-active.gif
new file mode 100644
index 0000000000..cc01da2c9e
--- /dev/null
+++ b/editor/composer/res/table-add-row-after-active.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-after-hover.gif b/editor/composer/res/table-add-row-after-hover.gif
new file mode 100644
index 0000000000..a829351b64
--- /dev/null
+++ b/editor/composer/res/table-add-row-after-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-after.gif b/editor/composer/res/table-add-row-after.gif
new file mode 100644
index 0000000000..3f1a39d981
--- /dev/null
+++ b/editor/composer/res/table-add-row-after.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-before-active.gif b/editor/composer/res/table-add-row-before-active.gif
new file mode 100644
index 0000000000..34f1e0adec
--- /dev/null
+++ b/editor/composer/res/table-add-row-before-active.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-before-hover.gif b/editor/composer/res/table-add-row-before-hover.gif
new file mode 100644
index 0000000000..e8f1d10b0c
--- /dev/null
+++ b/editor/composer/res/table-add-row-before-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-add-row-before.gif b/editor/composer/res/table-add-row-before.gif
new file mode 100644
index 0000000000..1682170cb3
--- /dev/null
+++ b/editor/composer/res/table-add-row-before.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-column-active.gif b/editor/composer/res/table-remove-column-active.gif
new file mode 100644
index 0000000000..4dfbde4ce2
--- /dev/null
+++ b/editor/composer/res/table-remove-column-active.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-column-hover.gif b/editor/composer/res/table-remove-column-hover.gif
new file mode 100644
index 0000000000..fd11bb52c3
--- /dev/null
+++ b/editor/composer/res/table-remove-column-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-column.gif b/editor/composer/res/table-remove-column.gif
new file mode 100644
index 0000000000..d8071da0a9
--- /dev/null
+++ b/editor/composer/res/table-remove-column.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-row-active.gif b/editor/composer/res/table-remove-row-active.gif
new file mode 100644
index 0000000000..4dfbde4ce2
--- /dev/null
+++ b/editor/composer/res/table-remove-row-active.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-row-hover.gif b/editor/composer/res/table-remove-row-hover.gif
new file mode 100644
index 0000000000..fd11bb52c3
--- /dev/null
+++ b/editor/composer/res/table-remove-row-hover.gif
Binary files differ
diff --git a/editor/composer/res/table-remove-row.gif b/editor/composer/res/table-remove-row.gif
new file mode 100644
index 0000000000..d8071da0a9
--- /dev/null
+++ b/editor/composer/res/table-remove-row.gif
Binary files differ
diff --git a/editor/composer/test/chrome.ini b/editor/composer/test/chrome.ini
new file mode 100644
index 0000000000..8b4d9c3333
--- /dev/null
+++ b/editor/composer/test/chrome.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+skip-if = os == 'android'
+
+[test_bug1266815.html]
+[test_bug434998.xhtml]
diff --git a/editor/composer/test/file_bug1453190.html b/editor/composer/test/file_bug1453190.html
new file mode 100644
index 0000000000..74b13bb0ec
--- /dev/null
+++ b/editor/composer/test/file_bug1453190.html
@@ -0,0 +1,12 @@
+<script>
+window.onload = function () {
+ document.createElement("frameset").setAttribute("onunload", "go()")
+}
+function go() {
+ let a = document.getElementById("a");
+ let b = document.getElementById("b");
+ a.appendChild(b);
+}
+</script>
+<div id="a">
+<li id="b" contenteditable="true">
diff --git a/editor/composer/test/mochitest.ini b/editor/composer/test/mochitest.ini
new file mode 100644
index 0000000000..eb364a4a50
--- /dev/null
+++ b/editor/composer/test/mochitest.ini
@@ -0,0 +1,9 @@
+[test_bug1453190.html]
+skip-if = os == "android"
+support-files =
+ file_bug1453190.html
+[test_bug348497.html]
+[test_bug384147.html]
+[test_bug389350.html]
+[test_bug519928.html]
+[test_bug738440.html]
diff --git a/editor/composer/test/test_bug1266815.html b/editor/composer/test/test_bug1266815.html
new file mode 100644
index 0000000000..1c565fb2c1
--- /dev/null
+++ b/editor/composer/test/test_bug1266815.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<script type="text/javascript">
+// XXX(nika): Why are we using SpecialPowers here? If we're a chrome mochitest
+// can't we avoid using the SpecialPowers wrappers?
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+const Cu = SpecialPowers.Cu;
+
+const { ComponentUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ComponentUtils.sys.mjs"
+);
+
+const HELPERAPP_DIALOG_CID =
+ SpecialPowers.wrap(SpecialPowers.Components)
+ .ID(Cc["@mozilla.org/helperapplauncherdialog;1"].number);
+const HELPERAPP_DIALOG_CONTRACT_ID = "@mozilla.org/helperapplauncherdialog;1";
+const MOCK_HELPERAPP_DIALOG_CID =
+ SpecialPowers.wrap(SpecialPowers.Components)
+ .ID("{391832c8-5232-4676-b838-cc8ad373f3d8}");
+
+var registrar = SpecialPowers.wrap(Components).manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+
+const HandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+].getService(Ci.nsIHandlerService);
+
+const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+var helperAppDlgPromise = new Promise(function(resolve) {
+ var mockHelperAppService;
+
+ function HelperAppLauncherDialog() {
+ }
+
+ HelperAppLauncherDialog.prototype = {
+ show(aLauncher, aWindowContext, aReason) {
+ ok(true, "Whether showing Dialog");
+ resolve();
+ registrar.unregisterFactory(MOCK_HELPERAPP_DIALOG_CID,
+ mockHelperAppService);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]),
+ };
+
+ mockHelperAppService = ComponentUtils.generateSingletonFactory(HelperAppLauncherDialog);
+ registrar.registerFactory(MOCK_HELPERAPP_DIALOG_CID, "",
+ HELPERAPP_DIALOG_CONTRACT_ID,
+ mockHelperAppService);
+});
+
+add_task(async function() {
+ // ensure the download triggers the external app dialog
+ const mimeInfo = MIMEService.getFromTypeAndExtension("application/octet-stream", "");
+ mimeInfo.alwaysAskBeforeHandling = true;
+ HandlerService.store(mimeInfo);
+
+ SimpleTest.registerCleanupFunction(() => {
+ HandlerService.remove(mimeInfo);
+ });
+
+ let promise = new Promise(function(resolve) {
+ let iframe = document.createElement("iframe");
+ iframe.onload = function() {
+ is(iframe.contentDocument.getElementById("edit").innerText, "abc",
+ "load iframe source");
+ resolve();
+ };
+ iframe.id = "testframe";
+ iframe.src = "data:text/html,<div id=edit contenteditable=true>abc</div>";
+ document.body.appendChild(iframe);
+ });
+
+ await promise;
+
+ let iframe = document.getElementById("testframe");
+ let docShell = SpecialPowers.wrap(iframe.contentWindow).docShell;
+
+ ok(docShell.hasEditingSession, "Should have editing session");
+
+ document.getElementById("testframe").src =
+ "data:application/octet-stream,TESTCONTENT";
+
+ await helperAppDlgPromise;
+
+ ok(docShell.hasEditingSession, "Should have editing session");
+});
+</script>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug1453190.html b/editor/composer/test/test_bug1453190.html
new file mode 100644
index 0000000000..cf6b0d888a
--- /dev/null
+++ b/editor/composer/test/test_bug1453190.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1453190
+-->
+<head>
+ <title>Test for Bug 1453190</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=1453190">Mozilla Bug 1453190</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+let testWindow = window.open("file_bug1453190.html");
+testWindow.addEventListener("load", () => {
+ SimpleTest.executeSoon(() => {
+ runTest(testWindow);
+ });
+}, {once: true});
+
+function runTest(win) {
+ ok(!win.closed, "test window is opened");
+ win.close();
+ ok(win.closed, "test window is closed");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug348497.html b/editor/composer/test/test_bug348497.html
new file mode 100644
index 0000000000..898fb49ae1
--- /dev/null
+++ b/editor/composer/test/test_bug348497.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=348497
+-->
+<head>
+ <title>Test for Bug 348497</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=348497">Mozilla Bug 348497</a>
+<p id="display"></p>
+<div id="content">
+ This page should not crash Mozilla<br>
+ <iframe id="testIframe"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 348497 **/
+function doe() {
+ document.getElementById("testIframe").style.display = "block";
+ document.getElementById("testIframe").contentDocument.designMode = "on";
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(doe);
+addLoadEvent(function() { ok(true, "enabling designmode on an iframe onload does not crash Mozilla"); });
+addLoadEvent(SimpleTest.finish);
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/editor/composer/test/test_bug384147.html b/editor/composer/test/test_bug384147.html
new file mode 100644
index 0000000000..6feb8b4ad4
--- /dev/null
+++ b/editor/composer/test/test_bug384147.html
@@ -0,0 +1,203 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=384147
+-->
+<head>
+ <title>Test for Bug 384147</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=384147">Mozilla Bug 384147</a>
+<p id="display"></p>
+<div id="content" style="display: block">
+<div contentEditable id="editor"></div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 384147 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var editor = document.getElementById("editor");
+
+editor.innerHTML = "<ol><li>Item 1</li><li>Item 2</li><ol><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>";
+editor.focus();
+
+// If executed directly, a race condition exists that will cause execCommand
+// to fail occasionally (but often). Defer test execution to page load.
+addLoadEvent(function() {
+ var sel = window.getSelection();
+
+ // Test the effect that the tab key has on list items. Each test is
+ // documented with the initial state of the list on the left, and the
+ // expected state of the list on the right. {\t} indicates the list item
+ // that will be indented. {\st} indicates that a shift-tab will be simulated
+ // on that list item, outdenting it.
+ //
+ // Note: any test failing will likely result in all following tests failing
+ // as well, since each test depends on the document being in a given state.
+ // Unfortunately, due to the problems getting document focus and key events
+ // to fire consistently, it's difficult to reset state between tests.
+ // If there are test failures here, only debug the first test failure.
+
+ // *** test 1 ***
+ // 1. Item 1 1. Item 1
+ // 2. {\t}Item 2 1. Item 2
+ // 1. Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[1]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 1");
+
+ // *** test 2 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. {\t}Item 3 1. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[2]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><ol><li>Item 3</li></ol></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 2");
+
+ // *** test 3 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 1. {\st}Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 3");
+
+ // *** test 4 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. {\st}Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 4");
+
+ // *** test 5 ***
+ // 1. Item 1 1. Item 1
+ // 1. {\st}Item 2 2. Item 2
+ // 2. Item 3 3. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[1]);
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><li>Item 2</li><li>Item 3</li></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 5");
+
+ // *** test 6 ***
+ // 1. Item 1 1. Item 1
+ // 2. {\t}Item 2 1. Item 2
+ // 3. Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 6");
+
+ // *** test 7 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. {\t}Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[2]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 7");
+
+ // That covers the basics of merging lists on indent and outdent.
+ // We also want to check that ul / ol lists won't be merged together,
+ // since they're different types of lists.
+ // *** test 8 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. Item 3 2. Item 3
+ // * {\t}Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[3]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><ul><li>Item 4</li></ul><li>Item 5</li></ul>",
+ "html output doesn't match expected value in test 8");
+
+ // Better test merging with <ul> rather than <ol> too.
+ // *** test 9 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. Item 3 2. Item 3
+ // * Item 4 * Item 4
+ // * {\t}Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[4]);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><ul><li>Item 4</li><li>Item 5</li></ul></ul>",
+ "html output doesn't match expected value in test 9");
+
+ // Same test as test 8, but with outdent rather than indent.
+ // *** test 10 ***
+ // 1. Item 1 1. Item 1
+ // 1. Item 2 1. Item 2
+ // 2. Item 3 2. Item 3
+ // * {\st}Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ sel.selectAllChildren(editor.getElementsByTagName("li")[3]);
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><ul><li>Item 5</li></ul></ul>",
+ "html output doesn't match expected value in test 10");
+
+ // Test indenting multiple items at once. Hold down "shift" and select
+ // upwards to get all the <ol> items and the first <ul> item.
+ // *** test 11 ***
+ // 1. Item 1 1. Item 1
+ // 1. {\t}Item 2 1. Item 2
+ // 2. {\t}Item 3 2. Item 3
+ // * {\t}Item 4 * Item 4
+ // * Item 5 * Item 5
+ sel.removeAllRanges();
+ var range = document.createRange();
+ range.setStart(editor.getElementsByTagName("li")[1], 0);
+ range.setEnd(editor.getElementsByTagName("li")[3], editor.getElementsByTagName("li")[3].childNodes.length);
+ sel.addRange(range);
+ document.execCommand("indent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><ol><li>Item 2</li><li>Item 3</li></ol></ol></ol><ul><ul><li>Item 4</li><li>Item 5</li></ul></ul>",
+ "html output doesn't match expected value in test 11");
+
+ // Test outdenting multiple items at once. Selection is already ready...
+ // *** test 12 ***
+ // 1. Item 1 1. Item 1
+ // 1. {\st}Item 2 1. Item 2
+ // 2. {\st}Item 3 2. Item 3
+ // * {\st}Item 4 * Item 4
+ // * Item 5 * Item 5
+ document.execCommand("outdent", false, null);
+ ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><ul><li>Item 5</li></ul></ul>",
+ "html output doesn't match expected value in test 12");
+
+ SimpleTest.finish();
+});
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/editor/composer/test/test_bug389350.html b/editor/composer/test/test_bug389350.html
new file mode 100644
index 0000000000..9b9aecd1e7
--- /dev/null
+++ b/editor/composer/test/test_bug389350.html
@@ -0,0 +1,33 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=389350
+-->
+<head>
+<title>Test for Bug 389350</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 type="text/javascript">
+
+function runTest() {
+ var e = document.getElementById("edit");
+ e.contentDocument.designMode = "on";
+ e.style.display = "block";
+ e.focus();
+ sendString("abc");
+ var expected = "<head></head><body>abc</body>";
+ var result = e.contentDocument.documentElement.innerHTML;
+ is(result, expected, "iframe with designmode on had incorrect content");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+</script>
+
+</head>
+<body id="body">
+<iframe id="edit" width="200" height="100" style="display: none;" src="">
+</body>
+</html>
diff --git a/editor/composer/test/test_bug434998.xhtml b/editor/composer/test/test_bug434998.xhtml
new file mode 100644
index 0000000000..db2261e3a5
--- /dev/null
+++ b/editor/composer/test/test_bug434998.xhtml
@@ -0,0 +1,106 @@
+<?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=434998
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 434998" 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=434998"
+ target="_blank">Mozilla Bug 434998</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) {
+ // Should not throw
+ var threw = false;
+ try {
+ this.mEditor.contentDocument.execCommand("bold", false, null);
+ } catch (e) {
+ threw = true;
+ }
+ ok(!threw, "The execCommand API should work on <xul:editor>");
+ progress.removeProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ 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/composer/test/test_bug519928.html b/editor/composer/test/test_bug519928.html
new file mode 100644
index 0000000000..4ccb1aaba5
--- /dev/null
+++ b/editor/composer/test/test_bug519928.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=519928
+-->
+<head>
+ <title>Test for Bug 519928</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=519928">Mozilla Bug 519928</a>
+<p id="display"></p>
+<div id="content">
+<iframe id="load-frame"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var iframe = document.getElementById("load-frame");
+
+function enableJS() { allowJS(true, iframe); }
+function disableJS() { allowJS(false, iframe); }
+function allowJS(allow, frame) {
+ SpecialPowers.wrap(frame.contentWindow).windowGlobalChild.windowContext.allowJavascript = allow;
+}
+
+function expectJSAllowed(allowed, testCondition, callback) {
+ window.ICanRunMyJS = false;
+ var self_ = window;
+ testCondition();
+
+ var doc = iframe.contentDocument;
+ doc.body.innerHTML = "<iframe></iframe>";
+ var innerFrame = doc.querySelector("iframe");
+ innerFrame.addEventListener("load", function() {
+ var msg = "The inner iframe should" + (allowed ? "" : " not") + " be able to run Javascript";
+ is(self_.ICanRunMyJS, allowed, msg);
+ callback();
+ }, {once: true});
+ // eslint-disable-next-line no-useless-concat
+ var iframeSrc = "<script>parent.parent.ICanRunMyJS = true;</scr" + "ipt>";
+ innerFrame.srcdoc = iframeSrc;
+}
+
+SimpleTest.waitForExplicitFinish();
+/* eslint-disable max-nested-callbacks */
+addLoadEvent(function() {
+ var enterDesignMode = function() { document.designMode = "on"; };
+ var leaveDesignMode = function() { document.designMode = "off"; };
+ expectJSAllowed(false, disableJS, function() {
+ expectJSAllowed(true, enableJS, function() {
+ expectJSAllowed(true, enterDesignMode, function() {
+ expectJSAllowed(true, leaveDesignMode, function() {
+ expectJSAllowed(false, disableJS, function() {
+ expectJSAllowed(false, enterDesignMode, function() {
+ expectJSAllowed(false, leaveDesignMode, function() {
+ expectJSAllowed(true, enableJS, function() {
+ enterDesignMode = function() { iframe.contentDocument.designMode = "on"; };
+ leaveDesignMode = function() { iframe.contentDocument.designMode = "off"; };
+ expectJSAllowed(false, disableJS, function() {
+ expectJSAllowed(true, enableJS, function() {
+ expectJSAllowed(true, enterDesignMode, function() {
+ expectJSAllowed(true, leaveDesignMode, function() {
+ expectJSAllowed(false, disableJS, function() {
+ expectJSAllowed(false, enterDesignMode, function() {
+ expectJSAllowed(false, leaveDesignMode, function() {
+ expectJSAllowed(true, enableJS, function() {
+ testDocumentDisabledJS();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
+/* eslint-enable max-nested-callbacks */
+
+function testDocumentDisabledJS() {
+ window.ICanRunMyJS = false;
+ var self_ = window;
+ // Ensure design modes are disabled
+ document.designMode = "off";
+ iframe.contentDocument.designMode = "off";
+
+ // Javascript enabled on the main iframe
+ enableJS();
+
+ var doc = iframe.contentDocument;
+ doc.body.innerHTML = "<iframe></iframe>";
+ var innerFrame = doc.querySelector("iframe");
+
+ // Javascript disabled on the innerFrame.
+ allowJS(false, innerFrame);
+
+ innerFrame.addEventListener("load", function() {
+ var msg = "The inner iframe should not be able to run Javascript";
+ is(self_.ICanRunMyJS, false, msg);
+ SimpleTest.finish();
+ }, {once: true});
+ // eslint-disable-next-line no-useless-concat
+ var iframeSrc = "<script>parent.parent.ICanRunMyJS = true;</scr" + "ipt>";
+ innerFrame.srcdoc = iframeSrc;
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/composer/test/test_bug738440.html b/editor/composer/test/test_bug738440.html
new file mode 100644
index 0000000000..a021906cfc
--- /dev/null
+++ b/editor/composer/test/test_bug738440.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=738440
+-->
+<title>Test for Bug 738440</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=738440">Mozilla Bug 738440</a>
+<div contenteditable></div>
+<script>
+
+/** Test for Bug 738440 **/
+document.execCommand("stylewithcss", false, "true");
+is(document.queryCommandState("stylewithcss"), true,
+ "setting stylewithcss to true should cause its state to be true");
+is(document.queryCommandState("usecss"), false,
+ "usecss state should always be false");
+
+document.execCommand("stylewithcss", false, "false");
+is(document.queryCommandState("stylewithcss"), false,
+ "setting stylewithcss to false should cause its state to be false");
+is(document.queryCommandState("usecss"), false,
+ "usecss state should always be false");
+
+document.execCommand("usecss", false, "true");
+is(document.queryCommandState("stylewithcss"), false,
+ "setting usecss to true should cause stylewithcss state to be false");
+is(document.queryCommandState("usecss"), false,
+ "usecss state should always be false");
+
+document.execCommand("usecss", false, "false");
+is(document.queryCommandState("stylewithcss"), true,
+ "setting usecss to false should cause stylewithcss state to be true");
+is(document.queryCommandState("usecss"), false,
+ "usecss state should always be false");
+
+</script>
diff --git a/editor/docs/ChangJie.png b/editor/docs/ChangJie.png
new file mode 100644
index 0000000000..04d872e258
--- /dev/null
+++ b/editor/docs/ChangJie.png
Binary files differ
diff --git a/editor/docs/EditorModuleSpecificRules.rst b/editor/docs/EditorModuleSpecificRules.rst
new file mode 100644
index 0000000000..dbe4a2b12f
--- /dev/null
+++ b/editor/docs/EditorModuleSpecificRules.rst
@@ -0,0 +1,215 @@
+############################
+Editor module specific rules
+############################
+
+The editor module has not been maintained aggressively about a decade. Therefore, this module needs
+to be treated as a young module or in a transition period to align the behavior to the other
+browsers and take modern C++ style.
+
+Undoubtedly, this editor module is under rewritten for modern and optimized for current specs.
+Additionally, this module does really complicated things which may cause security issues.
+Therefore, there are specific rules:
+
+Treat other browsers behavior as standard if the behavior is reasonable
+=======================================================================
+
+The editing behavior is not standardized since as you see too many lines in the editor classes, the
+number of cases which need to handle edge cases is crazy and that makes it impossible to standardize.
+Additionally, our editor behavior is not so stable. Some behaviors were aligned to Internet Explorer,
+some other behaviors were not for making "better" UX for users of email composer and HTML composer
+which were in SeaMonkey, and the other browser engines (Blink and WebKit) have same roots but the
+behavior is different from both IE and Gecko.
+
+Therefore, there were no reference behavior.
+
+In these days, compatibility between browsers becomes more important, and fortunately, the behavior
+of Blink (Chrome/Chromium) which has the biggest market share is more reasonable than ours in a lot
+of cases. Therefore, if we get web-compat issue reports, we should align the behavior to Blink in
+theory.
+
+However, if Blink's behavior is also odd, this is the worst case. In this case, we should try to
+align the behavior to WebKit if and only if WebKit's behavior is different from Blink and
+reasonable, or doing something "better" for hiding the issue from web-apps and file an issue to the
+Editing Working Group with creating a "tentative" web-platform test.
+
+Don't make methods of editor classes public if they are used only by helper classes
+===================================================================================
+
+Although this is a silly rule. Of course, APIs of editor classes need to be public for the other
+modules. However, the other methods which are used only by helper classes in the editor module --the
+methods may be crashed if called by the other modules because editor classes store and guarantee the
+colleagues (e.g., ``Selection``) when it starts to handle an edit action (edit command or
+operation)-- does not want to do it for the performance reason. Therefore, such methods are now
+declared as protected methods and the caller classes are registered as friends.
+
+For solving this issue, we could split the editor classes one is exported and the other is not
+exposed, and make the former to proxies and own the latter. However, this approach might cause
+performance regressions and requires a lot of line changes (at least each method definition and
+warning messages at the caller sides). Tracked in
+`bug 1555916 <https://bugzilla.mozilla.org/show_bug.cgi?id=1555916>`__.
+
+Steps to handle one editor command or operation
+===============================================
+
+One edit command or operation is called "edit action" in the editor module. Handling it starts
+when an XPCOM method or a public method which is named as ``*AsAction``. Those methods create
+``AutoEditActionDataSetter`` in the stack first, then, call one of ``CanHandle()``,
+``CanHandleAndMaybeDispatchBeforeInputEvent()`` or ``CanHandleAndFlushPendingNotifications()``.
+If ``CanHandleAndMaybeDispatchBeforeInputEvent()`` causes dispatching ``beforeinput`` event and if
+the event is consumed by the web app, it returns ``NS_ERROR_EDITOR_ACTION_CANCELED``. In this case,
+the method can do anything due to the ``beforeinput`` event definition.
+
+At this time, ``AutoEditActionDataSetter`` stores ``Selection`` etc which are required for handling
+the edit action in it and set ``EditorBase::mEditActionData`` to its address. Then all methods of
+editor can access the objects via the pointer (typically wrapped in inline methods) and the lifetime
+of the objects are guaranteed.
+
+Then, the methods call one or more edit-sub action handlers. E.g., when user types a character
+with a non-collapsed selection range, editor needs to delete the selected content first and insert
+the character there. For implementing this behavior, "insert text" edit action handler needs to call
+"delete selection" sub-action handler and "insert text" sub-action handler. The sub-edit action
+handlers are named as ``*AsSubAction``.
+
+The callers of edit sub-action handlers or the handlers themselves create ``AutoPlaceholderBatch``
+in the stack. This creates a placeholder transaction to make all transactions undoable with one
+"undo" command.
+
+Then, each edit sub-action handler creates ``AutoEditSubActionNotifier`` in the stack and if it's
+the topmost edit sub-action handling, ``OnStartToHandleTopLevelEditSubAction()`` is called at the
+creation and ``OnEndHandlingTopLevelEditSubAction()`` is called at the destruction. The latter will
+clean up the modified range, e.g., remove unnecessary empty nodes.
+
+Finally, the edit sub-actions does something while ``AutoEditSubActionNotifier`` is alive. Helper
+methods of edit sub-action handlers are typically named as ``*WithTransaction`` because they are
+done with transaction classes for making everything undoable.
+
+Don't update Selection immediately
+==================================
+
+Changing the ranges of ``Selection`` is expensive (due ot validating new range, notifying new
+selected or unselected frames, notifying selection listeners, etc), and retrieving current
+``Selection`` ranges at staring to handle something makes the code statefull which is harder to
+debug when you investigate a bug. Therefore, each method should return new caret position or
+update ranges given as in/out parameter of ``AutoRangeArray``. ``Result<CaretPoint, nsresult>``
+is a good result type for the former, and the latter is useful style if the method needs to keep
+``Selection`` similar to given ranges, e.g., when paragraphs around selection are changed to
+different type of blocks. Finally, edit sub-action handler methods should update ``Selection``
+before destroying ``AutoEditSubActionNotifier`` whose post-processing requires ``Selection``.
+
+Don't add new things into OnEndHandlingTopLevelEditSubAction()
+==============================================================
+
+When the topmost edit sub-action is handled, ``OnEndHandlingTopLevelEditSubAction`` is called and
+it cleans up something in (or around) the modified range. However, this "post-processing" approach
+makes it harder to change the behavior for fixing web-compat issues. For example, it deletes empty
+nodes in the range, but if only some empty nodes are inserted intentionally, it doesn't have the
+details and may unexpectedly delete necessary empty nodes.
+
+Instead, new things should be done immediately at or after modifying the DOM tree, and if it
+requires to disable the post-processing, add new ``bool`` flag to
+``EditorBase::TopLevelEditSubActionData`` and when it's set, make
+``OnEndHandlingTopLevelEditSubAction`` stop doing something.
+
+Don't use NS_WARN_IF for checking NS_FAILED, isErr() and Failed()
+=================================================================
+
+The warning messages like ``NS_FAILED(rv)`` does not help except the line number, and in the cases
+of that we get web-compat reports, somewhere in the editor modules may get unexpected result. For
+saving the investigation time of web-compat issues, each failure should warn which method call
+failed, for example:
+
+.. code:: cpp
+
+ nsresult rv = DoSomething();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DoSomething() failed");
+ return rv;
+ }
+
+These warnings will let you know the stack of failure in debug build. In other words, when you
+investigate a web-compat issue in editor, you should do the steps to reproduce in debug build first.
+Then, you'd see failure point stack in the terminal.
+
+Return NS_ERROR_EDITOR_DESTROYED when editor gets destroyed
+===========================================================
+
+The most critical error while an editor class method is running is what the editor instance is
+destroyed by the web app. This can be checked with a call of ``EditorBase::Destroyed()`` and
+if it returns ``true``, methods should return ``NS_ERROR_EDITOR_DESTROYED`` with stopping handling
+anything. Then, all callers which handle the error result properly will stop handling too.
+Finally, public methods should return ``EditorBase::ToGenericNSResult(rv)`` instead of exposing
+an internal error of the editor module.
+
+Note that destroying the editor is intentional thing for the web app. Thus we should not throw
+exception for this failure reason. Therefore, the public methods shouldn't return error.
+
+When you make a method return ``NS_ERROR_EDITOR_DESTROYED`` properly, you should mark the method
+as ``[[nodiscard]]``. In other words, if you see ``[[nodiscard]]`` in method definition and it
+returns ``nsresult`` or ``Result<*, nsresult>``, the method callers do not need to check
+``Destroyed()`` by themselves.
+
+Use reference instead of pointer as far as possible
+===================================================
+
+When you create or redesign a method, it should take references instead of pointers if they take.
+This rule forces that the caller to do null-check and this avoids a maybe unexpected case like:
+
+.. code:: cpp
+
+ inline bool IsBRElement(const nsINode* aNode) {
+ return aNode && aNode->IsHTMLElement(nsGkAtoms::br);
+ }
+
+ void DoSomethingExceptIfBRElement(const nsINode* aNode) {
+ if (IsBRElement(aNode)) {
+ return;
+ }
+ // Do something for non-BR element node.
+ }
+
+In this case, ``DoSomethingExceptIfBRElement`` expects that ``aNode`` is never ``nullptr`` but it
+could be at least in build time. Using reference fixes this mistake at build time.
+
+Use ``EditorUtils`` or ``HTMLEditUtils`` for stateless methods
+==============================================================
+
+When you create a new static method to the editor classes or a new inline method in cpp file which
+defines the editor classes, please check if it's a common method which may be used from other
+places in the editor module. If it's possible to be used only in ``HTMLEditor`` or its helper
+classes, the method should be in ``HTMLEditUtils``. If it's possible be used in ``EditorBase`` or
+``TextEditor`` or their helper classes, it should be in ``EditorUtils``.
+
+Don't use bool argument
+=======================
+
+If you create a new method which take one or more ``bool`` arguments, use ``enum class`` instead
+since ``true`` or ``false`` in the caller side is not easy to read. For example, you must not
+be able to understand what this example mean:
+
+.. code:: cpp
+
+ if (IsEmpty(aNode, true)) {
+
+For avoiding this issue, you should create new ``enum class`` for each. E.g.,
+
+.. code:: cpp
+
+ if (IsEmpty(aNode, TreatSingleBR::AsVisible)) {
+
+Basically, both ``enum class`` name and its value names explains what it means fluently. However, if
+it's impossible, use ``No`` and ``Yes`` for the value like:
+
+.. code:: cpp
+
+ if (DoSomething(aNode, OnlyIfEmpty::Yes)) {
+
+Don't use out parameters
+========================
+
+In most cases, editor methods meet error of low level APIs, thus editor methods usually return error
+code. On the other hand, a lot of code need to return computed things, e.g., new caret position,
+whether it's handled, ignored or canceled, a target node looked for, etc. We used ``nsresult`` for
+the return value type and out parameters for the other results, but it makes callers scattering a
+lot of auto variables and reusing them makes the code harder to understand.
+
+Now we can use ``mozilla::Result<Foo, nsresult>`` instead.
diff --git a/editor/docs/EditorModuleStructure.rst b/editor/docs/EditorModuleStructure.rst
new file mode 100644
index 0000000000..e179145a0d
--- /dev/null
+++ b/editor/docs/EditorModuleStructure.rst
@@ -0,0 +1,219 @@
+#######################
+Editor module structure
+#######################
+
+This document explains the structure of the editor module and overview of classes.
+
+Introduction
+============
+
+This module implements the builtin editors of editable elements or documents, and this does **not**
+implement the interface with DOM API and visual feedback of the editing UI. In other words, this
+module implements DOM tree editors.
+
+Directories
+===========
+
+composer
+--------
+
+Previously, this directory contained "Composer" UI related code. However, currently, this
+directory contains ``nsEditingSession`` and ``ComposerCommandsUpdater``.
+
+libeditor
+---------
+
+This is the main directory which contains "core" implementation of editors.
+
+spellchecker
+------------
+
+Despite of the directory name, implementation of the spellchecker is **not** here. This directory
+contains only a bridge between editor classes and the spellchecker and serialized text of editable
+content for spellchecking.
+
+txmgr
+-----
+
+This directory contains transaction items and transaction classes. They were designed for generic
+use cases, e.g., managing undo/redo of bookmarks/history of browser, etc, but they are used only by
+the editor.
+
+Main classes
+============
+
+EditorBase
+----------
+
+``EditorBase`` class is an abstract class of editors. This inherits ``nsIEditor`` XPCOM interface,
+implement common features which work with instance of classes, and exposed by
+``mozilla/EditorBase.h``.
+
+TextEditor
+----------
+
+``TextEditor`` class is the implementation of plaintext editor which works with ``<input>`` and
+``<textarea>``. Its exposed root is the host HTML elements, however, the editable root is an
+anonymous ``<div>`` created in a native anonymous subtree under the exposed root elements. This
+creates a ``Text`` node as the first child of the anonymous ``<div>`` and modify its data. If the text
+data ends with a line-break, i.e., the last line is empty, append a ``<br>`` element for making the
+empty last line visible.
+
+This also implements password editor. It works almost same as normal text editor, but each character
+may be masked by masked character such as "â—" or "*" by the layout module for the privacy.
+Therefore, this manages masked/unmasked range of password and maybe making typed character
+automatically after a while for mobile devices.
+
+This is exposed with ``mozilla/TextEditor.h``.
+
+Selection in TextEditor
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Independent ``Selection`` and ``nsFrameSelection`` per ``<input>`` or ``<textarea>``.
+
+Lifetime of TextEditor
+^^^^^^^^^^^^^^^^^^^^^^
+
+Created when an editable ``<textarea>`` is created or a text-editable ``<input>`` element gets focus.
+Note that the initialization may run asynchronously if it's requested when it's not safe to run
+script. Destroyed when the element becomes invisible. Note that ``TextEditor`` is recreated when
+every reframe of the host element. This means that when the size of ``<input>`` or ``<textarea>``
+is changed for example, ``TextEditor`` is recreated and forget undo/redo transactions, but takes
+over the value, selection ranges and composition of IME from the previous instance.
+
+HTMLEditor
+----------
+
+``HTMLEditor`` class is the implementation of rich text editor which works with ``contenteditable``,
+``Document.designMode`` and XUL ``<editor>``. Its instance is created per document even if the
+document has multiple elements having ``contenteditable`` attribute. Therefore, undo/redo
+transactions are shared in all editable regions.
+
+This is exposed with ``mozilla/HTMLEditor.h``.
+
+Selection in HTMLEditor
+^^^^^^^^^^^^^^^^^^^^^^^
+
+The instance for the ``Document`` and ``Window``. When an editable element gets focus, ``HTMLEditor``
+sets the ancestor limit of ``Selection`` to the focused element or the ``<body>`` of the ``Document``.
+Then, ``Selection`` cannot cross boundary of the limiter element.
+
+Lifetime of HTMLEditor
+^^^^^^^^^^^^^^^^^^^^^^
+
+Created when first editable region is created in the ``Document``. Destroyed when last editable
+region becomes non-editable.
+
+Currently, even while ``HTMLEditor`` is handling an edit command/operation (called edit action in
+editor classes), each DOM mutation can be tracked with legacy DOM mutation events synchronously.
+Thus, after changing the DOM tree from ``HTMLEditor``, any state could occur, e.g., the editor
+itself may have been destroyed, the DOM tree have been modified, the ``Selection`` have been
+modified, etc. This issue is tracked in
+`bug 1710784 <https://bugzilla.mozilla.org/show_bug.cgi?id=1710784>`__.
+
+
+EditorUtils
+-----------
+
+This class has only static utility methods which are used by ``EditorBase`` or ``TextEditor`` and
+may be used by ``HTMLEditor`` too. I.e., the utility methods which are used **not** only by
+``HTMLEditor`` should be implemented in this class.
+
+Typically, sateless methods should be implemented as ``static`` methods of utility classes because
+editor classes have too many methods and fields.
+
+This class is not exposed.
+
+HTMLEditUtils
+-------------
+
+This class has only static utility methods which are used only by ``HTMLEditor``.
+
+This class is not exposed.
+
+AutoRangeArray
+--------------
+
+This class is a stack only class and intended to copy of normal selection ranges. In the new code,
+`Selection` shouldn't be referred directly, instead, methods should take reference to this instance
+and modify it. Finally, root caller should apply the ranges to `Selection`. Then, `HTMLEditor`
+does not need to take care of unexpected `Selection` updates by legacy DOM mutation event listeners.
+
+This class is not exposed.
+
+EditorDOMPoint, EditorRawDOMPoint, EditorDOMPointInText, EditorRawDOMPointInText
+--------------------------------------------------------------------------------
+
+It represents a point in a DOM tree with one of the following:
+
+* Container node and offset in it
+* Container node and child node in it
+* Container node and both offset and child node in it
+
+In most cases, instances are initialized with a container and only offset or child node. Then,
+when ``Offset()`` or ``GetChild()`` is called, the last one is "fixed". After inserting new child
+node before the offset and/or the child node, ``IsSetAndValid()`` will return ``false`` since the
+child node is not the child at the offset.
+
+If you want to keep using after modifying the DOM tree, you can make the instance forget offset or
+child node with ``AutoEditorDOMPointChildInvalidator`` and ``AutoEditorDOMRangeChildrenInvalidator``.
+The reason why the forgetting methods are not simply exposed is, ``Offset()`` and ``GetChild()``
+are available even after the DOM tree is modified to get the cached offset and child node,
+additionally, which method may modify the DOM tree may be not clear for developers. Therefore,
+creating a block only for these helper classes makes the updating point clearer.
+
+These classes are exposed with ``mozilla/EditorDOMPoint.h``.
+
+EditorDOMRange, EditorRawDOMRange, EditorDOMRangeInTexts, EditorRawDOMRangeInTexts
+----------------------------------------------------------------------------------
+
+It represents 2 points in a DOM tree with 2 ``Editor*DOMPoint(InText)``. Different from ``nsRange``,
+the instances do not track the DOM tree changes. Therefore, the initialization is much faster than
+``nsRange`` and can be in the stack.
+
+These classes are exposed with ``mozilla/EditorDOMPoint.h``.
+
+AutoTrackDOMPoint, AutoTrackDOMRange
+------------------------------------
+
+These methods updates ``Editor*DOMPoint(InText)`` or ``Editor*DOMRange(InTexts)`` at destruction
+with applying the changes caused by the editor instance. In other words, they don't track the DOM
+tree changes by the web apps like changes from legacy DOM mutation event listeners.
+
+These classes are currently exposed with ``mozilla/SelectionState.h``, but we should stop exposing
+them.
+
+WSRunScanner
+------------
+
+A helper class of ``HTMLEditor``. This class scans previous or (inclusive) next visible thing from
+a DOM point or a DOM node. This is typically useful for considering whether a `<br>` is visible or
+invisible due to near a block element boundary, finding nearest editable character from caret
+position, etc. However, the running cost is **not** cheap, thus if you find another way to consider
+it simpler, use it instead, and also this does not check the actual style of the nodes (visible vs.
+invisible, block vs. inline), thus you'd get unexpected result in tricky cases.
+
+This class is not exposed.
+
+WhiteSpaceVisibilityKeeper
+--------------------------
+
+A helper class of ``HTMLEditor`` to handle collapsible white-spaces as what user expected. This
+class currently handles white-space normalization (e.g., when user inputs multiple collapsible
+white-spaces, this replaces some of them to NBSPs), but the behavior is different from the other
+browsers. We should re-implement this with emulating the other browsers' behavior as far as possible,
+but currently it's put off due to not affecting UX (tracked in
+`bug 1658699 <https://bugzilla.mozilla.org/show_bug.cgi?id=1658699>`__.
+
+This class is not exposed.
+
+\*Transaction
+-------------
+
+``*Transaction`` classes represents a small transaction of updating the DOM tree and implements
+"do", "undo" and "redo" of the update.
+
+Note that each class instance is created too many (one edit action may cause multiple transactions).
+Therefore, each instance must be smaller as far as possible, and if you have an idea to collapse
+multiple instances to one instance, you should fix it. Then, users can run Firefox with smaller
+memory devices especially if the transaction is used in ``TextEditor``.
diff --git a/editor/docs/IMEHandlingGuide.rst b/editor/docs/IMEHandlingGuide.rst
new file mode 100644
index 0000000000..62b154d8ff
--- /dev/null
+++ b/editor/docs/IMEHandlingGuide.rst
@@ -0,0 +1,1092 @@
+==================
+IME handling guide
+==================
+
+This document explains how Gecko handles IME.
+
+Introduction
+============
+
+IME is an abbreviation of Input Method Editor. This is a technical term from
+Windows but these days, this is used on other platforms as well.
+
+IME is a helper application of a user's text input. It handles native key
+events before or after focused application (depending on the platform) and
+creates a composition string (a.k.a. preedit string), suggests a list of what
+the user attempts to input, commits composition string as a selected item off
+the list and commits composition string without any conversion. IME is used by
+Chinese, Japanese, Korean and Taiwan users for inputting Chinese characters
+because the number of them is beyond thousands and cannot be input from the
+keyboard directly. However, especially on mobile devices nowadays, IME is also
+used for inputting Latin languages like autocomplete. Additionally, IME may be
+used for handwriting systems or speech input systems on some platforms.
+
+If IME is available on focused elements, we call that state "enabled". If IME
+is not fully available(i.e., user cannot enable IME), we call this state
+"disabled".
+
+If IME is enabled but users use direct input mode (e.g., for inputting Latin
+characters), we call it "IME is closed". Otherwise, we call it "IME is open".
+(FYI: "open" is also called "active" or "turned on". "closed" is also called
+"inactive" or "turned off")
+
+So, this document is useful when you're try to fix a bug for text input in
+Gecko.
+
+
+Composition string and clauses
+==============================
+
+Typical Japanese IME can input two or more words into a composition string.
+When a user converts from Hiragana characters to Chinese characters the
+composition string, Japanese IME separates the composition string into multiple
+clauses. For example, if a user types "watasinonamaehanakanodesu", it's
+converted to Hiragana characters, "ã‚ãŸã—ã®ãªã¾ãˆã¯ãªã‹ã®ã§ã™", automatically (In
+the following screenshots, the composition string has a wavy underline and the
+only one clause is called "raw input clause").
+
+.. image:: inputting_composition_string.png
+ :alt: Screenshot of raw composition string which is inputting Roman
+ character mode of MS-IME (Japanese)
+
+.. image:: raw_composition_string.png
+ :alt: Screenshot of raw composition string whose all characters are Hiragana
+ character (MS-IME, Japanese)
+
+When a user presses ``Convert`` key, Japanese IME separates the composition
+string as "ã‚ãŸã—ã®" (my), "ãªã¾ãˆã¯" (name is) and "ãªã‹ã®ã§ã™" (Nakano). Then,
+converts each clause with Chinese characters: "ç§ã®", "åå‰ã¯" and "中野ã§ã™" (In
+the following screenshot each clause is underlined and not connected
+adjacently. These clauses are called "converted clause").
+
+.. image:: converted_composition_string.png
+ :alt: Screenshot of converted composition string (MS-IME, Japanese)
+
+If one or more clauses were not converted as expected, the user can choose one
+of the clauses with Arrow keys and look for the expected result form the list
+in the drop down menu (In the following screenshot, the clause with the thicker
+underline is called "selected clause").
+
+.. image:: candidatewindow.png
+ :alt: Screenshot of candidate window of MS-IME (Japanese) which converts the
+ selected clause
+
+Basically, composition string and each clause style is rendered by Gecko. And
+the drop down menu is created by IME.
+
+Each clause is represented with selection in the editor. From chrome script,
+you can check it with ``nsISelectionController``. In native code, you can
+access it with either ``nsISelectionController`` or ``mozilla::SelectionType``
+(the latter is recommended because of type safer). And editor sets these IME
+selections from ``mozilla::TextRangeType`` which are sent by
+``mozilla::WidgetCompositionEvent`` as ``mozilla::TextRangeArray``. The
+following table explains the mapping between them.
+
+.. table:: Selection types of each clause of composition string or caret
+
+ +------------------------------------------------------------+---------------------------------------+-------------------------+-------------------------+
+ | |`nsISelectionController`_ |`mozilla::SelectionType`_|`mozilla::TextRangeType`_|
+ +============================================================+=======================================+=========================+=========================+
+ |Caret |``SELECTION_NORMAL`` |``eNormal`` |``eCaret`` |
+ +------------------------------------------------------------+---------------------------------------+-------------------------+-------------------------+
+ |Raw text typed by the user |``SELECTION_IME_RAW_INPUT`` |``eIMERawClause`` |``eRawClause`` |
+ +------------------------------------------------------------+---------------------------------------+-------------------------+-------------------------+
+ |Selected clause of raw text typed by the user |``SELECTION_IME_SELECTEDRAWTEXT`` |``eIMESelectedRawClause``|``eSelectedRawClause`` |
+ +------------------------------------------------------------+---------------------------------------+-------------------------+-------------------------+
+ |Converted clause by IME |``SELECTION_IME_CONVERTEDTEXT`` |``eIMEConvertedClause`` |``eConvertedClause`` |
+ +------------------------------------------------------------+---------------------------------------+-------------------------+-------------------------+
+ |Selected clause by the user or IME and also converted by IME|``SELECTION_IME_SELECTEDCONVERTEDTEXT``|``eIMESelectedClause`` |``eSelectedClause`` |
+ +------------------------------------------------------------+---------------------------------------+-------------------------+-------------------------+
+
+Note that typically, "Selected clause of raw text typed by the user" isn't used
+because when composition string is already separated to multiple clauses, that
+means that the composition string has already been converted by IME at least
+once.
+
+.. _nsISelectionController: https://searchfox.org/mozilla-central/source/dom/base/nsISelectionController.idl
+.. _mozilla::SelectionType: https://searchfox.org/mozilla-central/source/dom/base/nsISelectionController.idl
+.. _mozilla::TextRangeType: https://searchfox.org/mozilla-central/source/widget/TextRange.h
+
+Modules handling IME composition
+================================
+
+widget
+------
+
+Each widget handles native IME events and dispatches ``WidgetCompositionEvent``
+with ``mozilla::widget::TextEventDispatcher`` to represent the behavior of IME
+in the focused editor.
+
+This is the only module that depends on the users platform. See also
+`Native IME handlers`_ section for the detail of each platform's
+implementation.
+
+.. note::
+
+ Android widget still does not use ``TextEventDispatcher`` to dispatch
+ ``WidgetCompositionEvents``, see
+ `bug 1137567 <https://bugzilla.mozilla.org/show_bug.cgi?id=1137567>`__.
+
+mozilla::widget::TextEventDispatcher
+------------------------------------
+
+This class is used by native IME handler(s) on each platform. This capsules the
+logic to dispatch ``WidgetCompositionEvent`` and ``WidgetKeyboardEvent`` for
+making the behavior on each platform exactly same. For example, if
+``WidgetKeyboardEvent`` should be dispatched when there is a composition is
+managed by this class in XP level. First of use, native IME handlers get the
+rights to use ``TextEventDispatcher`` with a call of
+``BeginNativeInputTransaction()``. Then, ``StartComposition()``,
+``SetPendingComposition()``, ``FlushPendingComposition()``,
+``CommitComposition()``, etc. are available if
+``BeginNativeInputTransaction()`` return true. These methods automatically
+manage composition state and dispatch ``WidgetCompositionEvent`` properly.
+
+This is also used by ``mozilla::TextInputProcessor`` which can emulates (or
+implements) IME with chrome script. So, native IME handlers using this class
+means that the dispatching part is also tested by automated tests.
+
+mozilla::WidgetCompositionEvent
+-------------------------------
+
+Internally, ``WidgetCompositionEvent`` represents native IME behavior. Its
+message is one of following values:
+
+eCompositionStart
+^^^^^^^^^^^^^^^^^
+
+This is dispatched at starting a composition. This represents a DOM
+``compositionstart`` event. The mData value is a selected string at dispatching
+the DOM event and it's automatically set by ``TextComposition``.
+
+eCompositionUpdate
+^^^^^^^^^^^^^^^^^^
+
+This is dispatched by ``TextComposition`` when an ``eCompositionChange`` will
+change the composition string. This represents a DOM ``compositionupdate``
+event.
+
+eCompositionEnd
+^^^^^^^^^^^^^^^
+
+This is dispatched by ``TextComposition`` when an ``eCompositionCommitAsIs`` or
+``eCompositionCommit`` event is dispatched. This represents a DOM
+``compositionend`` event.
+
+eCompositionChange
+^^^^^^^^^^^^^^^^^^
+
+This is used internally only. This is dispatched at modifying a composition
+string, committing a composition, changing caret position and/or changing
+ranges of clauses. This represents a DOM text event which is not in any
+standards. ``mRanges`` should not be empty only with this message.
+
+eCompositionCommitAsIs
+^^^^^^^^^^^^^^^^^^^^^^
+
+This is used internally only. This is dispatched when a composition is
+committed with the string. The ``mData`` value should be always be an empty
+string. This causes a DOM text event without clause information and a DOM
+``compositionend`` event.
+
+eCompositionCommit
+^^^^^^^^^^^^^^^^^^
+
+This is used internally only. This is dispatched when a composition is
+committed with specific string. The ``mData`` value is the commit string. This
+causes a DOM text event without clause information and a DOM ``compositionend``
+event.
+
+.. table:: Table of event messages
+
+ +--------------------------+-------------------------------------------+-------------------------------+-----------------------+----------------------+
+ | |meaning of mData |who sets ``mData``? |``mRanges`` |representing DOM event|
+ +==========================+===========================================+===============================+=======================+======================+
+ |``eCompositionStart`` |selected string before starting composition|``TextComposition`` |``nullptr`` |``compositionstart`` |
+ +--------------------------+-------------------------------------------+-------------------------------+-----------------------+----------------------+
+ |``eCompositionUpdate`` |new composition string |``TextComposition`` |``nullptr`` |``compositionupdate`` |
+ +--------------------------+-------------------------------------------+-------------------------------+-----------------------+----------------------+
+ |``eCompositionEnd`` |commit string |``TextComposition`` |``nullptr`` |``compositionend`` |
+ +--------------------------+-------------------------------------------+-------------------------------+-----------------------+----------------------+
+ |``eCompositionChange`` |new composition string |widget (or ``TextComposition``)|must not be ``nullptr``|``text`` |
+ +--------------------------+-------------------------------------------+-------------------------------+-----------------------+----------------------+
+ |``eCompositionCommitAsIs``|N/A (must be empty) |nobody |``nullptr`` |None |
+ +--------------------------+-------------------------------------------+-------------------------------+-----------------------+----------------------+
+ |``eCompositionCommit`` |commit string |widget (or ``TextComposition``)|``nullptr`` |None |
+ +--------------------------+-------------------------------------------+-------------------------------+-----------------------+----------------------+
+
+PresShell
+---------
+
+``PresShell`` receives the widget events and decides an event target from
+focused document and element. Then, it sends the events and the event target to
+``IMEStateManager``.
+
+mozilla::IMEStateManager
+------------------------
+
+``IMEStateManager`` looks for a ``TextComposition`` instance whose native IME
+context is same as the widget' which dispatches the widget event. If there is
+no proper ``TextComposition`` instance, it creates the instance. And it sends
+the event to the ``TextComposition`` instance.
+
+Note that all instances of ``TextComposition`` are managed by
+``IMEStateManager``. When an instance is created, it's registered to the list.
+When composition completely ends, it's unregistered from the list (and released
+automatically).
+
+mozilla::TextComposition
+------------------------
+
+``TextComposition`` manages a composition and dispatches DOM
+``compositionupdate`` events.
+
+When this receives an ``eCompositionChange``, ``eCompositionCommit`` or
+``eCompositionCommitAsIs`` event, it dispatches the event to the stored node
+which was the event target of ``eCompositionStart`` event. Therefore, this
+class guarantees that all composition events for a composition are fired on
+same element.
+
+When this receives ``eCompositionChange`` or ``eCompositionCommit``, this
+checks if new composition string (or committing string) is different from the
+last data stored by the ``TextComposition``. If the composition event is
+changing the composition string, the ``TextComposition`` instance dispatches
+``WidgetCompositionEvent`` with ``eCompositionUpdate`` into the DOM tree
+directly and modifies the last data. The ``eCompositionUpdate`` event will
+cause a DOM ``compositionupdate`` event.
+
+When this receives ``eCompositionCommitAsIs`` or ``eCompositionCommit``, this
+dispatches an ``eCompositionEnd`` event which will cause a DOM
+``compositionend`` event after dispatching ``eCompositionUpdate`` event and/or
+``eCompositionChange`` event if necessary.
+
+One of the other important jobs of this is, when a focused editor handles a
+dispatched ``eCompositionChange`` event, this modifies the stored composition
+string and its clause information. The editor refers the stored information for
+creating or modifying a text node representing a composition string.
+
+And before dispatching ``eComposition*`` events, this class removes ASCII
+control characters from dispatching composition event's data in the default
+settings. Although, this can be disabled with
+``"dom.compositionevent.allow_control_characters"`` pref.
+
+Finally, this class guarantees that requesting to commit or cancel current
+composition to IME is perefored synchronously. See
+`Forcibly committing composition`_ section for the detail.
+
+editor/libeditor
+----------------
+
+`mozilla::EditorEventListener <https://searchfox.org/mozilla-central/source/editor/libeditor/EditorEventListener.cpp>`__
+listens for trusted DOM ``compositionstart``, ``text`` and ``compositionend``
+events and notifies
+`mozilla::EditorBase <https://searchfox.org/mozilla-central/source/editor/libeditor/EditorBase.cpp>`__
+and
+`mozilla::TextEditor <https://searchfox.org/mozilla-central/source/editor/libeditor/TextEditor.cpp>`__
+of the events.
+
+When ``EditorBase`` receives an ``eCompositionStart``
+(DOM ``"compositionstart"``) event, it looks for a proper ``TextComposition``
+instance and stores it.
+
+When ``TextEditor`` receives an ``eCompositionChange`` (DOM ``"text"``) event,
+it creates or modifies a text node which includes the composition string and
+`mozilla::CompositionTransaction <https://searchfox.org/mozilla-central/source/editor/libeditor/CompositionTransaction.cpp>`__
+(it was called ``IMETextTxn``) sets IME selections for representing the clauses
+of the composition string.
+
+When ``EditorBase`` receives an ``eCompositionEnd`` (DOM ``"compositionend"``)
+event, it releases the stored ``TextComposition`` instance.
+
+nsTextFrame
+-----------
+``nsTextFrame`` paints IME selections.
+
+mozilla::IMEContentObserver
+---------------------------
+
+``IMEContentObserver`` observes various changes of a focused editor. When a
+corresponding element of a ``TextEditor`` or ``HTMLEditor`` instance gets
+focus, an instance is created by ``IMEStateManager``, then, starts to observe
+and notifies ``widget`` of IME getting focus. When the editor loses focus, it
+notifies ``widget`` of IME losing focus and stops observing everything.
+Finally, it's destroyed by ``IMEStateManager``.
+
+This class observes selection changes (caret position changes), text changes of
+a focused editor and layout changes (by reflow or scroll) of everything in the
+document. It depends on the result of ``nsIWidget::GetIMEUpdatePreference()``
+what is observed.
+
+When this notifies ``widget`` of something, it needs to be safe to run
+script because notifying something may cause dispatching one or more DOM events
+and/or new reflow. Therefore, ``IMEContentObserver`` only stores which
+notification should be sent to ``widget``. Then,
+``mozilla::IMEContentObserver::IMENotificationSender`` tries to send the
+pending notifications when it might become safe to do that. Currently, it's
+tried:
+
+* after a native event is dispatched from ``PresShell::HandleEventInternal()``
+* when new focused editor receives DOM ``focus`` event
+* when next refresh driver tick
+
+.. note::
+
+ The 3rd timing may not be safe actually, but it causes a lot of oranges of
+ automated tests.
+
+See also `Notifications to IME`_ section for the detail of sending
+notifications.
+
+Currently, ``WidgetQueryContentEvent`` is handled via ``IMEContentObserver``
+because if it has a cache of selection, it can set reply of
+``eQuerySelectedText`` event only with the cache. That is much faster than
+using ``ContentEventHandler``.
+
+e10s support
+============
+
+Even when a remote process has focus, native IME handler in chrome process does
+its job. So, there is process boundary between native IME handler and focused
+editor. Unfortunately, it's not allowed to use synchronous communication from
+chrome process to a remote process. This means that chrome process (and also
+native IME and our native IME handler) cannot query the focused editor contents
+directly. For fixing this issue, we have ``ContentCache`` classes around
+process boundary.
+
+mozilla::ContentCache
+---------------------
+This is a base class of ``ContentCacheInChild`` and ``ContentCacheInParent``
+and IPC-aware. This has common members of them including all cache data:
+
+``mText``
+ Whole text in focused editor. This may be too big but IME may request all
+ text in the editor.
+
+ If we can separate editor contents per paragraph, moving selection between
+ paragraphs generates pseudo focus move, we can reduce this size and runtime
+ cost of ``ContentEventHandler``. However, we've not had a plan to do that
+ yet. Note that Microsoft Word uses this hack.
+
+``mCompositionStart``
+ Offset of composition string in ``mText``. When there is no composition,
+ this is ``UINT32_MAX``.
+
+``mSelection::mAnchor``, ``mSelection::mFocus``
+ Offset of selection anchor and focus in ``mText``.
+
+``mSelection::mWritingMode``
+ Writing mode at selection start.
+
+``mSelection::mAnchorCharRect``, ``mSelection::mFocusCharRect``
+ Next character rectangle of ``mSelection::mAnchor`` and
+ ``mSelection::mFocus``. If corresponding offset is end of the editor
+ contents, its rectangle should be a caret rectangle.
+
+ These rectangles shouldn't be empty rect.
+
+``mSelection::mRect``
+ Unified character rectangle in selection range. When the selection is
+ collapsed, this should be caret rect.
+
+``mFirstRect``
+ First character rect of ``mText``. When ``mText`` is empty string, this
+ should be caret rect.
+
+``mCaret::mOffset``
+ Always same as selection start offset even when selection isn't collapsed.
+
+``mCaret::mRect``
+ Caret rect at ``mCaret::mOffset``. If caret isn't actually exists, it's
+ computed with a character rect at the offset.
+
+``mTextRectArray::mStart``
+ If there is composition, ``mStart`` is same as ``mCompositionStart``.
+ Otherwise, ``UINT32_MAX``.
+
+``mTextRectArray::mRects``
+ Each character rectangle of composition string.
+
+``mEditorRect``
+ The rect of editor element.
+
+mozilla::ContentCacheInChild
+----------------------------
+
+This exists only in remote processes. This is created as a member of
+`PuppetWidget <https://searchfox.org/mozilla-central/source/widget/PuppetWidget.cpp>`__.
+When ``PuppetWidget`` receives notifications to IME from ``IMEContentObserver``
+in the remote process, it makes this class modify its cached content. Then,
+this class do that with ``WidgetQueryContentEvents``. Finally, ``PuppetWidget``
+sends the notification and ``ContentCacheInParent`` instance as
+``ContentCache`` to its parent process.
+
+mozilla::ContentCacheInParent
+-----------------------------
+
+This exists as a member of ``TabParent``. When ``TabParent`` receives
+notification from corresponding remote process, it assigns
+``ContentCacheInParent`` new ``ContentCache`` and post the notification to
+``ContentCacheInParent``. If all sent ``WidgetCompositionEvents`` and
+``WidgetSelectionEvents`` are already handled in the remote process,
+``ContentCacheInParent`` sending the notifications to widget.
+
+And also this handles ``WidgetQueryContentEvents`` with its cache. Supported
+event messages of them are:
+
+* ``eQuerySelectedText`` (only with ``SelectionType::eNormal``)
+* ``eQueryTextContent``
+* ``eQueryTextRect``
+* ``eQueryCaretRect``
+* ``eQueryEditorRect``
+
+Additionally, this does not support query content events with XP line breakers
+but this must not be any problem since native IME handlers query contents with
+native line breakers.
+
+``ContentCacheInParent`` also manages sent ``WidgetCompositionEvents`` and
+``WidgetSelectionEvents``. After these events are handled in the remote
+process, ``TabParent`` receives it with a call of
+``RecvOnEventNeedingAckHandled()``. Then, it calls
+``ContentCacheInParent::OnEventNeedingAckHandled()``. Finally,
+``ContentCacheInParent`` flushes pending notifications.
+
+How do mozilla::TextComposition and mozilla::IMEStateManager work in e10s mode?
+-------------------------------------------------------------------------------
+In remote process, they work as non-e10s mode. On the other hand, they work
+specially in parent process.
+
+When ``IMEStateManager`` in parent process receives ``eCompositionStart``, it
+creates ``TextComposition`` instance normally. However, if the event target has
+remote contents, ``TextComposition::DispatchCompositionEvent()`` directly sends
+the event to the remote process instead of dispatching the event into the
+target DOM tree in the process.
+
+That means that even in a parent process, anybody can retrieve
+``TextComposition`` instance, but it just does nothing in parent process.
+
+``IMEStateManager`` works more complicated because ``IMEStateManager`` in each
+process need to negotiate about owner ship of managing input context.
+
+When a remote process gets focus, temporarily, ``IMEStateManager`` in parent
+process disables IME in the widget. After that, ``IMEStateManager`` in the
+remote process will set proper input context for the focused editor. At this
+time, ``IMEStateManager`` in the parent process does nothing. Therefore,
+``IMEContentObserver`` is never created while a remote process has focus.
+
+When a remote process loses focus, ``IMEStateManager`` in parent process
+notifies ``IMEStateManager`` in the remote process of
+"Stop IME state management". When ``IMEStateManager::StopIMEStateManagement()``
+is called in the remote process by this, the ``IMEStateManager`` forgets all
+focus information (i.e., that indicates nobody has focus).
+
+When ``IMEStateManager`` in parent process is notified of pseudo focus move
+from or to menubar while a remote process has focus, it notifies the remote
+process of "Menu keyboard listener installed". Then, ``TabChild`` calls
+``IMEStateManager::OnInstalledMenuKeyboardListener()`` in the remote process.
+
+Style of each clause
+--------------------
+
+The style of each IME selection is managed by
+`LookAndFeel <https://searchfox.org/mozilla-central/source/widget/LookAndFeel.h>`__
+class per platform. Therefore, it can be overridden by prefs.
+
+Background color, foreground color (text color) and underline color can be
+specified with following prefs. The values must be string of "#rrggbb" format.
+
+* ``ui.IMERawInputBackground``
+* ``ui.IMERawInputForeground``
+* ``ui.IMERawInputUnderline``
+* ``ui.IMESelectedRawTextBackground``
+* ``ui.IMESelectedRawTextForeground``
+* ``ui.IMESelectedRawTextUnderline``
+* ``ui.IMEConvertedTextBackground``
+* ``ui.IMEConvertedTextForeground``
+* ``ui.IMEConvertedTextUnderline``
+* ``ui.IMESelectedConvertedTextBackground``
+* ``ui.IMESelectedConvertedTextForeground``
+* ``ui.IMESelectedConvertedTextUnderline``
+
+Underline style can be specified with the following prefs. The values are
+integer, 0: none, 1: dotted, 2: dashed, 3: solid, 4: double, 5: wavy (The
+values same as ``mozilla::StyleTextDecorationStyle`` defined in
+`nsStyleConsts.h <https://searchfox.org/mozilla-central/source/layout/style/nsStyleConsts.h>`__).
+
+* ``ui.IMERawInputUnderlineStyle``
+* ``ui.IMESelectedRawTextUnderlineStyle``
+* ``ui.IMEConvertedTextUnderlineStyle``
+* ``ui.IMESelectedConvertedTextUnderlineStyle``
+
+Underline width can be specified with ``"ui.IMEUnderlineRelativeSize"`` pref.
+This affects all types of clauses. The value should be 100 or 200. 100 means
+normal width, 200 means double width.
+
+On some platforms, IME may support its own style for each clause. Currently,
+this feature is supported in TSF mode of Windows and on Linux. The style
+information is stored in ``TextRangeStyle`` which is defined in
+`TextRange.h <https://searchfox.org/mozilla-central/source/widget/TextRange.h>`__.
+It's a member of ``TextRange``. ``TextRange`` is stored in ``mRanges`` of
+``WidgetCompositionEvent`` only when its message is ``eCompositionChange``.
+
+Lifetime of composition string
+==============================
+
+When native IME notifies Gecko of starting a composition, a widget dispatches
+``WidgetCompositionEvent`` with ``eCompositionStart`` which will cause a DOM
+``compositionstart`` event.
+
+When native IME notifies Gecko of a composition string change, a caret position
+change and/or a change of length of clauses, a widget dispatches
+``WidgetCompositionEvent`` with ``eCompositionChange`` event. It will cause a
+DOM ``compositionupdate`` event when composition string is changing. That is
+dispatched by ``TextComposition`` automatically. After that when the widget and
+``PresShell`` of the focused editor have not been destroyed yet, the
+``eCompositionChange`` will cause a DOM text event which is not in any web
+standards.
+
+When native IME notifies Gecko of the ending of a composition, a widget
+dispatches ``WidgetCompositionEvent`` with ``eCompositionCommitAsIs`` or
+``eCompositionCommit``. If the committing string is different from the last set
+of data (i.e., if the event message is ``eCompositionCommit``),
+``TextComposition`` dispatches a DOM ``compositionupdate`` event. After that,
+when the widget and ``PresShell`` of the focused editor have not been destroyed
+yet, an ``eCompositionChange`` event dispatched by ``TextComposition``, that
+causes a DOM text event. Finally, if the widget and PresShell of the focused
+editor has not been destroyed yet too, ``TextComposition`` dispatches an
+``eCompositionEnd`` event which will cause a DOM compositionend event.
+
+Limitation of handling composition
+==================================
+
+Currently, ``EditorBase`` touches undo stack at receiving every
+``WidgetCompositionEvent``. Therefore, ``EditorBase`` requests to commit
+composition when the following cases occur:
+
+* The editor loses focus
+* The caret is moved by mouse or Javascript
+* Value of the editor is changed by Javascript
+* Node of the editor is removed from DOM tree
+* Somethings object is modified in an HTML editor, e.g., resizing an image
+* Composition string is moved to a different position which is specified by
+ native IME (e.g., only a part of composition is committed)
+
+In the future, we should fix this limitation. If we make ``EditorBase`` not
+touch undo stack until composition is committed, some of the cases must be
+fixed.
+
+Notifications to IME
+====================
+
+XP part of Gecko uses ``nsIWidget::NotifyIME()`` for notifying ``widget`` of
+something useful to handle IME. Note that some of them are notified only when
+``nsIWidget::GetIMEUpdatePreference()`` returns flags which request the
+notifications.
+
+``NOTIFY_IME_OF_TEXT_CHANGE``, ``NOTIFY_IME_OF_SELECTION_CHANGE``,
+``NOTIFY_IME_OF_POSITION_CHANGE`` and
+``NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED`` are always sent by following order:
+
+1. ``NOTIFY_IME_OF_TEXT_CHANGE``
+2. ``NOTIFY_IME_OF_SELECTION_CHANGE``
+3. ``NOTIFY_IME_OF_POSITION_CHANGE``
+4. ``NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED``
+
+If sending one of above notifications causes higher priority notification, the
+sender should abort to send remaining notifications and restart from high
+priority notification again.
+
+Additionally, all notifications except ``NOTIFY_IME_OF_BLUR`` should be sent
+only when it's safe to run script since the notification may cause querying
+content and/or dispatching composition events.
+
+NOTIFY_IME_OF_FOCUS
+-------------------
+
+When an editable editor gets focus and ``IMEContentObserver`` starts to observe
+it, this is sent to widget. This must be called after the previous
+``IMEContentObserver`` notified widget of ``NOTIFY_IME_OF_BLUR``.
+
+Note that even if there are pending notifications, they are canceled when
+``NOTIFY_IME_OF_FOCUS`` is sent since querying content with following
+notifications immediately after getting focus does not make sense. The result
+is always same as the result of querying contents at receiving this
+notification.
+
+NOTIFY_IME_OF_BLUR
+------------------
+
+When an ``IMEContentObserver`` instance ends observing the focused editor, this
+is sent to ``widget`` synchronously because assumed that this notification
+causes neither query content events nor composition events.
+
+If ``widget`` wants notifications even while all windows are inactive,
+``IMEContentObserver`` doesn't end observing the focused editor. I.e., in this
+case, ``NOTIFY_IME_OF_FOCUS`` and ``NOTIFY_IME_OF_BLUR`` are not sent to
+``widget`` when a window which has a composition is being activated or
+inactivated.
+
+When ``widget`` wants notifications during inactive, ``widget`` includes
+``NOTIFY_DURING_DEACTIVE`` to the result of
+``nsIWidget::GetIMEUpdatePreference()``.
+
+If this notification is tried to sent before sending ``NOTIFY_IME_OF_FOCUS``,
+all pending notifications and ``NOTIFY_IME_OF_BLUR`` itself are canceled.
+
+NOTIFY_IME_OF_TEXT_CHANGE
+-------------------------
+
+When text of focused editor is changed, this is sent to ``widget`` with a range
+of the change. But this is sent only when result of
+``nsIWidget::GetIMEUpdatePreference()`` includes ``NOTIFY_TEXT_CHANGE``.
+
+If two or more text changes occurred after previous
+``NOTIFY_IME_OF_TEXT_CHANGE`` or ``NOTIFY_IME_OF_FOCUS``, the ranges of all
+changes are merged. E.g., if first change is from ``1`` to ``5`` and second
+change is from ``5`` to ``10``, the notified range is from ``1`` to ``10``.
+
+If all merged text changes were caused by composition,
+``IMENotification::mTextChangeData::mCausedOnlyByComposition`` is set to true.
+This is useful if native IME handler wants to ignore all text changes which are
+expected by native IME.
+
+If at least one text change of the merged text changes was caused by current
+composition,
+``IMENotification::mTextChangeData::mIncludingChangesDuringComposition`` is set
+to true. This is useful if native IME handler wants to ignore delayed text
+change notifications.
+
+If at least one text change of the merged text changes was caused when there
+was no composition,
+``IMENotification::mTextChangeData::mIncludingChangesWithoutComposition`` is
+set to true.
+
+NOTIFY_IME_OF_SELECTION_CHANGE
+------------------------------
+
+When selection (or caret position) is changed in focused editor, widget is
+notified of this.
+
+If the last selection change was occurred by a composition event event
+handling, ``IMENotification::mSelectionChangeData::mCausedByComposition`` is
+set to true. This is useful if native IME handler wants to ignore the last
+selection change which is expected by native IME.
+
+If the last selection change was occurred by an ``eSetSelection`` event,
+``IMENotification::mSelectionChangeData::mCausedBySelectionEvent`` is set to
+true. This is useful if native IME handler wants to ignore the last selection
+change which was requested by native IME.
+
+If the last selection is occurred during a composition,
+``IMENotification::mSelectionChangeData::mOccurredDuringComposition`` is set to
+true. This is useful if native IME handler wants to ignore the last selection
+change which occurred by web application's ``compositionstart`` or
+``compositionupdate`` event handler before inserting composition string.
+
+NOTIFY_IME_OF_POSITION_CHANGE
+-----------------------------
+
+When reflow or scroll occurs in the document, this is sent to widget, but this
+is sent only when result of ``nsIWidget::GetIMEUpdatePreference()`` includes
+``NOTIFY_POSITION_CHANGE``.
+
+This might be useful to update a candidate window position or something.
+
+NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED
+---------------------------------------
+
+After ``TextComposition`` handles ``eCompositionStart``,
+``eCompositionChange``, ``eComposiitionCommit`` or ``eCompositionCommitAsIs``,
+this notification is sent to widget. This might be useful to update a candidate
+window position or something.
+
+NOTIFY_IME_OF_MOUSE_BUTTON_EVENT
+--------------------------------
+
+When a ``mousedown`` event or a ``mouseup`` event is fired on a character in a
+focused editor, this is sent to widget. But this is sent only when result of
+``nsIWidget::GetIMEUpdatePreference()`` includes
+``NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR``. This is sent with various information.
+See ``IMENotification::mMouseButtonEventData`` in
+`IMEData.h <https://searchfox.org/mozilla-central/source/widget/IMEData.h>`__
+for the detail.
+
+If native IME supports mouse button event handling, ``widget`` should notify
+IME of mouse button events with this. If IME consumes an event, ``widget``
+should return ``NS_SUCCESS_EVENT_CONSUMED`` from ``nsIWidget::NotifyIME()``.
+Then, ``EditorBase`` doesn't handle the mouse event.
+
+Note that if a ``mousedown`` event or a ``mouseup`` event is consumed by a web
+application (before a focused editor handles it), this notification is not sent
+to ``widget``. This means that web applications can handle mouse button events
+before IME.
+
+Requests to IME
+===============
+
+XP part of Gecko can request IME to commit or cancel composition. This must be
+requested via ``IMEStateManager::NotifyIME()``. Then, ``IMEStateManager`` looks
+for a proper ``TextComposition`` instance. If it's found,
+``TextComposition::RequestToCommit()`` for calling ``nsIWidget::NotifyIME()``
+and handles some extra jobs.
+
+widget should call the proper native API if it's available. Even if commit or
+canceling composition does not occur synchronously, widget doesn't need to
+emulate it since ``TextComposition`` will emulate it automatically. In other
+words, widget should only request to commit or cancel composition to IME.
+
+REQUEST_TO_COMMIT_COMPOSITION
+-----------------------------
+
+A request to commit current composition to IME. See also following
+"`Forcibly committing composition`_" section for additional information.
+
+REQUEST_TO_CANCEL_COMPOSITION
+-----------------------------
+
+A request to cancel current composition to IME. In other words, a request to
+commit current composition with an empty string.
+
+Forcibly committing composition
+===============================
+
+When ``TextComposition::RequestToCommit()`` calls ``nsIWidget::NotifyIME()``,
+it guarantees synchronous commit or canceling composition.
+
+In order to put it into practice, we need to handle the following four
+scenarios:
+
+The composition is committed with non-empty string synchronously
+----------------------------------------------------------------
+
+This is the most usual case. In this case, ``TextComposition`` handles
+``WidgetCompositionEvent`` instances during a request normally. However, in a
+remote process in e10s mode, this case never occurs since requests to native
+IME is handled asynchronously.
+
+The composition is not committed synchronously but later
+--------------------------------------------------------
+
+This is the only case in a remote process in e10s mode or occurs on Linux even
+in non-e10s mode if the native IME is iBus. The callers of
+``NotifyIME(REQUEST_TO_COMMIT_COMPOSITION)`` may expect that composition string
+is committed immediately for their next job. For such a case,
+``TextComposition::RequestToCommit()`` synthesizes DOM composition events and a
+DOM text event for emulating to commit composition synchronously. Additionally,
+``TextComposition`` ignores committing events which are dispatched by widget
+when the widget receives native IME events.
+
+In this case, using the last composition string as commit string.
+
+However, if the last composition string is only an ideographic space (fullwidth
+space), the composition string may be a placeholder of some old Chinese IME on
+Windows.
+
+.. image:: ChangJie.png
+ :alt: aScreenshot of ChangJie (Traditional Chinese IME) which puts an
+ ideographic space into composition string for placeholder
+
+In this case, although, we should not commit the placeholder character because
+it's not a character which the user wanted to input but we commit it as is. The
+reason is, inputting an ideographic space causes a composition. Therefore, we
+cannot distinguish if committing composition is unexpected. If the user uses
+such old Chinese IME, ``"intl.ime.remove_placeholder_character_at_commit"``
+pref may be useful but we don't support them anymore in default settings
+(except if somebody will find a good way to fix this issue).
+
+The composition is committed synchronously but with empty string
+----------------------------------------------------------------
+
+This case may occur on Linux or with some IME on other platforms. If a web
+application implements autocomplete, committing with different strings
+especially an empty string it might cause confusion.
+
+In this case, TextComposition overwrites the commit string of
+``eCompositionChange`` event dispatched by widget. However, if the last
+composition string is only an ideographic space, it shouldn't be committed. See
+the previous case.
+
+Note that this case doesn't work as expected when composition is in a remote
+process in e10s mode.
+
+The composition is not committed
+--------------------------------
+
+On Linux, there is no API to request commit or canceling composition forcibly.
+Instead, Gecko uses ``gtk_im_context_reset()`` API for this purpose because
+most IME cancel composition with it. But there are some IMEs which do nothing
+when Gecko calls it.
+
+If this occurs, Gecko should restart composition with a DOM
+``compositionstart`` event , a DOM ``compositionupdate`` event and a DOM
+``text`` event at caret position.
+
+.. note::
+
+ This issue hasn't been supported yet.
+
+IME state management
+====================
+
+IME is a text input system. It means that except when a user wants to input
+some text, IME shouldn't be available. For example, pressing the space key to
+attempt scrolling a page may be consumed and prevented by IME. Additionally,
+password editors need to request special behavior with IME.
+
+For solving this issue, Gecko sets the proper IME state at DOM focus change.
+
+First, when a DOM node gets focus, nsFocusManager notifies ``IMEStateManager``
+of the new focused node (calls ``IMEStateManager::OnChangeFocus()``).
+``IMEStateManager`` asks desired IME state by calling
+``nsIContent::GetDesiredIMEState()`` of the node. If the node owns
+``TextEditor`` instance, it asks for the desired IME state from the editor and
+returns the result.
+
+Next, ``IMEStateManager`` initializes ``InputContext`` (defined in
+`IMEData.h <https://searchfox.org/mozilla-central/source/widget/IMEData.h>`__)
+with the desired IME state and node information. Then, it calls
+``nsIWidget::SetInputContext()`` with the ``InputContext``.
+
+Finally, widget stores the InputContext and enables or disables IME if the
+platform has such an API.
+
+InputContext
+------------
+
+InputContext is a struct. Its ``mIMEState``, ``mHTMLInputType``,
+``mHTMLInputInputMode`` and ``mActionHint`` are set at
+``nsIWidget::SetInputContext()`` called.
+
+mIMEState
+^^^^^^^^^
+IME state has two abilities. One is enabled state:
+
+ENABLED
+"""""""
+
+This means IME is fully available. E.g., when an editable element such as
+``<input type="text">``, ``<textarea>`` or ``<foo contenteditable>`` has focus.
+
+DISABLED
+""""""""
+
+This means IME is not available. E.g., when a non-editable element has focus or
+no element has focus, the desired IME state is ``DISABLED``.
+
+PASSWORD
+""""""""
+
+This means IME state should be the same as the state when a native password
+field has focus. This state is set only when
+``<input type="password"> (ime-mode: auto;)``,
+``<input type="text" style="ime-mode: disabled;">`` or
+``<textarea style="ime-mode: disabled;">``.
+
+The other is IME open state:
+
+DONT_CHANGE_OPEN_STATE
+""""""""""""""""""""""
+
+The open state of IME shouldn't be changed. I.e., Gecko should keep the last
+IME open state.
+
+OPEN
+""""
+Open IME. This is specified only when ime-mode of the new focused element is
+``active``.
+
+CLOSE
+"""""
+Close IME. This is specified only when ime-mode of the new focused element is
+``inactive``.
+
+.. note::
+
+ E.g., on Linux, applications cannot manage IME open state. On such
+ platforms, this is ignored.
+
+.. note::
+
+ IME open state should be changed only when ``nsIWidget::SetInputContext()``
+ is called at DOM focus change because changing IME open state while an
+ editor has focus makes users confused. The reason why
+ ``nsIWidget::SetInputContext()`` is called is stored in
+ ``InputContextAction::mCause``.
+
+How does Gecko disable IME in IMM mode on Windows
+"""""""""""""""""""""""""""""""""""""""""""""""""
+
+Every window on Windows is associated an ``IMContext``. When Gecko disables
+IME,
+`mozilla::widget::IMEHandler <https://searchfox.org/mozilla-central/source/widget/windows/WinIMEHandler.cpp>`__::SetInputContext()
+disassociates the context from the window.
+
+How does Gecko disable IME in TSF mode on Windows
+"""""""""""""""""""""""""""""""""""""""""""""""""
+
+`mozilla::widget::TSFTextStore <https://searchfox.org/mozilla-central/source/widget/windows/TSFTextStore.cpp>`__
+sets focus to a dummy context which disables the keyboard.
+
+How does Gecko disable IME on Mac
+"""""""""""""""""""""""""""""""""
+
+`mozilla::widget::TextInputHandler <https://searchfox.org/mozilla-central/source/widget/cocoa/TextInputHandler.mm>`__::HandleKeyDownEvent()
+doesn't call focused view's interpretKeyEvents. This prevents native key events
+to be passed to IME.
+
+How does Gecko disable IME on GTK
+"""""""""""""""""""""""""""""""""
+
+`mozilla::widget::IMContextWrapper <https://searchfox.org/mozilla-central/source/widget/gtk/IMContextWrapper.cpp>`__
+sets focus to a dummy context which doesn't have IME composition.
+
+How does Gecko disable IME on Android
+"""""""""""""""""""""""""""""""""""""
+
+?
+
+mHTMLInputType
+^^^^^^^^^^^^^^
+
+The value is a string representing the focused editor.
+
+``"text"``, ``"password"``, ``"number"``, etc.
+ When an ``<input>`` element gets focus, the value is the type of the input
+ element.
+
+``"textarea"``
+ When a ``<textarea>`` element gets focus, the value is ``"textarea"``.
+
+``""``
+ When an HTML editor (an element whose ``contenteditable`` attribute is
+ ``true`` or document whose ``designMode`` is ``"on"``) gets focus, the
+ value is empty. And also, when the other elements get focus.
+
+mHTMLInputMode
+^^^^^^^^^^^^^^
+
+The value is ``inputmode`` attribute value of the focused editor. This is set
+only when ``"dom.forms.inputmode"`` pref is true.
+
+mActionHint
+^^^^^^^^^^^
+
+The value is ``enterkeyhint`` attribute value of the focused editor when
+``"dom.forms.enterkeyhint"`` pref is true. This is useful for deciding the
+caption for the submit button in virtual keyboard. E.g., the value could be
+``"Go"``, ``"Next"`` or ``"Search"``.
+
+Native IME handlers
+===================
+
+Following classes handles IME on each platform:
+
+Windows
+-------
+
+`mozilla::widget::IMEHandler`__
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This class manages input method context of each window and makes ``IMMHandler``
+or ``TSFTextStore`` work with active IME and focused editor. This class has
+only static members, i.e., never created its instance.
+
+__ https://searchfox.org/mozilla-central/source/widget/windows/WinIMEHandler.cpp
+
+`mozilla::widget::IMMHandler`__
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This class is used when TSF mode is disabled by pref (``"intl.tsf.enabled"``
+since 108, formerly named ``"intl.tsf.enable"``) or active IME is for IMM
+(i.e., not TIP for TSF).
+
+This class handles ``WM_IME_*`` messages and uses ``Imm*()`` API. This is a
+singleton class since Gecko supports only on IM context in a process.
+Typically, a process creates windows with default IM context. Therefore, this
+design is enough (ideally, an instance should be created per IM context,
+though). The singleton instance is created when it becomes necessary.
+
+__ https://searchfox.org/mozilla-central/source/widget/windows/IMMHandler.cpp
+
+`mozilla::widget::TSFTextStore`__
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This class handles IME events in TSF mode and when TIP (IME implemented with
+TSF) is active. This instances are created when an editable element gets focus
+and released when it loses focus.
+
+``TSFTextStore`` implements some COM interfaces which is necessary to work with
+TIP. And similarly, there is a singleton class, ``TSFStaticSink``, to observe
+active TIP changes.
+
+TSF is the most complicated IME API on all platforms, therefore, design of this
+class is also very complicated.
+
+FIrst, TSF/TIP requests to lock the editor content for querying or modifying
+the content or selection. However, web standards don't have such mechanism.
+Therefore, when it's requested, ``TSFTextStore`` caches current content and
+selection with ``WidgetQueryContentEvent``. Then, it uses the cache to reply to
+query requests, and modifies the cache as they requested. At this time,
+``TSFTextStore`` saves the requests of modification into the queue called
+``PendingAction``. Finally, after unlocking the contents, it flushes the
+pending actions with dispatches ``WidgetCompositionEvent``s via
+``TextEventDispatcher``.
+
+Then, ``IMEContentObserver`` will notify some changes caused by the dispatched
+``WidgetCompositionEvents`` (they are notified synchronously in chrome or
+non-e10s mode, but asynchronously from a remote process in e10s mode). At this
+time, ``TSFTextStore`` may receive notifications which indicates web
+application changes the content differently from cache in ``TSFTextStore``.
+However, ``TSFTextStore`` ignores such fact temporarily until the composition
+is finished completely. The reason is that, notifying unexpected text or
+selection changes to TSF and/or TIP during composition may behave them odd.
+
+When a composition is committed and it receives
+``NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED``, ``TSFTextStore`` clears the cache
+of contents and notifying TSF of merged text changes and the last selection
+change if they are not caused by composition. By this step, TSF and TIP may
+sync its internal cache with actual contents.
+
+Note that if new composition is started before
+``NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED`` notification, ``TSFTextStore``
+handles the a composition with cached contents which may be different from
+actual contents. So, e.g., reconversion around caret may not work as unexpected
+in such case, but we don't have a good solution for this issue.
+
+On the other hand, ``TSFTextStore`` cannot cache character rectangles since if
+there are a lot of characters, caching the rectangles require a lot of CPU cost
+(to compute each rect) and memory. Therefore, ``TSFTextStore`` will use
+insertion point relative query for them
+`bug 1286157 <https://bugzilla.mozilla.org/show_bug.cgi?id=1286157>`__. Then,
+it can retrieve expected character's rect even if the cache of ``TSFTextStore``
+is different from the actual contents because TIP typically needs caret
+position's character rect (for a popup to indicate current input mode or next
+word suggestion list) or first character rect of the target clause of current
+composition (for a candidate list window of conversion).
+
+__ https://searchfox.org/mozilla-central/source/widget/windows/TSFTextStore.cpp
+
+Mac
+---
+
+Both IME and key events are handled in
+`TextInputHandler.mm <https://searchfox.org/mozilla-central/source/widget/cocoa/TextInputHandler.mm>`__.
+
+``mozilla::widget::TextInputHandlerBase`` is the most base class.
+``mozilla::widget::IMEInputHandler`` inherits ``TextInputHandlerBase`` and
+handles IME related events. ``mozilla::widget::TextInputHandler`` inherits
+``TextInputHandlerBase`` and implements ``NSTextInput`` protocol of Cocoa. Its
+instance is created per
+`nsChildView <https://searchfox.org/mozilla-central/source/widget/cocoa/nsChildView.mm>`__
+instance.
+
+GTK
+---
+
+`mozilla::widget::IMContextWrapper <https://searchfox.org/mozilla-central/source/widget/gtk/IMContextWrapper.cpp>`__
+handles IME. The instance is created per top level window.
+
+Android
+-------
+
+`org.mozilla.geckoview.GeckoEditable <https://searchfox.org/mozilla-central/source/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoEditable.java>`__ handles native IME events and `mozilla::widget::GeckoEditableSupport <https://searchfox.org/mozilla-central/source/widget/android/GeckoEditableSupport.cpp>`__
+dispatches ``Widget*Event``.
diff --git a/editor/docs/candidatewindow.png b/editor/docs/candidatewindow.png
new file mode 100644
index 0000000000..b11822a2d8
--- /dev/null
+++ b/editor/docs/candidatewindow.png
Binary files differ
diff --git a/editor/docs/converted_composition_string.png b/editor/docs/converted_composition_string.png
new file mode 100644
index 0000000000..fe61095bfb
--- /dev/null
+++ b/editor/docs/converted_composition_string.png
Binary files differ
diff --git a/editor/docs/index.rst b/editor/docs/index.rst
new file mode 100644
index 0000000000..f272dd6684
--- /dev/null
+++ b/editor/docs/index.rst
@@ -0,0 +1,12 @@
+Editor
+======
+
+This collection of linked pages contains the document for the
+editor. The documents live in editor/docs/.
+
+.. toctree::
+ :maxdepth: 1
+
+ EditorModuleStructure
+ EditorModuleSpecificRules
+ IMEHandlingGuide
diff --git a/editor/docs/inputting_composition_string.png b/editor/docs/inputting_composition_string.png
new file mode 100644
index 0000000000..0df68b90c1
--- /dev/null
+++ b/editor/docs/inputting_composition_string.png
Binary files differ
diff --git a/editor/docs/raw_composition_string.png b/editor/docs/raw_composition_string.png
new file mode 100644
index 0000000000..afa3c0062a
--- /dev/null
+++ b/editor/docs/raw_composition_string.png
Binary files differ
diff --git a/editor/libeditor/AutoRangeArray.cpp b/editor/libeditor/AutoRangeArray.cpp
new file mode 100644
index 0000000000..bff0806a8d
--- /dev/null
+++ b/editor/libeditor/AutoRangeArray.cpp
@@ -0,0 +1,1195 @@
+/* -*- 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 "EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc
+#include "EditorForwards.h" // for CollectChildrenOptions
+#include "HTMLEditUtils.h" // for HTMLEditUtils
+#include "HTMLEditHelpers.h" // for SplitNodeResult
+#include "WSRunObject.h" // for WSRunScanner
+
+#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;
+
+/******************************************************************************
+ * 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 (size_t i = mRanges.Length(); i > 0; i--) {
+ const OwningNonNull<nsRange>& range = mRanges[i - 1];
+ if (!AutoRangeArray::IsEditableRange(range, aEditingHost)) {
+ mRanges.RemoveElementAt(i - 1);
+ 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(i - 1);
+ 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 (uint32_t i : IntegerRange(mRanges.Length())) {
+ const OwningNonNull<nsRange>& range = mRanges[i];
+ 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 (uint32_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.FindBetterInsertionPoint(atStartOfSelection);
+ 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 (auto& 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);
+ 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)) {
+ 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
+GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock(
+ const EditorDOMPoint& aPointInLine, EditSubAction aEditSubAction,
+ 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, &aEditingHost);
+ previousEditableContent && previousEditableContent->GetParentNode() &&
+ !HTMLEditUtils::IsVisibleBRElement(*previousEditableContent) &&
+ !HTMLEditUtils::IsBlockElement(*previousEditableContent);
+ previousEditableContent = HTMLEditUtils::GetPreviousContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary, &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, &aEditingHost);
+ !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
+ point.GetContainerParent();
+ nearContent = HTMLEditUtils::GetPreviousContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary, &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;
+ // 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;
+ }
+
+ 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 GetPointAfterFollowingLineBreakOrAtFollowingBlock(
+ const EditorDOMPoint& aPointInLine, 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 retrun 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 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, &aEditingHost);
+ nextEditableContent &&
+ !HTMLEditUtils::IsBlockElement(*nextEditableContent) &&
+ nextEditableContent->GetParent();
+ nextEditableContent = HTMLEditUtils::GetNextContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary, &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 retrun 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, &aEditingHost);
+ !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
+ point.GetContainerParent();
+ nearContent = HTMLEditUtils::GetNextContent(
+ point, ignoreNonEditableNodeAndStopAtBlockBoundary, &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;
+ }
+
+ point.SetAfter(point.GetContainer());
+ if (NS_WARN_IF(!point.IsSet())) {
+ break;
+ }
+ }
+ return point;
+}
+
+void AutoRangeArray::ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction aEditSubAction, const Element& aEditingHost) {
+ // FYI: This is originated in
+ // https://searchfox.org/mozilla-central/rev/1739f1301d658c9bff544a0a095ab11fca2e549d/editor/libeditor/HTMLEditSubActionHandler.cpp#6712
+
+ bool removeSomeRanges = false;
+ for (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, 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 (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,
+ 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());
+ 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 = GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock(
+ startPoint, aEditSubAction, 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 =
+ GetPointAfterFollowingLineBreakOrAtFollowingBlock(endPoint, 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, 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 (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 (OwningNonNull<RangeItem>& item : Reversed(rangeItemArray)) {
+ // MOZ_KnownLive because 'rangeItemArray' is guaranteed to keep it alive.
+ Result<EditorDOMPoint, nsresult> splitParentsResult =
+ aHTMLEditor.SplitParentInlineElementsAtRangeBoundaries(
+ MOZ_KnownLive(*item), aEditingHost, aAncestorLimiter);
+ if (MOZ_UNLIKELY(splitParentsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitParentInlineElementsAtRangeBoundaries() failed");
+ rv = splitParentsResult.unwrapErr();
+ break;
+ }
+ if (splitParentsResult.inspect().IsSet()) {
+ pointToPutCaret = splitParentsResult.unwrap();
+ }
+ }
+ // Then unregister the ranges
+ for (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 (size_t i : Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
+ if (!EditorUtils::IsEditableContent(aOutArrayOfContents[i],
+ EditorUtils::EditorType::HTML)) {
+ aOutArrayOfContents.RemoveElementAt(i);
+ }
+ }
+ }
+ }
+
+ switch (aEditSubAction) {
+ case EditSubAction::eCreateOrRemoveBlock: {
+ // 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;
+ }
+ for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsIContent> content = aOutArrayOfContents[i];
+ if (HTMLEditUtils::IsListItem(content)) {
+ aOutArrayOfContents.RemoveElementAt(i);
+ HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, i,
+ options);
+ }
+ }
+ // Empty text node shouldn't be selected if unnecessary
+ for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
+ if (Text* text = aOutArrayOfContents[i]->GetAsText()) {
+ // Don't select empty text except to empty block
+ if (!HTMLEditUtils::IsVisibleTextNode(*text)) {
+ aOutArrayOfContents.RemoveElementAt(i);
+ }
+ }
+ }
+ break;
+ }
+ case EditSubAction::eCreateOrChangeList: {
+ // XXX aCollectNonEditableNodes is ignored here. Maybe a bug.
+ CollectChildrenOptions options = {
+ CollectChildrenOption::CollectTableChildren};
+ for (size_t i = aOutArrayOfContents.Length(); i > 0; i--) {
+ // 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[i - 1];
+ if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
+ aOutArrayOfContents.RemoveElementAt(i - 1);
+ HTMLEditUtils::CollectChildren(content, aOutArrayOfContents, i - 1,
+ 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},
+ 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 (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsIContent> content = aOutArrayOfContents[i];
+ if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
+ aOutArrayOfContents.RemoveElementAt(i);
+ HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, i,
+ 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 (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsIContent> content = aOutArrayOfContents[i];
+ if (content->IsHTMLElement(nsGkAtoms::div)) {
+ aOutArrayOfContents.RemoveElementAt(i);
+ HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, i,
+ 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* 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..ff0f223663
--- /dev/null
+++ b/editor/libeditor/AutoRangeArray.h
@@ -0,0 +1,469 @@
+/* -*- 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 "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 wrap lines to handle block level edit actions such as
+ * updating the block parent or indent/outdent around the selection.
+ */
+ void ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction aEditSubAction, 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,
+ const dom::Element& aEditingHost) {
+ if (!aRange.IsPositioned()) {
+ return nullptr;
+ }
+ return CreateRangeWrappingStartAndEndLinesContainingBoundaries(
+ aRange.StartRef(), aRange.EndRef(), aEditSubAction, aEditingHost);
+ }
+ static already_AddRefed<nsRange>
+ CreateRangeWrappingStartAndEndLinesContainingBoundaries(
+ const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint,
+ EditSubAction aEditSubAction, 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, 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 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, 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,
+ 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..fedd23d955
--- /dev/null
+++ b/editor/libeditor/CSSEditUtils.cpp
@@ -0,0 +1,1320 @@
+/* -*- 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;
+ MOZ_ALWAYS_SUCCEEDS(
+ 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(kNameSpaceID_None, nsGkAtoms::id) ||
+ aOtherStyledElement.HasAttr(kNameSpaceID_None, 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(kNameSpaceID_None, 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);
+ DebugOnly<nsresult> rvIgnored =
+ firstCSSDecl->GetPropertyValue(propertyNameString, firstValue);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsICSSDeclaration::GetPropertyValue() failed, but ignored");
+ rvIgnored = otherCSSDecl->GetPropertyValue(propertyNameString, otherValue);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsICSSDeclaration::GetPropertyValue() failed, but ignored");
+ // 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);
+ DebugOnly<nsresult> rvIgnored =
+ otherCSSDecl->GetPropertyValue(propertyNameString, otherValue);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsICSSDeclaration::GetPropertyValue() failed, but ignored");
+ rvIgnored = firstCSSDecl->GetPropertyValue(propertyNameString, firstValue);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsICSSDeclaration::GetPropertyValue() failed, but ignored");
+ // 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..1e57c94267
--- /dev/null
+++ b/editor/libeditor/ChangeAttributeTransaction.cpp
@@ -0,0 +1,143 @@
+/* -*- 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(kNameSpaceID_None, 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..e831621541
--- /dev/null
+++ b/editor/libeditor/ChangeStyleTransaction.cpp
@@ -0,0 +1,342 @@
+/* -*- 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),
+ mUndoValue(),
+ mRedoValue(),
+ 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(kNameSpaceID_None, nsGkAtoms::style);
+
+ nsAutoCString values;
+ nsresult rv = cssDecl->GetPropertyValue(propertyNameString, values);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsICSSDeclaration::GetPropertyPriorityValue() failed");
+ return rv;
+ }
+ 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;
+ }
+
+ rv = cssDecl->GetPropertyValue(propertyNameString, mRedoValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsICSSDeclaration::GetPropertyValue() failed");
+ return rv;
+}
+
+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..27f6fb1a7f
--- /dev/null
+++ b/editor/libeditor/EditAction.h
@@ -0,0 +1,862 @@
+/* -*- 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,
+
+ // 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..0f864a4bb2
--- /dev/null
+++ b/editor/libeditor/EditorBase.cpp
@@ -0,0 +1,7065 @@
+/* -*- 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 EditorDOMPoint EditorBase::FindBetterInsertionPoint(
+ const EditorDOMPoint& aPoint) const;
+template EditorRawDOMPoint EditorBase::FindBetterInsertionPoint(
+ const EditorRawDOMPoint& aPoint) 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),
+ mWrapColumn(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");
+
+ MOZ_ASSERT(IsInitialized());
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SelectionRef().AddSelectionListener(this);
+
+ // Make sure that the editor will be destroyed properly
+ mDidPreDestroy = false;
+ // Make sure that the editor will be created properly
+ mDidPostCreate = false;
+
+ 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() {
+ if (NS_WARN_IF(!IsInitialized()) || 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 (!IsInitialized() || !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 IsInitialized() && 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;
+ }
+
+ if (!IsInPlaintextMode()) {
+ // 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, the plaintext mode should always be set.
+ // If we're an `HTMLEditor` instance, either is fine.
+ 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::SetShouldTxnSetSelection(bool aShould) {
+ MakeThisAllowTransactionsToChangeSelection(aShould);
+ return NS_OK;
+}
+
+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 = 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) {
+ nsCOMPtr<nsIContent> contentToInsert = do_QueryInterface(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);
+ }
+
+ 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) {
+ 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);
+ }
+
+ rv = DeleteNodeWithTransaction(MOZ_KnownLive(*aNode->AsContent()));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+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(kNameSpaceID_None, &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;
+}
+
+template <typename EditorDOMPointType>
+EditorDOMPointType EditorBase::FindBetterInsertionPoint(
+ const EditorDOMPointType& aPoint) const {
+ if (MOZ_UNLIKELY(NS_WARN_IF(!aPoint.IsInContentNode()))) {
+ return aPoint;
+ }
+
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (aPoint.IsInTextNode()) {
+ // There is no "better" insertion point.
+ return aPoint;
+ }
+
+ if (!IsInPlaintextMode()) {
+ // We cannot find "better" insertion point in HTML editor.
+ // WARNING: When you add some code to find better node in HTML editor,
+ // you need to call this before calling InsertTextWithTransaction()
+ // in HTMLEditor.
+ return aPoint;
+ }
+
+ RefPtr<Element> rootElement = GetRoot();
+ if (aPoint.GetContainer() == rootElement) {
+ // In some cases, aNode is the anonymous DIV, and offset is 0. 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() && aPoint.GetContainer()->HasChildren() &&
+ aPoint.GetContainer()->GetFirstChild()->IsText()) {
+ return EditorDOMPointType(aPoint.GetContainer()->GetFirstChild(), 0u);
+ }
+
+ // In some other cases, aNode is the anonymous DIV, and offset points to
+ // the terminating padding <br> element for empty last line. In that case,
+ // we'll adjust aInOutNode and aInOutOffset to the preceding text node,
+ // if any.
+ if (!aPoint.IsStartOfContainer()) {
+ if (IsHTMLEditor()) {
+ // Fall back to a slow path that uses GetChildAt_Deprecated() for
+ // Thunderbird's plaintext editor.
+ nsIContent* child = aPoint.GetPreviousSiblingOfChild();
+ if (child && child->IsText()) {
+ return EditorDOMPointType::AtEndOf(*child);
+ }
+ } else {
+ // If we're in a real plaintext editor, use a fast path that avoids
+ // calling GetChildAt_Deprecated() which may perform a linear search.
+ nsIContent* child = aPoint.GetContainer()->GetLastChild();
+ while (child) {
+ if (child->IsText()) {
+ return EditorDOMPointType::AtEndOf(*child);
+ }
+ child = child->GetPreviousSibling();
+ }
+ }
+ }
+ }
+
+ // Sometimes, aNode is the padding <br> element itself. 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 == rootElement) {
+ return EditorDOMPointType(parentOfContainer,
+ aPoint.template ContainerAs<nsIContent>(), 0u);
+ }
+ }
+
+ return aPoint;
+}
+
+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 = FindBetterInsertionPoint(aPointToInsert);
+
+ // If a neighboring text node already exists, use that
+ if (!pointToInsert.IsInTextNode()) {
+ nsIContent* child = nullptr;
+ if (!pointToInsert.IsStartOfContainer() &&
+ (child = pointToInsert.GetPreviousSiblingOfChild()) &&
+ child->IsText()) {
+ pointToInsert.Set(child, child->Length());
+ } else if (!pointToInsert.IsEndOfContainer() &&
+ (child = pointToInsert.GetChild()) && child->IsText()) {
+ pointToInsert.Set(child, 0);
+ }
+ }
+
+ 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(IsInPlaintextMode());
+ 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},
+ 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},
+ 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},
+ anonymousDivOrEditingHost)
+ : HTMLEditUtils::GetNextContent(
+ point, {WalkTreeOption::IgnoreNonEditableNode},
+ 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},
+ anonymousDivOrEditingHost)
+ : HTMLEditUtils::GetNextContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ 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 = EditorUtils::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;
+ }
+ }
+ }
+ }
+
+ if (IsInPlaintextMode()) {
+ for (nsIContent* content = droppedAt.ContainerAs<nsIContent>(); content;
+ content = content->GetParent()) {
+ nsCOMPtr<nsIFormControl> formControl(do_QueryInterface(content));
+ if (formControl && !formControl->AllowDrop()) {
+ // Don't allow dropping into a form control that doesn't allow being
+ // dropped into.
+ return NS_OK;
+ }
+ }
+ }
+
+ // 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() || aKeyboardEvent->IsOS()) {
+ 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() ||
+ aKeyboardEvent->IsOS()) {
+ 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()) {
+ // 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 =
+ 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 (nsCOMPtr<nsINode> node = do_QueryInterface(GetDOMEventTarget())) {
+ if (node->OwnerDoc()->GetUnretargetedFocusedContent() != node) {
+ 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* rootElement = GetExposedRoot();
+
+ if (aTextDirection == TextDirection::eLTR) {
+ NS_ASSERTION(!IsLeftToRight(), "Unexpected mutually exclusive flag");
+ mFlags &= ~nsIEditor::eEditorRightToLeft;
+ mFlags |= nsIEditor::eEditorLeftToRight;
+ nsresult rv = rootElement->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 = rootElement->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;
+ }
+ 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::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 EditorBase::SetWrapWidth(int32_t aWrapColumn) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetWrapWidth);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ SetWrapColumn(aWrapColumn);
+
+ // Make sure we're a plaintext editor, otherwise we shouldn't
+ // do the rest of this.
+ if (!IsInPlaintextMode()) {
+ return NS_OK;
+ }
+
+ // Ought to set a style sheet here ...
+ // Probably should keep around an mPlaintextStyleSheet for this purpose.
+ 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(kNameSpaceID_None, 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;
+}
+
+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 {
+ if (!StaticPrefs::dom_input_events_beforeinput_enabled()) {
+ return false;
+ }
+
+ // Don't dispatch "beforeinput" event when the editor user makes us stop
+ // dispatching input event.
+ if (mEditorBase.IsSuppressingDispatchingInputEvent()) {
+ return false;
+ }
+
+ // If mPrincipal has set, it means that we're handling an edit action
+ // which is requested by JS. If it's not chrome script, we shouldn't
+ // dispatch "beforeinput" event.
+ if (mPrincipal && !mPrincipal->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 (!mPrincipal->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,
+ SplitNodeDirection aSplitNodeDirection) {
+ 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 =
+ aSplitNodeDirection == SplitNodeDirection::LeftNodeIsNewOne
+ ? AddRangeToChangedRange(*aEditorBase.AsHTMLEditor(),
+ EditorRawDOMPoint(&aNewContent, 0),
+ EditorRawDOMPoint(&aSplitContent, 0))
+ : 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..40bdaa073c
--- /dev/null
+++ b/editor/libeditor/EditorBase.h
@@ -0,0 +1,2963 @@
+/* -*- 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);
+
+ bool IsInitialized() const { return !!mDocument; }
+ 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;
+ }
+
+ 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);
+
+ /**
+ * Similar to the setter for wrapWidth, but just sets the editor
+ * internal state without actually changing the content being edited
+ * to wrap at that column. This should only be used by callers who
+ * are sure that their content is already set up correctly.
+ */
+ void SetWrapColumn(int32_t aWrapColumn) { mWrapColumn = aWrapColumn; }
+
+ /**
+ * 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 IsInPlaintextMode() const {
+ const bool isPlaintextMode =
+ (mFlags & nsIEditor::eEditorPlaintextMask) != 0;
+ MOZ_ASSERT_IF(IsTextEditor(), isPlaintextMode);
+ return isPlaintextMode;
+ }
+
+ 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 IsWrapHackEnabled() const {
+ return (mFlags & nsIEditor::eEditorEnableWrapHackMask) != 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,
+ SplitNodeDirection aSplitNodeDirection);
+ 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.IsInitialized();
+ }
+
+ /**
+ * 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::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 FindBetterInsertionPoint(). 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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();
+ virtual 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;
+ 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..73fa8ab36d
--- /dev/null
+++ b/editor/libeditor/EditorCommands.h
@@ -0,0 +1,906 @@
+/* -*- 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:
+ 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;
+};
+
+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..bfe07cbb40
--- /dev/null
+++ b/editor/libeditor/EditorDOMPoint.h
@@ -0,0 +1,1586 @@
+/* -*- 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);
+ }
+
+ 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 {
+ 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();
+ }
+
+ 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..ad4d15847b
--- /dev/null
+++ b/editor/libeditor/EditorEventListener.cpp
@@ -0,0 +1,1187 @@
+/* -*- 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/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/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;
+ }
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ eventListenerManager->AddEventListenerByType(
+ this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->AddEventListenerByType(
+ this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble());
+#endif
+ eventListenerManager->AddEventListenerByType(
+ this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->AddEventListenerByType(
+ this, u"dragover"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->AddEventListenerByType(
+ this, u"dragleave"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->AddEventListenerByType(
+ this, u"drop"_ns, TrustedEventsAtSystemGroupBubble());
+ // 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;
+ }
+
+#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble());
+#endif
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"dragover"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"dragleave"_ns, TrustedEventsAtSystemGroupBubble());
+ eventListenerManager->RemoveEventListenerByType(
+ this, u"drop"_ns, TrustedEventsAtSystemGroupBubble());
+ 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();
+ switch (internalEvent->mMessage) {
+ // dragover and drop
+ case eDragOver:
+ case eDrop: {
+ // 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.
+ if (aKeyboardEvent->IsAlt() || aKeyboardEvent->IsOS()) {
+ return false;
+ }
+
+ return true;
+}
+
+// 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->IsInPlaintextMode()) {
+ 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;
+ }
+ if (DetachedFromEditorOrDefaultPrevented(aKeyboardEvent)) {
+ return NS_OK;
+ }
+
+ if (!ShouldHandleNativeKeyBindings(aKeyboardEvent)) {
+ return NS_OK;
+ }
+
+ // Now, ask the native key bindings to handle the event.
+ nsIWidget* widget = aKeyboardEvent->mWidget;
+ // If the event is created by chrome script, the widget is always nullptr.
+ if (!widget) {
+ nsPresContext* presContext = GetPresContext();
+ if (NS_WARN_IF(!presContext)) {
+ return NS_OK;
+ }
+ widget = presContext->GetNearestWidget();
+ 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;
+ }
+
+ aDragEvent->PreventDefault();
+ aDragEvent->StopImmediatePropagation();
+
+ if (aDragEvent->WidgetEventPtr()->mMessage == eDrop) {
+ RefPtr<EditorBase> editorBase = mEditorBase;
+ nsresult rv = editorBase->HandleDropEvent(aDragEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::HandleDropEvent() failed");
+ return rv;
+ }
+
+ MOZ_ASSERT(aDragEvent->WidgetEventPtr()->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->IsInPlaintextMode() &&
+ (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 !EditorUtils::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 =
+ do_QueryInterface(aKeyboardEvent->GetDOMEventTarget());
+ 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..7adc988d02
--- /dev/null
+++ b/editor/libeditor/EditorForwards.h
@@ -0,0 +1,169 @@
+/* -*- 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 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 JoinNodesDirection; // JoinSplitNodeDirection.h
+enum class ParagraphSeparator; // mozilla/HTMLEditor.h
+enum class SpecifiedStyle : uint8_t; // mozilla/PendingStyles.h
+enum class SplitNodeDirection; // JoinSplitNodeDirection.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..b1146a6bc5
--- /dev/null
+++ b/editor/libeditor/EditorUtils.cpp
@@ -0,0 +1,474 @@
+/* -*- 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<StyleWhiteSpace> EditorUtils::GetComputedWhiteSpaceStyle(
+ 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();
+ }
+ return Some(elementStyle->StyleText()->mWhiteSpace);
+}
+
+// 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()->mWhiteSpace == StyleWhiteSpace::PreLine;
+}
+
+bool EditorUtils::IsPointInSelection(const Selection& aSelection,
+ const nsINode& aParentNode,
+ uint32_t aOffset) {
+ if (aSelection.IsCollapsed()) {
+ return false;
+ }
+
+ const uint32_t rangeCount = aSelection.RangeCount();
+ for (const uint32_t i : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(aSelection.RangeCount() == rangeCount);
+ RefPtr<const nsRange> range = aSelection.GetRangeAt(i);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!range))) {
+ // Don't bail yet, iterate through them all
+ continue;
+ }
+
+ IgnoredErrorResult ignoredError;
+ bool nodeIsInSelection =
+ range->IsPointInRange(aParentNode, aOffset, ignoredError) &&
+ !ignoredError.Failed();
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "nsRange::IsPointInRange() failed");
+
+ // Done when we find a range that we are in
+ if (nodeIsInSelection) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// 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..9664adaaaa
--- /dev/null
+++ b/editor/libeditor/EditorUtils.h
@@ -0,0 +1,483 @@
+/* -*- 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 computed white-space style of aContent.
+ */
+ static Maybe<StyleWhiteSpace> GetComputedWhiteSpaceStyle(
+ 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: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;
+ }
+ }
+
+ /**
+ * Returns true if aSelection includes the point in aParentContent.
+ */
+ static bool IsPointInSelection(const Selection& aSelection,
+ const nsINode& aParentNode, uint32_t aOffset);
+
+ /**
+ * 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..4c83c0a22e
--- /dev/null
+++ b/editor/libeditor/HTMLAnonymousNodeEditor.cpp
@@ -0,0 +1,611 @@
+/* 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;
+ nsresult rv = aComputedStyle->GetPropertyValue(aProperty, value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsComputedDOMStyle::GetPropertyValue() failed");
+ return 0;
+ }
+
+ // 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.
+ 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->IsInUncomposedDoc()) {
+ 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(kNameSpaceID_None, 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..b75167221a
--- /dev/null
+++ b/editor/libeditor/HTMLEditHelpers.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 "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() : DOMIterator() {
+ 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(kNameSpaceID_None, 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..499bd82f00
--- /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 "HTMLEditHelpers.h"
+#include "JoinSplitNodeDirection.h"
+
+#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 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)
+ : CaretPoint(),
+ mNextInsertionPoint(aNextInsertionPoint),
+ mHandled(aHandled && aNextInsertionPoint.IsSet()) {
+ AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
+ mNextInsertionPoint);
+ }
+ explicit MoveNodeResult(EditorDOMPoint&& aNextInsertionPoint, bool aHandled)
+ : CaretPoint(),
+ 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 mDirection == SplitNodeDirection::LeftNodeIsNewOne ? mPreviousNode
+ : 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>();
+ }
+ if (mDirection == SplitNodeDirection::LeftNodeIsNewOne) {
+ return mNextNode ? mNextNode : mPreviousNode;
+ }
+ 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() = delete;
+ 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 aDirection The split direction which the HTML editor tried to split
+ * a node with.
+ * @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,
+ SplitNodeDirection aDirection,
+ const Maybe<EditorDOMPoint>& aNewCaretPoint = Nothing())
+ : CaretPoint(aNewCaretPoint.isSome()
+ ? aNewCaretPoint.ref()
+ : EditorDOMPoint::AtEndOf(*PreviousNode(
+ aDirection, &aNewNode, &aSplitNode))),
+ mPreviousNode(PreviousNode(aDirection, &aNewNode, &aSplitNode)),
+ mNextNode(NextNode(aDirection, &aNewNode, &aSplitNode)),
+ mDirection(aDirection) {}
+
+ SplitNodeResult ToHandledResult() const {
+ CaretPointHandled();
+ SplitNodeResult result(mDirection);
+ 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, SplitNodeDirection aDirection,
+ const SplitNodeResult* aDeeperSplitNodeResult = nullptr) {
+ SplitNodeResult result(aDirection);
+ 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, SplitNodeDirection aDirection,
+ const SplitNodeResult* aDeeperSplitNodeResult = nullptr) {
+ SplitNodeResult result(aDirection);
+ 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,
+ SplitNodeDirection aDirection,
+ const SplitNodeResult* aDeeperSplitNodeResult = nullptr) {
+ SplitNodeResult result(aDirection);
+ 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:
+ explicit SplitNodeResult(SplitNodeDirection aDirection)
+ : mDirection(aDirection) {}
+
+ // Helper methods to consider previous/next node from new/old node and split
+ // direction.
+ static nsIContent* PreviousNode(SplitNodeDirection aDirection,
+ nsIContent* aNewOne, nsIContent* aOldOne) {
+ return aDirection == SplitNodeDirection::LeftNodeIsNewOne ? aNewOne
+ : aOldOne;
+ }
+ static nsIContent* NextNode(SplitNodeDirection aDirection,
+ nsIContent* aNewOne, nsIContent* aOldOne) {
+ return aDirection == SplitNodeDirection::LeftNodeIsNewOne ? aOldOne
+ : aNewOne;
+ }
+
+ // 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;
+
+ SplitNodeDirection mDirection;
+};
+
+/*****************************************************************************
+ * 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.
+ * @param aDirection The join direction which the HTML editor tried
+ * to join the nodes with.
+ */
+ JoinNodesResult(const EditorDOMPoint& aJoinedPoint,
+ nsIContent& aRemovedContent, JoinNodesDirection aDirection)
+ : 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)
+ : CaretPoint(),
+ 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)
+ : CaretPoint(),
+ 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:
+ ContentIteratorBase* 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..8e89d26d46
--- /dev/null
+++ b/editor/libeditor/HTMLEditSubActionHandler.cpp
@@ -0,0 +1,11794 @@
+/* -*- 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/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::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 (IsInPlaintextMode()) {
+ // 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()) {
+ 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;
+ }
+ default: {
+ if (Element* editingHost = ComputeEditingHost()) {
+ if (RefPtr<nsRange> extendedChangedRange = AutoRangeArray::
+ CreateRangeWrappingStartAndEndLinesContainingBoundaries(
+ changedRange, GetTopLevelEditSubAction(),
+ *editingHost)) {
+ 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})) {
+ 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, {}, 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);
+ const RefPtr<const Element> parentBlockElementOfBRElement =
+ HTMLEditUtils::GetAncestorElement(*previousBRElement,
+ HTMLEditUtils::ClosestBlockElement);
+
+ 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;
+ }
+
+ 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");
+ ignoredError.SuppressException();
+
+ RefPtr<Element> rootElement = GetRoot();
+ if (!rootElement) {
+ 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(*rootElement, editorType);
+ for (nsIContent* rootChild = rootElement->GetFirstChild(); rootChild;
+ rootChild = rootChild->GetNextSibling()) {
+ if (EditorUtils::IsPaddingBRElementForEmptyEditor(*rootChild) ||
+ !isRootEditable ||
+ EditorUtils::IsEditableContent(*rootChild, editorType) ||
+ HTMLEditUtils::IsBlockElement(*rootChild)) {
+ return NS_OK;
+ }
+ }
+
+ // Skip adding the padding <br> element for empty editor if body
+ // is read-only.
+ if (IsHTMLEditor() && !HTMLEditUtils::IsSimplyEditableNode(*rootElement)) {
+ return NS_OK;
+ }
+
+ // 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(rootElement, 0u));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertBRElementResult.unwrapErr();
+ }
+
+ // Set selection.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ nsresult rv = CollapseSelectionToStartOf(*rootElement);
+ 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 || IsInPlaintextMode()) {
+ 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());
+ // Next inserting text should be inserted into styled inline elements if
+ // they have first visible thing in the new line.
+ auto pointToPutCaret = [&]() -> EditorDOMPoint {
+ WSScanResult forwardScanResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ editingHost, EditorRawDOMPoint::After(
+ *unwrappedInsertBRElementResult.GetNewNode()));
+ if (forwardScanResult.InVisibleOrCollapsibleCharacters()) {
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ return forwardScanResult.Point<EditorDOMPoint>();
+ }
+ if (forwardScanResult.ReachedSpecialContent()) {
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ return forwardScanResult.PointAtContent<EditorDOMPoint>();
+ }
+ return unwrappedInsertBRElementResult.UnwrapCaretPoint();
+ }();
+
+ 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(aEditingHost) ||
+ HTMLEditUtils::ShouldInsertLinefeedCharacter(
+ aCandidatePointToSplit, 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)) {
+ 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);
+
+ // 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);
+ Result<RefPtr<Element>, nsresult> suggestBlockElementToPutCaretOrError =
+ FormatBlockContainerWithTransaction(
+ selectionRanges,
+ MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator)),
+ 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);
+ 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})) {
+ 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());
+
+ bool brElementIsAfterBlock = false, brElementIsBeforeBlock = false;
+
+ // First, insert a <br> element.
+ RefPtr<Element> brElement;
+ if (IsInPlaintextMode()) {
+ 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);
+ WSRunScanner wsRunScanner(&aEditingHost, pointToBreak);
+ WSScanResult backwardScanResult =
+ wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToBreak);
+ if (MOZ_UNLIKELY(backwardScanResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary();
+ WSScanResult forwardScanResult =
+ wsRunScanner.ScanNextVisibleNodeOrBlockBoundaryFrom(pointToBreak);
+ if (MOZ_UNLIKELY(forwardScanResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary();
+ // 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);
+ }
+
+ 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(brElement,
+ InterlinePosition::StartOfNextLine);
+ return CreateElementResult(std::move(brElement),
+ std::move(pointToPutCaret));
+ }
+
+ auto afterBRElement = EditorDOMPoint::After(brElement);
+ WSScanResult forwardScanFromAfterBRElementResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(&aEditingHost,
+ afterBRElement);
+ 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");
+ }
+ }
+
+ // 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)
+ ? 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);
+ 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),
+ "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);
+ 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::IsInlineElement(aMailCiteElement)) {
+ 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);
+ 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));
+ 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)) {
+ // 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)) {
+ // 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);
+ 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);
+ 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);
+ const EditorDOMPointInText followingCharPoint =
+ WSRunScanner::GetInclusiveNextEditableCharPoint(editingHost,
+ aEndToDelete);
+ // 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)) {
+ Element* editingHost = ComputeEditingHost();
+ // Try to put caret next to immediately after previous editable leaf.
+ nsIContent* previousContent =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ newCaretPosition, *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, editingHost);
+ if (previousContent && !HTMLEditUtils::IsBlockElement(*previousContent)) {
+ 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},
+ 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>())) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ WSRunScanner wsRunScanner(ComputeEditingHost(), aPointToInsert);
+ // 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eCreateOrChangeList, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges.SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ aHTMLEditor, 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})) {
+ 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},
+ 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}))) {
+ 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.
+ // XXX Despite the name, HTMLEditUtils::IsInlineElement() returns true for
+ // non-element content nodes too.
+ if (HTMLEditUtils::IsInlineElement(aHandlingContent)) {
+ 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::IgnoreEditableState,
+ 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::IsInlineElement(aHandlingInlineContent));
+
+ // 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::IsInlineElement(aHandlingContent)) {
+ 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
+ .ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eCreateOrChangeList, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedSelectionRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, 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, nsAtom& blockType,
+ 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eCreateOrRemoveBlock, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ aSelectionRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed");
+ return splitResult.propagateErr();
+ }
+ nsresult rv = aSelectionRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eCreateOrRemoveBlock,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eCreateOrRemoveBlock, CollectNonEditableNodes::No) failed");
+ return Err(rv);
+ }
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eCreateOrRemoveBlock);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eCreateOrRemoveBlock) 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)) {
+ if (NS_WARN_IF(aSelectionRanges.Ranges().IsEmpty())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ auto pointToInsertBlock =
+ aSelectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (&blockType == nsGkAtoms::normal || &blockType == 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);
+ if (!editableBlockElement) {
+ NS_WARNING(
+ "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find "
+ "block parent");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (!HTMLEditUtils::IsFormatNode(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},
+ &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},
+ &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(
+ blockType, pointToInsertBlock, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "%s) failed",
+ nsAtomCString(&blockType).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();
+ }
+ // 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 (&blockType == 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 (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) {
+ Result<EditorDOMPoint, nsresult> removeBlockContainerElementsResult =
+ RemoveBlockContainerElementsWithTransaction(arrayOfContents);
+ if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed");
+ return removeBlockContainerElementsResult.propagateErr();
+ }
+ return RefPtr<Element>();
+ }
+ Result<CreateElementResult, nsresult> wrapContentsInBlockElementResult =
+ CreateOrChangeBlockContainerElement(arrayOfContents, blockType,
+ aEditingHost);
+ if (MOZ_UNLIKELY(wrapContentsInBlockElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::CreateOrChangeBlockContainerElement() 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);
+ if (editableBlockElement &&
+ HTMLEditUtils::IsListItem(editableBlockElement)) {
+ arrayOfContents.AppendElement(*editableBlockElement);
+ }
+ }
+
+ EditorDOMPoint pointToPutCaret;
+ if (arrayOfContents.IsEmpty()) {
+ {
+ AutoRangeArray extendedRanges(aRanges);
+ extendedRanges.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eIndent, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, 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)) {
+ 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)) {
+ 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eIndent, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, 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)) {
+ const EditorDOMPoint pointToInsertBlockquoteElement =
+ pointToPutCaret.IsSet()
+ ? std::move(pointToPutCaret)
+ : EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsertBlockquoteElement.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure we can put a block here.
+ 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 (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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eOutdent, 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)) {
+ 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) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ EditorDOMPoint pointToPutCaret;
+ Result<SplitRangeOffFromNodeResult, nsresult> splitResult =
+ SplitRangeOffFromBlock(aBlockContainerElement, aStartOfRange,
+ aEndOfRange);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ if (splitResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::SplitRangeOffFromBlock() failed");
+ return splitResult;
+ }
+ NS_WARNING(
+ "HTMLEditor::SplitRangeOffFromBlock() 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_ASSERT_IF(GetSplitNodeDirection() == SplitNodeDirection::LeftNodeIsNewOne,
+ rightmostElement == &aBlockContainerElement);
+ {
+ // 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::SplitRangeOffFromBlock(Element& aBlockElement,
+ nsIContent& aStartOfMiddleElement,
+ nsIContent& aEndOfMiddleElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // aStartOfMiddleElement and aEndOfMiddleElement must be exclusive
+ // descendants of aBlockElement.
+ MOZ_ASSERT(EditorUtils::IsDescendantOf(aStartOfMiddleElement, aBlockElement));
+ MOZ_ASSERT(EditorUtils::IsDescendantOf(aEndOfMiddleElement, aBlockElement));
+
+ EditorDOMPoint pointToPutCaret;
+ // Split at the start.
+ Result<SplitNodeResult, nsresult> splitAtStartResult =
+ SplitNodeDeepWithTransaction(aBlockElement,
+ 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>()
+ : &aBlockElement;
+ // MOZ_KnownLive(rightElement) because it's grabbed by splitAtStartResult or
+ // aBlockElement 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, &aBlockElement, 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 =
+ SplitRangeOffFromBlock(aBlockElement, aStartOfOutdent, aEndOfOutdent);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitRangeOffFromBlock() failed");
+ return splitResult;
+ }
+
+ SplitRangeOffFromNodeResult unwrappedSplitResult = splitResult.unwrap();
+ Element* middleElement = unwrappedSplitResult.GetMiddleContentAs<Element>();
+ if (MOZ_UNLIKELY(!middleElement)) {
+ NS_WARNING(
+ "HTMLEditor::SplitRangeOffFromBlock() 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eSetOrClearAlignment, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, 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)) {
+ // 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}))) {
+ 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);
+ 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})) {
+ 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},
+ &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);
+ 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})) {
+ 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},
+ &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::eMergeBlockContents:
+ case EditSubAction::eCreateOrChangeList:
+ case EditSubAction::eSetOrClearAlignment:
+ case EditSubAction::eSetPositionToAbsolute:
+ case EditSubAction::eIndent:
+ case EditSubAction::eOutdent: {
+ EditorDOMPoint pointToPutCaret;
+ for (int32_t i = aArrayOfContents.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsIContent>& content = aArrayOfContents[i];
+ if (HTMLEditUtils::IsInlineElement(content) &&
+ 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(i);
+ aArrayOfContents.InsertElementsAt(i, arrayOfInlineContents);
+ }
+ }
+ return pointToPutCaret;
+ }
+ default:
+ return EditorDOMPoint();
+ }
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::SplitParentInlineElementsAtRangeBoundaries(
+ RangeItem& aRangeItem, 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(), &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(), &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})) {
+ 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, {})) {
+ 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,
+ GetSplitNodeDirection());
+ }
+ 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,
+ GetSplitNodeDirection());
+ }
+ 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,
+ GetSplitNodeDirection());
+ }
+
+ // 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}, &aEditingHost));
+ if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) ||
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ // is there a BR after it?
+ brElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetNextContent(
+ pointToSplit, {WalkTreeOption::IgnoreNonEditableNode},
+ &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,
+ GetSplitNodeDirection());
+ }
+ 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)) {
+ 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::IsInlineElement(*maybeDeepestInlineContainer) &&
+ 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});
+ 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, {})) {
+ 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})) {
+ 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)) {
+ // 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));
+ 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) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // 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 (auto& content : aArrayOfContents) {
+ // If curNode is an <address>, <p>, <hn>, or <pre>, remove it.
+ if (HTMLEditUtils::IsFormatNode(content)) {
+ // Process any partial progress saved
+ if (blockElement) {
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent);
+ 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);
+ 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);
+ if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed");
+ return removeBlockContainerElementsResult;
+ }
+ if (removeBlockContainerElementsResult.inspect().IsSet()) {
+ pointToPutCaret = removeBlockContainerElementsResult.unwrap();
+ }
+ continue;
+ }
+
+ if (HTMLEditUtils::IsInlineElement(content)) {
+ 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);
+ 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);
+ if (!blockElement || !HTMLEditUtils::IsFormatNode(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);
+ 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);
+ 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::CreateOrChangeBlockContainerElement(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents, nsAtom& aBlockTag,
+ 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;
+ 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;
+ continue;
+ }
+
+ // Is it already the right kind of block, or an uneditable block?
+ if (content->IsHTMLElement(&aBlockTag) ||
+ (!EditorUtils::IsEditableContent(content, EditorType::HTML) &&
+ HTMLEditUtils::IsBlockElement(content))) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ // Do nothing to this block
+ continue;
+ }
+
+ // If content is a address, p, header, address, or pre, replace it with a
+ // new block of correct type.
+ // XXX: pre can't hold everything the others can
+ if (HTMLEditUtils::IsMozDiv(content) ||
+ HTMLEditUtils::IsFormatNode(content)) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ Result<CreateElementResult, nsresult> replaceWithNewBlockElementResult =
+ ReplaceContainerAndCloneAttributesWithTransaction(
+ MOZ_KnownLive(*content->AsElement()), aBlockTag);
+ 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() != atContent.GetContainer())) {
+ 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;
+ // Recursion time
+ AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
+ HTMLEditUtils::CollectAllChildren(*content, childContents);
+ if (!childContents.IsEmpty()) {
+ Result<CreateElementResult, nsresult> wrapChildrenInBlockElementResult =
+ CreateOrChangeBlockContainerElement(childContents, aBlockTag,
+ aEditingHost);
+ if (MOZ_UNLIKELY(wrapChildrenInBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateOrChangeBlockContainerElement() 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(
+ aBlockTag, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction(%s) failed",
+ nsAtomCString(&aBlockTag).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 the node is a break, we honor it by putting further nodes in a new
+ // parent
+ if (curBlock) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ // 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(
+ aBlockTag, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction(%s) failed",
+ nsAtomCString(&aBlockTag).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::IsInlineElement(content)) {
+ // 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 contructed, 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 (&aBlockTag == 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(
+ aBlockTag, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(nsPrintfCString("HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTr"
+ "ansaction(%s) failed",
+ nsAtomCString(&aBlockTag).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);
+ }
+
+ // 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(
+ 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 (aStartOfDeepestRightNode.GetContainer() != &aEditingHost &&
+ !EditorUtils::IsDescendantOf(*aStartOfDeepestRightNode.GetContainer(),
+ aEditingHost)) {
+ NS_WARNING("aStartOfDeepestRightNode was not in editing host");
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ // Look for a node that can legally contain the tag.
+ EditorDOMPoint pointToInsert(aStartOfDeepestRightNode);
+ for (; pointToInsert.IsSet();
+ pointToInsert.Set(pointToInsert.GetContainer())) {
+ // We cannot split active editing host and its ancestor. So, there is
+ // no element to contain the specified element.
+ if (pointToInsert.GetChild() == &aEditingHost) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() reached "
+ "editing host");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(), aTag)) {
+ // Found an ancestor node which can contain the element.
+ break;
+ }
+ }
+
+ // If we got lost the editing host, we can do nothing.
+ if (NS_WARN_IF(!pointToInsert.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // 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,
+ GetSplitNodeDirection());
+ }
+
+ 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(
+ 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},
+ &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)) {
+ 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 = IsInPlaintextMode();
+ 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) ||
+ (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(kNameSpaceID_None, nsGkAtoms::face, value);
+ if (!value.IsEmpty()) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::face, value));
+ value.Truncate();
+ }
+ }
+ if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::size)) {
+ inclusiveAncestor->GetAttr(kNameSpaceID_None, nsGkAtoms::size, value);
+ if (!value.IsEmpty()) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::size, value));
+ value.Truncate();
+ }
+ }
+ if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::color)) {
+ inclusiveAncestor->GetAttr(kNameSpaceID_None, 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});
+ },
+ 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},
+ 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)) {
+ 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)) {
+ 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)) {
+ 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}, 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);
+ const Element* const blockElementContainingPreviousEditableContent =
+ HTMLEditUtils::GetAncestorElement(*previousEditableContent,
+ HTMLEditUtils::ClosestBlockElement);
+ // 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},
+ 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},
+ 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},
+ 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};
+ if (!HTMLEditUtils::IsBlockElement(*content)) {
+ 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::IsFormatNode(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;
+ }
+ 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})) {
+ // 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.
+ bool isFirstListItem = HTMLEditUtils::IsFirstChild(
+ aListItemElement, {WalkTreeOption::IgnoreNonEditableNode});
+ 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)) {
+ return NS_OK;
+ }
+
+ if (!HTMLEditUtils::IsEmptyNode(
+ aElement, {EmptyCheckOption::TreatSingleBRElementAsVisible})) {
+ 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) &&
+ !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) ||
+ firstEditableChild->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousSibling(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!previousEditableContent) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(*previousEditableContent) ||
+ 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) ||
+ firstEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ nsIContent* nextEditableContent = HTMLEditUtils::GetPreviousSibling(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!nextEditableContent) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(*nextEditableContent) ||
+ 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) ||
+ 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eSetPositionToAbsolute, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedSelectionRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, 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)) {
+ 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..74ec95fd4a
--- /dev/null
+++ b/editor/libeditor/HTMLEditUtils.cpp
@@ -0,0 +1,2514 @@
+/* -*- 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/RangeUtils.h" // for RangeUtils
+#include "mozilla/dom/Element.h" // for Element, nsINode
+#include "mozilla/dom/HTMLAnchorElement.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 "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 "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,
+ const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetPreviousContent(
+ const EditorRawDOMPoint& aPoint, const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetPreviousContent(
+ const EditorDOMPointInText& aPoint, const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetPreviousContent(
+ const EditorRawDOMPointInText& aPoint, const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorDOMPoint& aPoint, const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorRawDOMPoint& aPoint, const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorDOMPointInText& aPoint, const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter);
+template nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorRawDOMPointInText& aPoint, const WalkTreeOptions& aOptions,
+ 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);
+}
+
+bool HTMLEditUtils::IsBlockElement(const nsIContent& aContent) {
+ if (!aContent.IsElement()) {
+ return false;
+ }
+ if (aContent.IsHTMLElement(nsGkAtoms::br)) { // shortcut for TextEditor
+ MOZ_ASSERT(!nsHTMLElement::IsBlock(nsHTMLTags::AtomTagToId(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::AtomTagToId(aContent.NodeInfo()->NameAtom()));
+}
+
+bool HTMLEditUtils::IsVisibleElementEvenIfLeafNode(const nsIContent& aContent) {
+ if (!aContent.IsElement()) {
+ return false;
+ }
+ // Assume non-HTML element is visible.
+ if (!aContent.IsHTMLElement()) {
+ return true;
+ }
+ if (HTMLEditUtils::IsBlockElement(aContent)) {
+ 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");
+}
+
+/**
+ * IsFormatNode() returns true if aNode is a format node.
+ */
+bool HTMLEditUtils::IsFormatNode(const nsINode* aNode) {
+ MOZ_ASSERT(aNode);
+ return aNode->IsAnyOfHTMLElements(
+ nsGkAtoms::p, nsGkAtoms::pre, nsGkAtoms::h1, nsGkAtoms::h2, nsGkAtoms::h3,
+ nsGkAtoms::h4, nsGkAtoms::h5, nsGkAtoms::h6, nsGkAtoms::address);
+}
+
+/**
+ * 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(kNameSpaceID_None, 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);
+ 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},
+ maybeNonEditableAncestorBlock)
+ : HTMLEditUtils::GetPreviousContent(
+ aContent,
+ {WalkTreeOption::IgnoreDataNodeExceptText,
+ WalkTreeOption::StopAtBlockBoundary},
+ 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)) {
+ 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;
+ }
+
+ Text* textNode = Text::FromNode(nextContent);
+ if (NS_WARN_IF(!textNode)) {
+ continue; // XXX Is this possible?
+ }
+ if (!textNode->TextLength()) {
+ continue; // empty invisible text node, keep scanning next one.
+ }
+ 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,
+ aBlockElement.GetParentElement());
+ content;
+ content =
+ aScanLineBreak == ScanLineBreak::AtEndOfBlock
+ ? HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *content, aBlockElement, leafNodeOrNonEditableNode)
+ : HTMLEditUtils::GetPreviousContent(
+ *content, onlyPrecedingLine,
+ 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)) {
+ 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);
+ for (nsIContent* content =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *lastLineBreakContent, *blockElement,
+ leafNodeOrNonEditableNodeOrChildBlock);
+ content;
+ content = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *content, *blockElement, leafNodeOrNonEditableNodeOrChildBlock)) {
+ if (HTMLEditUtils::IsBlockElement(*content) ||
+ (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);
+ }
+
+ // XXX Why do we treat non-content node is not empty?
+ // XXX Why do we treat non-text data node may be not empty?
+ if (!aNode.IsContent() ||
+ // 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) ||
+ // If the caller treats a list item element as visible, respect it.
+ (aOptions.contains(EmptyCheckOption::TreatListItemAsVisible) &&
+ IsListItem(&aNode)) ||
+ // If the caller treats a table cell element as visible, respect it.
+ (aOptions.contains(EmptyCheckOption::TreatTableCellAsVisible) &&
+ IsTableCell(&aNode))) {
+ return false;
+ }
+
+ const bool isListItem = IsListItem(&aNode);
+ const bool isTableCell = IsTableCell(&aNode);
+
+ 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::IgnoreEditableState) &&
+ !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;
+ }
+
+ // An editable, non-text node. We need to check its content.
+ // Is it the node we are iterating over?
+ if (childContent == &aNode) {
+ break;
+ }
+
+ 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;
+ }
+
+ // is it an empty node of some sort?
+ // note: list items or table cells are not considered empty
+ // if they contain other lists or tables
+ if (childContent->IsElement()) {
+ if (isListItem || isTableCell) {
+ if (IsAnyListElement(childContent) ||
+ childContent->IsHTMLElement(nsGkAtoms::table)) {
+ // break out if we find we aren't empty
+ return false;
+ }
+ } else if (IsFormWidget(childContent)) {
+ // is it a form widget?
+ // break out if we find we aren't empty
+ return false;
+ }
+ }
+
+ if (!IsEmptyNode(aPresContext, *childContent, aOptions, &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);
+
+ // If and only if the nearest block is the editing host or its parent,
+ // and the outer display value of the editing host is inline, and new
+ // line character is preformatted, we should insert a linefeed.
+ return (!closestEditableBlockElement ||
+ closestEditableBlockElement == &aEditingHost) &&
+ HTMLEditUtils::IsDisplayOutsideInline(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, 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(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,
+ 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>())) {
+ // If we aren't allowed to cross blocks, don't look before this block.
+ return nullptr;
+ }
+ return HTMLEditUtils::GetPreviousContent(*aPoint.GetContainer(), aOptions,
+ aAncestorLimiter);
+ }
+
+ // else look before the child at 'aOffset'
+ if (aPoint.GetChild()) {
+ return HTMLEditUtils::GetPreviousContent(*aPoint.GetChild(), aOptions,
+ 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});
+ 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,
+ aAncestorLimiter);
+}
+
+// static
+template <typename PT, typename CT>
+nsIContent* HTMLEditUtils::GetNextContent(
+ const EditorDOMPointBase<PT, CT>& aPoint, const WalkTreeOptions& aOptions,
+ 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())) {
+ return point.GetChild();
+ }
+
+ nsIContent* firstLeafContent = HTMLEditUtils::GetFirstLeafContent(
+ *point.GetChild(),
+ {aOptions.contains(WalkTreeOption::StopAtBlockBoundary)
+ ? LeafNodeType::LeafNodeOrChildBlock
+ : LeafNodeType::OnlyLeafNode});
+ 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,
+ 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>())) {
+ // don't cross out of parent block
+ return nullptr;
+ }
+
+ return HTMLEditUtils::GetNextContent(*point.GetContainer(), aOptions,
+ aAncestorLimiter);
+}
+
+// static
+nsIContent* HTMLEditUtils::GetAdjacentLeafContent(
+ const nsINode& aNode, WalkTreeDirection aWalkTreeDirection,
+ const WalkTreeOptions& aOptions,
+ 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) {
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*sibling)) {
+ // don't look inside prevsib, 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)
+ : HTMLEditUtils::GetLastLeafContent(*sibling, leafNodeTypes);
+ return leafContent ? leafContent : sibling;
+ }
+
+ nsIContent* parent = node->GetParent();
+ if (!parent) {
+ return nullptr;
+ }
+
+ if (parent == aAncestorLimiter ||
+ (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*parent))) {
+ 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,
+ 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, aAncestorLimiter);
+ if (!leafContent) {
+ return nullptr;
+ }
+
+ if (!HTMLEditUtils::IsContentIgnored(*leafContent, aOptions)) {
+ return leafContent;
+ }
+
+ return HTMLEditUtils::GetAdjacentContent(*leafContent, aWalkTreeDirection,
+ aOptions, 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");
+
+ // 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>()) {
+ previousContent = parentElement->GetPreviousSibling();
+ if (!previousContent &&
+ (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;
+ }
+
+ 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");
+
+ // 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>()) {
+ // 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 &&
+ (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;
+ }
+
+ 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,
+ 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)) ||
+ (lookingForMostDistantInlineElementInBlock &&
+ HTMLEditUtils::IsInlineElement(aContent)) ||
+ (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)) {
+ if (lookingForClosestBlockElement) {
+ return element; // closest block element
+ }
+ MOZ_ASSERT_IF(lastAncestorElement,
+ HTMLEditUtils::IsInlineElement(*lastAncestorElement));
+ 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,
+ 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)) ||
+ (lookingForMostDistantInlineElementInBlock &&
+ HTMLEditUtils::IsInlineElement(aContent)) ||
+ (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) &&
+ !(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()) &&
+ !(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,
+ 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);
+ 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)) {
+ return pointToInsert;
+ }
+
+ WSRunScanner wsScannerForPointToInsert(const_cast<Element*>(&aEditingHost),
+ pointToInsert);
+
+ // 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);
+ 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(kNameSpaceID_None, 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) {
+ size_t numberOfFoundElements = 0;
+ for (Element* element = aNode.GetFirstElementChild(); element;) {
+ if (HTMLEditUtils::IsEmptyInlineContainer(*element, aOptions)) {
+ 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() == '#' ||
+ NS_ColorNameToRGB(colorValue, &color)) {
+ return false;
+ }
+ if (colorValue.LowerCaseEqualsASCII("initial") ||
+ colorValue.LowerCaseEqualsASCII("inherit") ||
+ colorValue.LowerCaseEqualsASCII("unset") ||
+ colorValue.LowerCaseEqualsASCII("revert") ||
+ colorValue.LowerCaseEqualsASCII("currentcolor")) {
+ return true;
+ }
+ return ServoCSSParser::IsValidCSSColor(NS_ConvertUTF16toUTF8(colorValue));
+}
+
+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..e07fa79095
--- /dev/null
+++ b/editor/libeditor/HTMLEditUtils.h
@@ -0,0 +1,2559 @@
+/* -*- 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 "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::picture, 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);
+
+ /**
+ * IsBlockElement() returns true if aContent is an element and it should
+ * be treated as a block. (This does not refer style information.)
+ */
+ static bool IsBlockElement(const nsIContent& aContent);
+ /**
+ * IsInlineElement() returns true if aElement is an element node but
+ * shouldn't be treated as a block or aElement is not an element.
+ * XXX This name is wrong. Must be renamed to IsInlineContent() or something.
+ */
+ static bool IsInlineElement(const nsIContent& aContent) {
+ return !IsBlockElement(aContent);
+ }
+
+ /**
+ * 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);
+
+ /**
+ * 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);
+ static bool IsFormatNode(const nsINode* aNode);
+ 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, 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(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(nsAtom& aParentNodeName, 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(&aChildNodeName);
+ }
+
+ nsHTMLTag parentTagEnum = nsHTMLTags::AtomTagToId(&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;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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 meething visible things.
+ */
+ enum class EmptyCheckOption {
+ TreatSingleBRElementAsVisible,
+ TreatListItemAsVisible,
+ TreatTableCellAsVisible,
+ IgnoreEditableState, // TODO: Change to "TreatNonEditableContentAsVisible"
+ 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) {
+ return HTMLEditUtils::IsInlineElement(aContent) &&
+ 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) {
+ return HTMLEditUtils::IsBlockElement(aElement) &&
+ 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, {EmptyCheckOption::IgnoreEditableState})) {
+ 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) {
+ 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})) {
+ 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 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,
+ const Element* aAncestorLimiter = nullptr) {
+ if (&aNode == aAncestorLimiter ||
+ (aAncestorLimiter &&
+ !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) {
+ return nullptr;
+ }
+ return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Backward,
+ aOptions, aAncestorLimiter);
+ }
+ static nsIContent* GetNextContent(const nsINode& aNode,
+ const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter = nullptr) {
+ if (&aNode == aAncestorLimiter ||
+ (aAncestorLimiter &&
+ !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) {
+ return nullptr;
+ }
+ return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Forward,
+ aOptions, 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,
+ 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,
+ const Element* aAncestorLimiter = nullptr);
+
+ /**
+ * GetPreviousSibling() and GetNextSibling() return the nearest sibling of
+ * aContent which does not match with aOption.
+ */
+ static nsIContent* GetPreviousSibling(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ for (nsIContent* sibling = aContent.GetPreviousSibling(); sibling;
+ sibling = sibling->GetPreviousSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*sibling)) {
+ return nullptr;
+ }
+ return sibling;
+ }
+ return nullptr;
+ }
+
+ static nsIContent* GetNextSibling(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ for (nsIContent* sibling = aContent.GetNextSibling(); sibling;
+ sibling = sibling->GetNextSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*sibling)) {
+ return nullptr;
+ }
+ return sibling;
+ }
+ return nullptr;
+ }
+
+ /**
+ * GetLastChild() and GetFirstChild() return the first or last child of aNode
+ * which does not match with aOption.
+ */
+ static nsIContent* GetLastChild(const nsINode& aNode,
+ const WalkTreeOptions& aOptions) {
+ for (nsIContent* child = aNode.GetLastChild(); child;
+ child = child->GetPreviousSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*child)) {
+ return nullptr;
+ }
+ return child;
+ }
+ return nullptr;
+ }
+
+ static nsIContent* GetFirstChild(const nsINode& aNode,
+ const WalkTreeOptions& aOptions) {
+ for (nsIContent* child = aNode.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) {
+ continue;
+ }
+ if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
+ HTMLEditUtils::IsBlockElement(*child)) {
+ return nullptr;
+ }
+ return child;
+ }
+ return nullptr;
+ }
+
+ static bool IsLastChild(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ nsINode* parentNode = aContent.GetParentNode();
+ if (!parentNode) {
+ return false;
+ }
+ return HTMLEditUtils::GetLastChild(*parentNode, aOptions) == &aContent;
+ }
+
+ static bool IsFirstChild(const nsIContent& aContent,
+ const WalkTreeOptions& aOptions) {
+ nsINode* parentNode = aContent.GetParentNode();
+ if (!parentNode) {
+ return false;
+ }
+ return HTMLEditUtils::GetFirstChild(*parentNode, aOptions) == &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}, &aEditingHost);
+ if (!editableContent) {
+ return nullptr; // Not illegal.
+ }
+ } else {
+ editableContent = HTMLEditUtils::GetNextContent(
+ aPoint, {WalkTreeOption::IgnoreNonEditableNode}, &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},
+ &aEditingHost);
+ if (NS_WARN_IF(!editableContent)) {
+ return nullptr;
+ }
+ } else {
+ editableContent = HTMLEditUtils::GetNextContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ &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;
+ }
+
+ /**
+ * GetLastLeafContent() returns rightmost leaf content in aNode. It depends
+ * on aLeafNodeTypes whether this which types of nodes are treated as leaf
+ * nodes.
+ */
+ 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>;
+ static nsIContent* GetLastLeafContent(
+ const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes,
+ 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},
+ aAncestorLimiter);
+ continue;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
+ HTMLEditUtils::IsBlockElement(*content)) {
+ 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.
+ */
+ static nsIContent* GetFirstLeafContent(
+ const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes,
+ 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},
+ aAncestorLimiter);
+ continue;
+ }
+ if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
+ HTMLEditUtils::IsBlockElement(*content)) {
+ 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,
+ 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);
+ }
+
+ // We have a next content. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*nextContent)) {
+ 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)) {
+ 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,
+ 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, aAncestorLimiter);
+ }
+ if (!HTMLEditUtils::IsContainerNode(
+ *aStartPoint.template ContainerAs<nsIContent>())) {
+ return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, 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, aAncestorLimiter);
+ }
+
+ // We have a next node. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*nextContent)) {
+ 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)) {
+ 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 contet outside aCurrentBlock if
+ * aStartContent equals aCurrentBlock.
+ *
+ * @param aStartContent The start content to scan previous 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* GetPreviousLeafContentOrPreviousBlockElement(
+ const nsIContent& aStartContent, const nsIContent& aCurrentBlock,
+ const LeafNodeTypes& aLeafNodeTypes,
+ 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);
+ }
+
+ // We have a next content. If it's a block, return it.
+ if (HTMLEditUtils::IsBlockElement(*previousContent)) {
+ 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)) {
+ 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,
+ 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, aAncestorLimiter);
+ }
+ if (!HTMLEditUtils::IsContainerNode(
+ *aStartPoint.template ContainerAs<nsIContent>())) {
+ return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
+ aLeafNodeTypes, 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, 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)) {
+ 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)) {
+ 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,
+ const Element* aAncestorLimiter = nullptr);
+ static Element* GetInclusiveAncestorElement(
+ const nsIContent& aContent, const AncestorTypes& aAncestorTypes,
+ 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, const Element* aEditingHost = nullptr,
+ const nsIContent* aAncestorLimiter = nullptr) {
+ if (HTMLEditUtils::IsBlockElement(aContent)) {
+ 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::IsInlineElement(*element)) {
+ 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, 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::IsInlineElement(*element) ||
+ !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,
+ 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) != 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},
+ &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.
+ * @return Number of found elements.
+ */
+ static size_t CollectEmptyInlineContainerDescendants(
+ const nsINode& aNode,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
+ const EmptyCheckOptions& aOptions);
+
+ /**
+ * 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) {
+ 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)) {
+ break;
+ }
+ ++count;
+ }
+ return count;
+ }
+
+ /**
+ * Helper for GetPreviousContent() and GetNextContent().
+ */
+ static nsIContent* GetAdjacentLeafContent(
+ const nsINode& aNode, WalkTreeDirection aWalkTreeDirection,
+ const WalkTreeOptions& aOptions,
+ const Element* aAncestorLimiter = nullptr);
+ static nsIContent* GetAdjacentContent(
+ const nsINode& aNode, WalkTreeDirection aWalkTreeDirection,
+ const WalkTreeOptions& aOptions,
+ 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..474ce8b841
--- /dev/null
+++ b/editor/libeditor/HTMLEditor.cpp
@@ -0,0 +1,7199 @@
+/* -*- 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 "JoinSplitNodeDirection.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/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/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));
+ };
+
+static bool ShouldUseTraditionalJoinSplitDirection(const Document& aDocument) {
+ if (nsIPrincipal* principal = aDocument.GetPrincipalForPrefBasedHacks()) {
+ if (principal->IsURIInPrefList("editor.join_split_direction."
+ "force_use_traditional_direction")) {
+ return true;
+ }
+ if (principal->IsURIInPrefList("editor.join_split_direction."
+ "force_use_compatible_direction")) {
+ return false;
+ }
+ }
+ return !StaticPrefs::
+ editor_join_split_direction_compatible_with_the_other_browsers();
+}
+
+HTMLEditor::HTMLEditor(const Document& aDocument)
+ : EditorBase(EditorBase::EditorType::HTML),
+ mCRInParagraphCreatesParagraph(false),
+ mUseGeckoTraditionalJoinSplitBehavior(
+ ShouldUseTraditionalJoinSplitDirection(aDocument)),
+ 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() {
+ // Collect the data of `beforeinput` event only when it's enabled because
+ // web apps should switch their behavior with feature detection with
+ // checking `onbeforeinput` or `getTargetRanges`.
+ if (StaticPrefs::dom_input_events_beforeinput_enabled()) {
+ 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 (!IsInPlaintextMode() && !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(kNameSpaceID_None, nsGkAtoms::httpEquiv, currentValue);
+
+ if (!FindInReadable(u"content-type"_ns, currentValue,
+ nsCaseInsensitiveStringComparator)) {
+ continue;
+ }
+
+ metaElement->GetAttr(kNameSpaceID_None, 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() {
+ if (NS_WARN_IF(!IsInitialized()) || 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::RemoveEventListeners() {
+ if (!IsInitialized()) {
+ return;
+ }
+
+ EditorBase::RemoveEventListeners();
+}
+
+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},
+ 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)) {
+ 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 (leafContent->IsElement() &&
+ HTMLEditUtils::IsInlineElement(*leafContent) &&
+ !HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent) &&
+ HTMLEditUtils::CanNodeContain(*leafContent, *nsGkAtoms::textTagName)) {
+ // Chromium collaps 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(
+ *leafContent, *editingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock},
+ 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));
+ 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},
+ 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) &&
+ !HTMLEditUtils::IsEmptyNode(
+ *leafContent, {EmptyCheckOption::TreatSingleBRElementAsVisible}) &&
+ !HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) {
+ leafContent = HTMLEditUtils::GetFirstLeafContent(
+ *leafContent,
+ {LeafNodeType::LeafNodeOrNonEditableNode,
+ LeafNodeType::LeafNodeOrChildBlock},
+ 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},
+ 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 Returing 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 (IsInPlaintextMode()) {
+ if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() ||
+ aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() ||
+ aKeyboardEvent->IsOS()) {
+ 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() || aKeyboardEvent->IsOS()) {
+ 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);
+ 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();
+ nsAutoString str(aKeyboardEvent->mCharCode);
+ nsresult rv = OnInputText(str);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed");
+ return rv;
+}
+
+NS_IMETHODIMP HTMLEditor::NodeIsBlock(nsINode* aNode, bool* aIsBlock) {
+ *aIsBlock = aNode && aNode->IsContent() &&
+ HTMLEditUtils::IsBlockElement(*aNode->AsContent());
+ 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)) {
+ // 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::SetParagraphFormatAsAction(
+ 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 (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(*tagName);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::FormatBlockContainerAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+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, 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);
+ if (NS_WARN_IF(!closestBlockElement)) {
+ return NS_OK;
+ }
+
+ for (RefPtr<Element> blockElement = closestBlockElement; blockElement;) {
+ RefPtr<Element> nextBlockElement = HTMLEditUtils::GetAncestorElement(
+ *blockElement, HTMLEditUtils::ClosestBlockElement);
+ 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))) {
+ 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)) {
+ // 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(kNameSpaceID_None, 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(kNameSpaceID_None, 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(nsAtom& aTagName) {
+ 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,
+ *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, 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(kNameSpaceID_None, 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) ||
+ !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);
+ if (!editableBlockElement ||
+ HTMLEditUtils::IsEmptyNode(
+ *editableBlockElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible})) {
+ return NS_OK;
+ }
+ }
+
+ OwningNonNull<nsIContent> content = aContent;
+ for (nsIContent* parentContent : aContent.AncestorsOfType<nsIContent>()) {
+ if (HTMLEditUtils::IsBlockElement(*parentContent) ||
+ 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) {
+ 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);
+ }
+
+ 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);
+ }
+
+ const RefPtr<Element> newContainer = CreateHTMLContent(&aTagName);
+ if (NS_WARN_IF(!newContainer)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Set or clone attribute if needed.
+ 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 = *aOldContainer.GetParentNode();
+ const nsCOMPtr<nsINode> referenceNode = aOldContainer.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 referenc 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 aOldContainer from the DOM tree to make it not referred by
+ // InsertNodeTransaction.
+ nsresult rv = DeleteNodeWithTransaction(aOldContainer);
+ 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) &&
+ !previousSibling->IsHTMLElement(nsGkAtoms::br) &&
+ !HTMLEditUtils::IsBlockElement(*child)) {
+ // 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)) {
+ if (nsIContent* lastChild = HTMLEditUtils::GetLastChild(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (!HTMLEditUtils::IsBlockElement(*lastChild) &&
+ !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) &&
+ !previousSibling->IsHTMLElement(nsGkAtoms::br)) {
+ if (nsIContent* nextSibling = HTMLEditUtils::GetNextSibling(
+ aElement, {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (!HTMLEditUtils::IsBlockElement(*nextSibling) &&
+ !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");
+
+ mMaybeHasJoinSplitTransactions = true;
+ 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, transaction->GetSplitNodeDirection());
+ if (NS_WARN_IF(!newContent->IsInComposedDoc()) ||
+ NS_WARN_IF(!splitContent->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ return SplitNodeResult(*newContent, *splitContent,
+ transaction->GetSplitNodeDirection());
+}
+
+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, GetSplitNodeDirection());
+ 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, GetSplitNodeDirection(), &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, GetSplitNodeDirection(), &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, GetSplitNodeDirection(), &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,
+ SplitNodeDirection aDirection) {
+ // Ensure computing the offset if it's intialized 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,
+ aDirection == SplitNodeDirection::LeftNodeIsNewOne
+ ? aStartOfRightNode.GetContainer()
+ : 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 (!(aDirection == SplitNodeDirection::LeftNodeIsNewOne &&
+ aStartOfRightNode.IsStartOfContainer()) &&
+ !(aDirection == SplitNodeDirection::RightNodeIsNewOne &&
+ aStartOfRightNode.IsEndOfContainer())) {
+ Text* originalTextNode = aStartOfRightNode.ContainerAs<Text>();
+ Text* newTextNode = aNewNode.AsText();
+ nsAutoString movingText;
+ const uint32_t cutStartOffset =
+ aDirection == SplitNodeDirection::LeftNodeIsNewOne
+ ? 0u
+ : aStartOfRightNode.Offset();
+ const uint32_t cutLength =
+ aDirection == SplitNodeDirection::LeftNodeIsNewOne
+ ? aStartOfRightNode.Offset()
+ : 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 eventhing in such case.
+ else if (firstChildOfRightNode &&
+ aStartOfRightNode.GetContainer() !=
+ firstChildOfRightNode->GetParentNode()) {
+ NS_WARNING(
+ "The web app interupped us and touched the DOM tree, we stopped "
+ "splitting anything");
+ } else if (aDirection == SplitNodeDirection::LeftNodeIsNewOne) {
+ // If Splitting at end of container which is not a text node, we need to
+ // move all children if the left node is new one. Otherwise, nothing to do.
+ if (!firstChildOfRightNode) {
+ // XXX Why do we ignore an error while moving nodes from the right
+ // node to the left node?
+ IgnoredErrorResult error;
+ MoveAllChildren(*aStartOfRightNode.GetContainer(),
+ EditorRawDOMPoint(&aNewNode, 0u), error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "HTMLEditor::MoveAllChildren() failed, but ignored");
+ }
+ // If the left node is new one and splitting middle of it, we need to
+ // previous siblings of the given point to the new left node.
+ else if (firstChildOfRightNode->GetPreviousSibling()) {
+ // XXX Why do we ignore an error while moving nodes from the right node
+ // to the left node?
+ IgnoredErrorResult error;
+ MovePreviousSiblings(*firstChildOfRightNode,
+ EditorRawDOMPoint(&aNewNode, 0u), error);
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::MovePreviousSiblings() failed, but ignored");
+ }
+ } else {
+ MOZ_ASSERT(aDirection == SplitNodeDirection::RightNodeIsNewOne);
+ // 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?
+ IgnoredErrorResult error;
+ MoveAllChildren(*aStartOfRightNode.GetContainer(),
+ EditorRawDOMPoint(&aNewNode, 0u), error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "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?
+ IgnoredErrorResult error;
+ MoveInclusiveNextSiblings(*firstChildOfRightNode,
+ EditorRawDOMPoint(&aNewNode, 0u), error);
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "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 (aDirection == SplitNodeDirection::LeftNodeIsNewOne) {
+ // If the container is the right node and offset is before the split
+ // point, the content was moved into aNewNode. So, just changing the
+ // container will point proper position.
+ if (aOffset < aStartOfRightNode.Offset()) {
+ aContainer = &aNewNode;
+ return;
+ }
+
+ // If the container is the right node and offset equals or is larger
+ // than the split point, we need to decrease the offset since some
+ // content before the split point was moved to aNewNode.
+ if (aOffset >= aStartOfRightNode.Offset()) {
+ aOffset -= aStartOfRightNode.Offset();
+ return;
+ }
+
+ NS_WARNING("The stored offset was smaller than the right node offset");
+ aOffset = 0u;
+ 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()) ||
+ (aDirection == SplitNodeDirection::LeftNodeIsNewOne &&
+ NS_WARN_IF(aNewNode.GetNextSibling() !=
+ aStartOfRightNode.GetContainer())) ||
+ (aDirection == SplitNodeDirection::RightNodeIsNewOne &&
+ 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, aDirection);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "RangeUpdater::SelAdjSplitNode() failed, but ignored");
+
+ return SplitNodeResult(aNewNode, *aStartOfRightNode.ContainerAs<nsIContent>(),
+ aDirection);
+}
+
+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);
+ }
+
+ mMaybeHasJoinSplitTransactions = true;
+ 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(),
+ transaction->GetJoinNodesDirection());
+}
+
+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(),
+ aTransaction.GetJoinNodesDirection());
+ }
+ }
+
+ if (!mActionListeners.IsEmpty()) {
+ for (auto& listener : mActionListeners.Clone()) {
+ DebugOnly<nsresult> rvIgnored = listener->DidJoinContents(
+ aTransaction.CreateJoinedPoint<EditorRawDOMPoint>(),
+ aTransaction.GetRemovedContent(),
+ aTransaction.GetJoinNodesDirection() ==
+ JoinNodesDirection::LeftNodeIntoRightNode);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "nsIEditActionListener::DidJoinContents() failed, but ignored");
+ }
+ }
+}
+
+nsresult HTMLEditor::DoJoinNodes(nsIContent& aContentToKeep,
+ nsIContent& aContentToRemove,
+ JoinNodesDirection aDirection) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ const uint32_t keepingContentLength = aContentToKeep.Length();
+ const uint32_t removingContentLength = aContentToRemove.Length();
+ const EditorDOMPoint oldPointAtRightContent(
+ aDirection == JoinNodesDirection::LeftNodeIntoRightNode
+ ? &aContentToKeep
+ : &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 (aDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
+ // If range boundary points aContentToKeep and aContentToRemove
+ // is its left node, remember it as being at end of the removing
+ // node. Then, only chaning the container to aContentToKeep will
+ // point start of the current first content of aContentToKeep.
+ if (aContainer == atRemovingNode.GetContainer() &&
+ atRemovingNode.Offset() < aOffset &&
+ aOffset <= atNodeToKeep.Offset()) {
+ aContainer = &aContentToRemove;
+ aOffset = removingContentLength;
+ }
+ return;
+ }
+ // 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;
+ const nsIContent& rightTextNode =
+ aDirection == JoinNodesDirection::LeftNodeIntoRightNode
+ ? aContentToKeep
+ : aContentToRemove;
+ const nsIContent& leftTextNode =
+ aDirection == JoinNodesDirection::LeftNodeIntoRightNode
+ ? aContentToRemove
+ : aContentToKeep;
+ rightTextNode.AsText()->GetData(rightText);
+ leftTextNode.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);
+
+ if (aDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
+ for (const OwningNonNull<nsIContent>& child :
+ Reversed(arrayOfChildContents)) {
+ // Note that it's safe to pass the reference node to insert the child
+ // without making it grabbed by nsINode::mNextSibling before touching
+ // the DOM tree.
+ IgnoredErrorResult error;
+ aContentToKeep.InsertBefore(child, aContentToKeep.GetFirstChild(),
+ error);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (error.Failed()) {
+ NS_WARNING("nsINode::InsertBefore() failed");
+ return error.StealNSResult();
+ }
+ }
+ } else {
+ 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,
+ aDirection == JoinNodesDirection::LeftNodeIntoRightNode
+ ? std::min(removingContentLength, aContentToKeep.Length())
+ : std::min(keepingContentLength, aContentToKeep.Length())),
+ aContentToRemove, oldPointAtRightContent, aDirection);
+ 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) {
+ if (aDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
+ // Now, all content of aContentToRemove are moved to start of
+ // aContentToKeep. Therefore, if a range boundary was in
+ // aContentToRemove, we just need to change the container to
+ // aContentToKeep.
+ if (aContainer == &aContentToRemove) {
+ aContainer = &aContentToKeep;
+ return;
+ }
+ // And also if the range boundary was in aContentToKeep, we need to
+ // adjust the offset because the content in aContentToRemove was
+ // instarted before ex-start content of aContentToKeep.
+ if (aContainer == &aContentToKeep) {
+ aOffset += removingContentLength;
+ }
+ return;
+ }
+ // 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 = CollapseSelectionTo(
+ aDirection == JoinNodesDirection::LeftNodeIntoRightNode
+ ? EditorRawDOMPoint(&aContentToKeep, removingContentLength)
+ : EditorRawDOMPoint(&aContentToKeep, 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),
+ "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(kNameSpaceID_None, 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(kNameSpaceID_None, 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(kNameSpaceID_None, 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 (IsInPlaintextMode()) {
+ 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));
+ 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));
+ 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);
+ 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);
+ 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);
+ 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},
+ &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;
+}
+
+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.
+ MOZ_ASSERT(IsInitialized(), "The HTMLEditor has not been initialized yet");
+ return GetDocument();
+}
+
+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 {
+ MOZ_ASSERT(IsInitialized(), "The HTMLEditor hasn't been initialized yet");
+ Document* document = GetDocument();
+ 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;
+ }
+ }
+
+ // 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..fabdf1d9e4
--- /dev/null
+++ b/editor/libeditor/HTMLEditor.h
@@ -0,0 +1,4696 @@
+/* -*- 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 "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) 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);
+
+ MOZ_CAN_RUN_SCRIPT nsresult SetParagraphFormatAsAction(
+ 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;
+ }
+
+ /**
+ * Enable/disable Gecko's traditional join/split node direction, that is,
+ * creating left node at splitting a node and removing left node at joining 2
+ * nodes. This is acceptable only before first join/split transaction is
+ * created.
+ */
+ bool EnableCompatibleJoinSplitNodeDirection(bool aEnable) {
+ if (!CanChangeJoinSplitNodeDirection()) {
+ return false;
+ }
+ mUseGeckoTraditionalJoinSplitBehavior = !aEnable;
+ return true;
+ }
+
+ /**
+ * Return true if the instance works with the legacy join/split node
+ * direction.
+ */
+ [[nodiscard]] bool IsCompatibleJoinSplitNodeDirectionEnabled() const {
+ return !mUseGeckoTraditionalJoinSplitBehavior;
+ }
+
+ /**
+ * Return true if web apps can still change the join split node direction.
+ * For saving the footprint, each transaction does not store join/split node
+ * direction at first run. Therefore, join/split node transactions need to
+ * refer the direction of corresponding HTMLEditor. So if the direction were
+ * changed after creating join/split transactions, they would break the DOM
+ * tree with undoing/redoing within wrong direction. Therefore, once this
+ * instance created a join or split node transaction, this returns false to
+ * block to change the direction.
+ */
+ [[nodiscard]] bool CanChangeJoinSplitNodeDirection() const {
+ return !mMaybeHasJoinSplitTransactions;
+ }
+
+ /**
+ * 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();
+
+ 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.
+ * @param aDirection Whether aNewNode will have previous or following
+ * content of aStartOfRightNode.
+ */
+ MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult> DoSplitNode(
+ const EditorDOMPoint& aStartOfRightNode, nsIContent& aNewNode,
+ SplitNodeDirection aDirection);
+
+ /**
+ * 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.
+ * @param aDirection Whether aContentToKeep is right node or left node,
+ * and whether aContentToRemove is left node or right
+ * node.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
+ DoJoinNodes(nsIContent& aContentToKeep, nsIContent& aContentToRemove,
+ JoinNodesDirection aDirection);
+
+ /**
+ * 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 parent inline nodes 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 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>
+ SplitParentInlineElementsAtRangeBoundaries(
+ RangeItem& aRangeItem, 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, 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(
+ 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(
+ nsAtom& aTagName, const EditorDOMPoint& aPointToInsert,
+ BRElementNextToSplitPoint aBRElementNextToSplitPoint,
+ const Element& aEditingHost,
+ const InitializeInsertingElement& aInitializer = DoNothingForNewElement);
+
+ /**
+ * SplitRangeOffFromBlock() splits aBlockElement 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 aBlockElement A block 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>
+ SplitRangeOffFromBlock(Element& aBlockElement,
+ 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.
+ * @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);
+
+ /**
+ * 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);
+
+ /**
+ * 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.
+ *
+ * @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);
+
+ /**
+ * CreateOrChangeBlockContainerElement() formats all nodes in aArrayOfContents
+ * with block elements whose name is aBlockTag.
+ * 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 aBlockTag The element name of new block elements.
+ * @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>
+ CreateOrChangeBlockContainerElement(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents, nsAtom& aBlockTag,
+ 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 aBlockType 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 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,
+ nsAtom& aBlockType,
+ 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;
+
+ /**
+ * 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 };
+ 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.
+ * @param aError The result. If this succeeds to move children,
+ * returns NS_OK. Otherwise, an error.
+ */
+ void MoveAllChildren(nsINode& aContainer,
+ const EditorRawDOMPoint& aPointToInsert,
+ ErrorResult& aError);
+
+ /**
+ * 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.
+ * @param aError The result. If this succeeds to move children,
+ * returns NS_OK. Otherwise, an error.
+ */
+ void MoveChildrenBetween(nsIContent& aFirstChild, nsIContent& aLastChild,
+ const EditorRawDOMPoint& aPointToInsert,
+ ErrorResult& aError);
+
+ /**
+ * 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.
+ * @param aError The result. If this succeeds to move children,
+ * returns NS_OK. Otherwise, an error.
+ */
+ void MovePreviousSiblings(nsIContent& aChild,
+ const EditorRawDOMPoint& aPointToInsert,
+ ErrorResult& aError);
+
+ /**
+ * 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.
+ * @param aError The result. If this succeeds to move children,
+ * returns NS_OK. Otherwise, an error.
+ */
+ void MoveInclusiveNextSiblings(nsIContent& aChild,
+ const EditorRawDOMPoint& aPointToInsert,
+ ErrorResult& aError);
+
+ /**
+ * 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.
+ /**
+ * Get split/join node(s) direction for **this** instance.
+ */
+ [[nodiscard]] inline SplitNodeDirection GetSplitNodeDirection() const;
+ [[nodiscard]] inline JoinNodesDirection GetJoinNodesDirection() const;
+
+ 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 IsInPlaintextMode() 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;
+ void RemoveEventListeners() 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.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult FormatBlockContainerAsSubAction(nsAtom& aTagName);
+
+ /**
+ * 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;
+
+ // Whether use Blink/WebKit compatible joining nodes and split a node
+ // direction or Gecko's traditional direction.
+ bool mUseGeckoTraditionalJoinSplitBehavior;
+
+ // 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;
+
+ // Set to true once the instance creates a JoinNodesTransaction or
+ // SplitNodeTransaction. See also CanChangeJoinSplitNodeDirection().
+ bool mMaybeHasJoinSplitTransactions = false;
+
+ ParagraphSeparator mDefaultParagraphSeparator;
+
+ friend class AlignStateAtSelection; // CollectEditableTargetNodes,
+ // CollectNonEditableNodes
+ friend class AutoRangeArray; // RangeUpdaterRef, SplitNodeWithTransaction,
+ // SplitParentInlineElementsAtRangeBoundaries
+ 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, GetJoinNodesDirection,
+ // 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,
+ // GetSplitNodeDirection
+ 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:
+ ParagraphStateAtSelection() = delete;
+ ParagraphStateAtSelection(HTMLEditor& aHTMLEditor, 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 mFirstParagraphState;
+ }
+
+ /**
+ * If selected nodes are not in same format node nor only in no-format blocks,
+ * this returns true.
+ */
+ bool IsMixed() const { return mIsMixed; }
+
+ private:
+ using EditorType = EditorBase::EditorType;
+
+ /**
+ * 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 aNonFormatBlockElement Must be a non-format block element.
+ */
+ static void AppendDescendantFormatNodesAndFirstInlineNode(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ dom::Element& aNonFormatBlockElement);
+
+ /**
+ * CollectEditableFormatNodesInSelection() collects only editable nodes
+ * around selection ranges (with
+ * `AutoRangeArray::ExtendRangesToWrapLinesToHandleBlockLevelEditAction()` 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.
+ */
+ static nsresult CollectEditableFormatNodesInSelection(
+ HTMLEditor& aHTMLEditor, const dom::Element& aEditingHost,
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents);
+
+ RefPtr<nsAtom> mFirstParagraphState;
+ 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..b8af794c09
--- /dev/null
+++ b/editor/libeditor/HTMLEditorCommands.cpp
@@ -0,0 +1,1302 @@
+/* -*- 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.SetParagraphFormatAsAction(
+ 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::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, 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->SetParagraphFormatAsAction(aNewState, aPrincipal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::SetParagraphFormatAsAction() 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..19cd6c9c90
--- /dev/null
+++ b/editor/libeditor/HTMLEditorController.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 "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(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();
+ 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..70110fd6a6
--- /dev/null
+++ b/editor/libeditor/HTMLEditorDataTransfer.cpp
@@ -0,0 +1,4325 @@
+/* -*- 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);
+ 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},
+ 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);
+ if (wsRunScannerAtCaret
+ .ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToPutCaret)
+ .ReachedInvisibleBRElement()) {
+ WSRunScanner wsRunScannerAtStartReason(
+ editingHost,
+ EditorDOMPoint(wsRunScannerAtCaret.GetStartReasonContent()));
+ 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)
+ : 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())) {
+ 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.IsInPlaintextMode()) {
+ 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 = IsInPlaintextMode();
+ 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
+ rv = clipboard->GetData(transferable, aClipboardType);
+ 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);
+ 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");
+ clipboard->GetData(infoTransferable, aClipboardType);
+ 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;
+ }
+
+ 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);
+ 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 (IsInPlaintextMode()) {
+ 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 (IsInPlaintextMode()) {
+ 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 (IsInPlaintextMode()) {
+ 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();
+ 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);
+ 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 (IsInPlaintextMode()) {
+ 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 (IsInPlaintextMode()) {
+ 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(!IsInPlaintextMode());
+
+ {
+ 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,
+ // Ignore editable state because 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.
+ // TODO: Currently, HTMLEditor does not support deleting such list
+ // element with Backspace. We should fix this.
+ EmptyCheckOption::IgnoreEditableState});
+ }
+ // 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..928d9b3b4c
--- /dev/null
+++ b/editor/libeditor/HTMLEditorDeleteHandler.cpp
@@ -0,0 +1,6854 @@
+/* -*- 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 "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/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;
+
+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);
+ 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());
+ 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());
+ 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},
+ &aOtherBlockElement);
+ mLeftContent = mLeafContentInOtherBlock;
+ mRightContent = aCaretPoint.GetContainerAs<nsIContent>();
+ } else {
+ mLeafContentInOtherBlock = HTMLEditUtils::GetFirstLeafContent(
+ aOtherBlockElement, {LeafNodeType::OnlyEditableLeafNode},
+ &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));
+ 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},
+ editingHost)
+ : HTMLEditUtils::GetNextContent(
+ aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode},
+ 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},
+ editingHost)
+ : HTMLEditUtils::GetNextContent(
+ *targetContent, {WalkTreeOption::StopAtBlockBoundary},
+ editingHost);
+ adjacentContent;
+ adjacentContent =
+ aDirectionAndAmount == nsIEditor::ePrevious
+ ? HTMLEditUtils::GetPreviousContent(
+ *adjacentContent, {WalkTreeOption::StopAtBlockBoundary},
+ editingHost)
+ : HTMLEditUtils::GetNextContent(
+ *adjacentContent, {WalkTreeOption::StopAtBlockBoundary},
+ 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)) {
+ 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.IsInPlaintextMode()) {
+ 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.IsInPlaintextMode()) {
+ {
+ 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);
+ mRightContent = HTMLEditUtils::GetInclusiveAncestorElement(
+ *aRangesToDelete.FirstRangeRef()->GetEndContainer()->AsContent(),
+ HTMLEditUtils::ClosestEditableBlockElement);
+ // 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 adjuscent 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::IsInlineElement(*aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->AsContent()
+ ->GetEditingHost()));
+
+ 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::IsInlineElement(*aRangesToDelete.FirstRangeRef()
+ ->GetStartContainer()
+ ->AsContent()
+ ->GetEditingHost()));
+
+ // 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");
+ }
+ }
+ 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);
+ 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})) {
+ 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);
+ 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()));
+ 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},
+ 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},
+ 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},
+ editingHost)
+ : HTMLEditUtils::GetNextContent(
+ caretPoint, {WalkTreeOption::IgnoreNonEditableNode},
+ editingHost);
+ if (!editableContent) {
+ continue;
+ }
+ while (editableContent && editableContent->IsCharacterData() &&
+ !editableContent->Length()) {
+ editableContent =
+ howToHandleCollapsedRange ==
+ EditorBase::HowToHandleCollapsedRange::ExtendBackward
+ ? HTMLEditUtils::GetPreviousContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ editingHost)
+ : HTMLEditUtils::GetNextContent(
+ *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
+ 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, 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);
+ mRightBlockElement = HTMLEditUtils::GetInclusiveAncestorElement(
+ mInclusiveDescendantOfRightBlockElement,
+ HTMLEditUtils::ClosestEditableBlockElementExceptHRElement);
+
+ 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));
+ // `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);
+ // `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));
+ // `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},
+ 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);
+ 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, 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)) {
+ if (HTMLEditUtils::IsEmptyNode(*blockElement)) {
+ 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());
+
+ if (NS_WARN_IF(mPointToInsert.IsInNativeAnonymousSubtree())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ mSrcInclusiveAncestorBlock =
+ aPointInHardLine.IsInContentNode()
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *aPointInHardLine.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement)
+ : nullptr;
+ mDestInclusiveAncestorBlock =
+ mPointToInsert.IsInContentNode()
+ ? HTMLEditUtils::GetInclusiveAncestorElement(
+ *mPointToInsert.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement)
+ : 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eMergeBlockContents, aEditingHost);
+ MOZ_ASSERT(rangesToWrapTheLine.Ranges().Length() <= 1u);
+ mLineRange = EditorDOMRange(rangesToWrapTheLine.FirstRangeRef());
+ 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, 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));
+
+ if (&aBlockElement == &aAncestorElement) {
+ return nullptr;
+ }
+
+ Element* lastBlockAncestor = &aBlockElement;
+ for (Element* element : aBlockElement.InclusiveAncestorsOfType<Element>()) {
+ if (element == &aAncestorElement) {
+ return lastBlockAncestor;
+ }
+ if (HTMLEditUtils::IsBlockElement(*lastBlockAncestor)) {
+ 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::GetComputedWhiteSpaceStyle(aContent).valueOr(
+ StyleWhiteSpace::Normal) != StyleWhiteSpace::Pre) {
+ 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());
+
+ 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");
+ return splitAtLineEdgesResult.propagateErr();
+ }
+ splitAtLineEdgesResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ aHTMLEditor.MaybeSplitElementsAtEveryBRElement(
+ arrayOfContents, EditSubAction::eMergeBlockContents);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eMergeBlockContents) failed");
+ return splitAtBRElementsResult.propagateErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+ }
+
+ if (!pointToInsert.IsSetAndValid()) {
+ 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");
+ return Err(rv);
+ }
+ }
+
+ if (arrayOfContents.IsEmpty()) {
+ 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) {
+ {
+ AutoEditorDOMRangeChildrenInvalidator lockOffsets(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)) {
+ Result<MoveNodeResult, nsresult> moveChildrenResult =
+ aHTMLEditor.MoveChildrenWithTransaction(
+ MOZ_KnownLive(*content->AsElement()), pointToInsert,
+ mPreserveWhiteSpaceStyle, RemoveIfCommentNode::Yes);
+ if (MOZ_UNLIKELY(moveChildrenResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveChildrenWithTransaction() failed");
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return moveChildrenResult;
+ }
+ moveContentsInLineResult |= moveChildrenResult.inspect();
+ moveContentsInLineResult.MarkAsHandled();
+ // MOZ_KnownLive due to bug 1620312
+ nsresult rv =
+ aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(content));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed, but 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() ||
+ HTMLEditUtils::IsEmptyInlineContainer(
+ content, {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible})) {
+ nsCOMPtr<nsIContent> emptyContent =
+ HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
+ content, &aEditingHost,
+ pointToInsert.ContainerAs<nsIContent>());
+ if (!emptyContent) {
+ emptyContent = content;
+ }
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*emptyContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return Err(rv);
+ }
+ } else {
+ // 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");
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return moveNodeOrChildrenResult;
+ }
+ moveContentsInLineResult |= moveNodeOrChildrenResult.inspect();
+ }
+ }
+ // 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();
+ movedContentRange.SetEnd(pointToInsert);
+ }
+ // 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);
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(
+ moveContentsInLineResult.NextInsertionPointRef().IsSet());
+ mPointToInsert = moveContentsInLineResult.NextInsertionPointRef();
+ pointToInsert = NextInsertionPointRef();
+ if (!aHTMLEditor.MayHaveMutationEventListeners() ||
+ movedContentRange.EndRef().IsBefore(pointToInsert)) {
+ movedContentRange.SetEnd(pointToInsert);
+ }
+ }
+ }
+
+ // 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)) {
+ 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())) {
+ return moveContentsInLineResult;
+ }
+
+ nsresult rv = DeleteUnnecessaryTrailingLineBreakInMovedLineEnd(
+ aHTMLEditor, movedContentRange, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoMoveOneLineHandler::"
+ "DeleteUnnecessaryTrailingLineBreakInMovedLineEnd() failed");
+ moveContentsInLineResult.IgnoreCaretPointSuggestion();
+ return Err(rv);
+ }
+ 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},
+ 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, &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, &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 destWhiteSpaceStyle = [&]() -> Maybe<StyleWhiteSpace> {
+ if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No ||
+ !aPointToInsert.IsInContentNode()) {
+ return Nothing();
+ }
+ auto style = EditorUtils::GetComputedWhiteSpaceStyle(
+ *aPointToInsert.ContainerAs<nsIContent>());
+ if (NS_WARN_IF(style.isSome() &&
+ style.value() == StyleWhiteSpace::PreSpace)) {
+ return Nothing();
+ }
+ return style;
+ }();
+ const auto srcWhiteSpaceStyle = [&]() -> Maybe<StyleWhiteSpace> {
+ if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No) {
+ return Nothing();
+ }
+ auto style = EditorUtils::GetComputedWhiteSpaceStyle(aContentToMove);
+ if (NS_WARN_IF(style.isSome() &&
+ style.value() == StyleWhiteSpace::PreSpace)) {
+ return Nothing();
+ }
+ return style;
+ }();
+ const auto GetWhiteSpaceStyleValue = [](StyleWhiteSpace aStyleWhiteSpace) {
+ switch (aStyleWhiteSpace) {
+ case StyleWhiteSpace::Normal:
+ return u"normal"_ns;
+ case StyleWhiteSpace::Pre:
+ return u"pre"_ns;
+ case StyleWhiteSpace::Nowrap:
+ return u"nowrap"_ns;
+ case StyleWhiteSpace::PreWrap:
+ return u"pre-wrap"_ns;
+ case StyleWhiteSpace::PreLine:
+ return u"pre-line"_ns;
+ case StyleWhiteSpace::BreakSpaces:
+ return u"break-spaces"_ns;
+ case StyleWhiteSpace::PreSpace:
+ MOZ_ASSERT_UNREACHABLE("Don't handle -moz-pre-space");
+ return u""_ns;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Handle the new white-space value");
+ return u""_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 (destWhiteSpaceStyle.isSome() && srcWhiteSpaceStyle.isSome() &&
+ destWhiteSpaceStyle.value() != srcWhiteSpaceStyle.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(srcWhiteSpaceStyle.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(srcWhiteSpaceStyle.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;
+}
+
+void HTMLEditor::MoveAllChildren(nsINode& aContainer,
+ const EditorRawDOMPoint& aPointToInsert,
+ ErrorResult& aError) {
+ MOZ_ASSERT(!aError.Failed());
+
+ if (!aContainer.HasChildren()) {
+ return;
+ }
+ nsIContent* firstChild = aContainer.GetFirstChild();
+ if (NS_WARN_IF(!firstChild)) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ nsIContent* lastChild = aContainer.GetLastChild();
+ if (NS_WARN_IF(!lastChild)) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert, aError);
+ NS_WARNING_ASSERTION(!aError.Failed(),
+ "HTMLEditor::MoveChildrenBetween() failed");
+}
+
+void HTMLEditor::MoveChildrenBetween(nsIContent& aFirstChild,
+ nsIContent& aLastChild,
+ const EditorRawDOMPoint& aPointToInsert,
+ ErrorResult& aError) {
+ nsCOMPtr<nsINode> oldContainer = aFirstChild.GetParentNode();
+ if (NS_WARN_IF(oldContainer != aLastChild.GetParentNode()) ||
+ NS_WARN_IF(!aPointToInsert.IsInContentNode()) ||
+ NS_WARN_IF(!aPointToInsert.CanContainerHaveChildren())) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ // 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)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ nsCOMPtr<nsIContent> newContainer = aPointToInsert.ContainerAs<nsIContent>();
+ nsCOMPtr<nsIContent> nextNode = aPointToInsert.GetChild();
+ 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))) {
+ aError.Throw(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ return;
+ }
+ oldContainer->RemoveChild(*child, aError);
+ if (NS_WARN_IF(Destroyed())) {
+ aError.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+ if (aError.Failed()) {
+ NS_WARNING("nsINode::RemoveChild() failed");
+ return;
+ }
+ 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?
+ aError.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+ if (NS_WARN_IF(
+ !EditorUtils::IsEditableContent(*newContainer, EditorType::HTML))) {
+ aError.Throw(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ return;
+ }
+ newContainer->InsertBefore(*child, nextNode, aError);
+ if (NS_WARN_IF(Destroyed())) {
+ aError.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+ if (aError.Failed()) {
+ NS_WARNING("nsINode::InsertBefore() failed");
+ return;
+ }
+ // 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;
+ }
+ }
+}
+
+void HTMLEditor::MovePreviousSiblings(nsIContent& aChild,
+ const EditorRawDOMPoint& aPointToInsert,
+ ErrorResult& aError) {
+ MOZ_ASSERT(!aError.Failed());
+
+ if (NS_WARN_IF(!aChild.GetParentNode())) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+ nsIContent* firstChild = aChild.GetParentNode()->GetFirstChild();
+ if (NS_WARN_IF(!firstChild)) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ nsIContent* lastChild =
+ &aChild == firstChild ? firstChild : aChild.GetPreviousSibling();
+ if (NS_WARN_IF(!lastChild)) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert, aError);
+ NS_WARNING_ASSERTION(!aError.Failed(),
+ "HTMLEditor::MoveChildrenBetween() failed");
+}
+
+void HTMLEditor::MoveInclusiveNextSiblings(
+ nsIContent& aChild, const EditorRawDOMPoint& aPointToInsert,
+ ErrorResult& aError) {
+ MOZ_ASSERT(!aError.Failed());
+
+ if (NS_WARN_IF(!aChild.GetParentNode())) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+ nsIContent* lastChild = aChild.GetParentNode()->GetLastChild();
+ if (NS_WARN_IF(!lastChild)) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ MoveChildrenBetween(aChild, *lastChild, aPointToInsert, aError);
+ NS_WARNING_ASSERTION(!aError.Failed(),
+ "HTMLEditor::MoveChildrenBetween() failed");
+}
+
+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},
+ &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);
+ 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);
+ }
+ 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, {}, 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},
+ 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)) {
+ 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);
+ 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());
+ if (!backwardScanFromStartResult.ReachedCurrentBlockBoundary()) {
+ break;
+ }
+ MOZ_ASSERT(backwardScanFromStartResult.GetContent() ==
+ WSRunScanner(editingHost, rangeToDelete.StartRef())
+ .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;
+ }
+ 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());
+ 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;
+ }
+ 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)) {
+ 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..840f4b38ee
--- /dev/null
+++ b/editor/libeditor/HTMLEditorDocumentCommands.cpp
@@ -0,0 +1,482 @@
+/* -*- 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 "HTMLEditor.h" // for HTMLEditor
+
+#include "mozilla/BasePrincipal.h" // for nsIPrincipal::IsSystemPrincipal()
+#include "mozilla/StaticPrefs_editor.h"
+#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;
+ case Command::EnableCompatibleJoinSplitNodeDirection:
+ return aEditorBase && aEditorBase->IsHTMLEditor() &&
+ aEditorBase->AsHTMLEditor()->CanChangeJoinSplitNodeDirection();
+ 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:
+ MOZ_ASSERT_IF(
+ StaticPrefs::
+ editor_join_split_direction_compatible_with_the_other_browsers() &&
+ aPrincipal && !aPrincipal->IsSystemPrincipal(),
+ aBoolParam.value());
+ return MOZ_KnownLive(aEditorBase.AsHTMLEditor())
+ ->EnableCompatibleJoinSplitNodeDirection(
+ aBoolParam.value())
+ ? NS_OK
+ : NS_SUCCESS_DOM_NO_OPERATION;
+ 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;
+ }
+ return aParams.SetBool(
+ STATE_ALL, htmlEditor->IsCompatibleJoinSplitNodeDirectionEnabled());
+ }
+ 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..a90d6c7a7d
--- /dev/null
+++ b/editor/libeditor/HTMLEditorEventListener.cpp
@@ -0,0 +1,442 @@
+/* -*- 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 (EditorUtils::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");
+ } else {
+ DebugOnly<nsresult> rvIgnored = selection->CollapseInLimiter(
+ parentContent, AssertedCast<uint32_t>(offset));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Selection::CollapseInLimiter() 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->GetDOMEventTarget());
+ 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..201265dfab
--- /dev/null
+++ b/editor/libeditor/HTMLEditorEventListener.h
@@ -0,0 +1,100 @@
+/* -*- 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()
+ : EditorEventListener(),
+ 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..1b6e5a1834
--- /dev/null
+++ b/editor/libeditor/HTMLEditorInlines.h
@@ -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/. */
+
+#ifndef HTMLEditorInlines_h
+#define HTMLEditorInlines_h
+
+#include "HTMLEditor.h"
+
+#include "EditorDOMPoint.h"
+#include "HTMLEditHelpers.h"
+#include "JoinSplitNodeDirection.h" // for JoinNodesDirection and SplitNodeDirection
+#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"
+
+namespace mozilla {
+
+using namespace dom;
+
+SplitNodeDirection HTMLEditor::GetSplitNodeDirection() const {
+ return MOZ_LIKELY(mUseGeckoTraditionalJoinSplitBehavior)
+ ? SplitNodeDirection::LeftNodeIsNewOne
+ : SplitNodeDirection::RightNodeIsNewOne;
+}
+
+JoinNodesDirection HTMLEditor::GetJoinNodesDirection() const {
+ return MOZ_LIKELY(mUseGeckoTraditionalJoinSplitBehavior)
+ ? JoinNodesDirection::LeftNodeIntoRightNode
+ : JoinNodesDirection::RightNodeIntoLeftNode;
+}
+
+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;
+}
+
+} // 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..ea3caf4778
--- /dev/null
+++ b/editor/libeditor/HTMLEditorObjectResizer.cpp
@@ -0,0 +1,1495 @@
+/* -*- 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(kNameSpaceID_None, 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(kNameSpaceID_None, 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(kNameSpaceID_None, 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..06f10e82e6
--- /dev/null
+++ b/editor/libeditor/HTMLEditorState.cpp
@@ -0,0 +1,656 @@
+/* -*- 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eCreateOrChangeList, *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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eCreateOrChangeList, *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},
+ aHTMLEditor.ComputeEditingHost());
+ if (NS_WARN_IF(!editTargetContent)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+ // Otherwise, use first selected node.
+ // XXX Only for retreiving 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eSetOrClearAlignment, *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);
+ 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(kNameSpaceID_None, 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,
+ 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;
+ }
+
+ 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;
+ nsresult rv = CollectEditableFormatNodesInSelection(
+ aHTMLEditor, *editingHostOrRoot, 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 (int32_t i = arrayOfContents.Length() - 1; i >= 0; i--) {
+ auto& content = arrayOfContents[i];
+ nsAutoString format;
+ if (HTMLEditUtils::IsBlockElement(content) &&
+ !HTMLEditUtils::IsFormatNode(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, *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())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ arrayOfContents.AppendElement(*atCaret.ContainerAs<nsIContent>());
+ }
+
+ dom::Element* bodyOrDocumentElement = aHTMLEditor.GetRoot();
+ if (NS_WARN_IF(!bodyOrDocumentElement)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ for (auto& content : Reversed(arrayOfContents)) {
+ nsAtom* paragraphStateOfNode = nsGkAtoms::_empty;
+ if (HTMLEditUtils::IsFormatNode(content)) {
+ MOZ_ASSERT(content->NodeInfo()->NameAtom());
+ paragraphStateOfNode = content->NodeInfo()->NameAtom();
+ }
+ // Ignore non-format block node since its children have been appended
+ // the list above so that we'll handle this descendants later.
+ else if (HTMLEditUtils::IsBlockElement(content)) {
+ continue;
+ }
+ // If we meet an inline node, let's get its parent format.
+ else {
+ for (nsINode* parentNode = content->GetParentNode(); parentNode;
+ parentNode = parentNode->GetParentNode()) {
+ // If we reach `HTMLDocument.body` or `Document.documentElement`,
+ // there is no format.
+ if (parentNode == bodyOrDocumentElement) {
+ break;
+ }
+ if (HTMLEditUtils::IsFormatNode(parentNode)) {
+ MOZ_ASSERT(parentNode->NodeInfo()->NameAtom());
+ paragraphStateOfNode = parentNode->NodeInfo()->NameAtom();
+ break;
+ }
+ }
+ }
+
+ // if this is the first node, we've found, remember it as the format
+ if (!mFirstParagraphState) {
+ mFirstParagraphState = paragraphStateOfNode;
+ continue;
+ }
+ // else make sure it matches previously found format
+ if (mFirstParagraphState != paragraphStateOfNode) {
+ mIsMixed = true;
+ break;
+ }
+ }
+}
+
+// static
+void ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ dom::Element& aNonFormatBlockElement) {
+ MOZ_ASSERT(HTMLEditUtils::IsBlockElement(aNonFormatBlockElement));
+ MOZ_ASSERT(!HTMLEditUtils::IsFormatNode(&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()) {
+ bool isBlock = HTMLEditUtils::IsBlockElement(*childContent);
+ bool isFormat = HTMLEditUtils::IsFormatNode(childContent);
+ // If the child is a non-format block element, let's check its children
+ // recursively.
+ if (isBlock && !isFormat) {
+ ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
+ aArrayOfContents, *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, const Element& aEditingHost,
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents) {
+ {
+ AutoRangeArray extendedSelectionRanges(aHTMLEditor.SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
+ EditSubAction::eCreateOrRemoveBlock, aEditingHost);
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ aHTMLEditor, aArrayOfContents, EditSubAction::eCreateOrRemoveBlock,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eCreateOrRemoveBlock, CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+
+ // Pre-process our list of nodes
+ for (int32_t i = aArrayOfContents.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsIContent> content = aArrayOfContents[i];
+
+ // Remove all non-editable nodes. Leave them be.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ aArrayOfContents.RemoveElementAt(i);
+ 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(i);
+ HTMLEditUtils::CollectChildren(
+ content, aArrayOfContents, i,
+ {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..34192660ff
--- /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(kNameSpaceID_None, 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..5bee16b3ec
--- /dev/null
+++ b/editor/libeditor/HTMLStyleEditor.cpp
@@ -0,0 +1,4430 @@
+/* -*- 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/StaticPrefs_editor.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 (IsInPlaintextMode()) {
+ 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) {
+ for (const EditorInlineStyleAndValue& styleToSet : aStylesToSet) {
+ if (!StaticPrefs::
+ editor_inline_style_range_compatible_with_the_other_browsers() &&
+ !aRanges.IsCollapsed()) {
+ MOZ_ALWAYS_TRUE(aRanges.SaveAndTrackRanges(*this));
+ }
+ AutoInlineStyleSetter inlineStyleSetter(styleToSet);
+ for (OwningNonNull<nsRange>& domRange : aRanges.Ranges()) {
+ inlineStyleSetter.Reset();
+ auto rangeOrError =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMRange, nsresult> {
+ if (aRanges.HasSavedRanges()) {
+ return EditorDOMRange(
+ GetExtendedRangeWrappingEntirelySelectedElements(
+ EditorRawDOMRange(domRange)));
+ }
+ 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 (aRanges.HasSavedRanges()) {
+ return;
+ }
+ // 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();
+ }
+ if (aRanges.HasSavedRanges()) {
+ aRanges.RestoreFromSavedRanges();
+ }
+ }
+ 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(kNameSpaceID_None, 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(kNameSpaceID_None, 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())) {
+ 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,
+ aHTMLEditor.GetSplitNodeDirection());
+ }
+ // 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,
+ aHTMLEditor.GetSplitNodeDirection());
+ }
+ // 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()) ||
+ 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)
+ ? 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()) ||
+ 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)
+ ? 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)) {
+ 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)) {
+ 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) ||
+ 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) ||
+ 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>()) ||
+ 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>()) ||
+ 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>()) &&
+ // 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>()) &&
+ // 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());
+ if (nextContentData.ReachedInvisibleBRElement() &&
+ nextContentData.BRElementPtr()->GetParentElement() &&
+ HTMLEditUtils::IsInlineElement(
+ *nextContentData.BRElementPtr()->GetParentElement())) {
+ 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));
+ }
+ 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 (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));
+ }
+ 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, GetSplitNodeDirection());
+ }
+
+ // 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 (HTMLEditUtils::IsBlockElement(*element) || !element->GetParent() ||
+ !EditorUtils::IsEditableContent(*element->GetParent(),
+ EditorType::HTML)) {
+ break;
+ }
+ arrayOfParents.AppendElement(*element);
+ }
+
+ // Split any matching style nodes above the point.
+ SplitNodeResult result =
+ SplitNodeResult::NotHandled(aPointToSplit, GetSplitNodeDirection());
+ 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;
+ }
+ // Mark the final result as handled forcibly.
+ 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});
+ 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 (IsInPlaintextMode()) {
+ 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()) ||
+ !StaticPrefs::
+ editor_inline_style_range_compatible_with_the_other_browsers()) {
+ 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(kNameSpaceID_None, 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..7aee5e0ac6
--- /dev/null
+++ b/editor/libeditor/HTMLTableEditor.cpp
@@ -0,0 +1,4600 @@
+/* -*- 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});
+}
+
+} // 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..8010fe32f6
--- /dev/null
+++ b/editor/libeditor/InsertTextTransaction.cpp
@@ -0,0 +1,182 @@
+/* -*- 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;
+ }
+
+ nsAutoString otherData;
+ otherInsertTextTransaction->GetData(otherData);
+ mStringToInsert += otherData;
+ *aDidMerge = true;
+ MOZ_LOG(GetLogModule(), LogLevel::Debug,
+ ("%p InsertTextTransaction::%s(aOtherTransaction=%p) returned true",
+ this, __FUNCTION__, aOtherTransaction));
+ return NS_OK;
+}
+
+/* ============ private methods ================== */
+
+void InsertTextTransaction::GetData(nsString& aResult) {
+ aResult = mStringToInsert;
+}
+
+bool InsertTextTransaction::IsSequentialInsert(
+ InsertTextTransaction& aOtherTransaction) {
+ 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..6b78d107d7
--- /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.
+ */
+ void GetData(nsString& aResult);
+
+ 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& aOtherTrasaction);
+
+ // 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..15891938c8
--- /dev/null
+++ b/editor/libeditor/JoinNodesTransaction.cpp
@@ -0,0 +1,212 @@
+/* -*- 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 "JoinSplitNodeDirection.h" // JoinNodesDirection
+#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(aHTMLEditor.GetJoinNodesDirection() ==
+ JoinNodesDirection::LeftNodeIntoRightNode
+ ? &aLeftContent
+ : &aRightContent),
+ mKeepingContent(aHTMLEditor.GetJoinNodesDirection() ==
+ JoinNodesDirection::LeftNodeIntoRightNode
+ ? &aRightContent
+ : &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()
+ << ", GetJoinNodesDirection()="
+ << aTransaction.GetJoinNodesDirection() << " }";
+ 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)
+
+SplitNodeDirection JoinNodesTransaction::GetSplitNodeDirection() const {
+ return MOZ_LIKELY(mHTMLEditor) ? mHTMLEditor->GetSplitNodeDirection()
+ : SplitNodeDirection::LeftNodeIsNewOne;
+}
+
+JoinNodesDirection JoinNodesTransaction::GetJoinNodesDirection() const {
+ return MOZ_LIKELY(mHTMLEditor) ? mHTMLEditor->GetJoinNodesDirection()
+ : JoinNodesDirection::LeftNodeIntoRightNode;
+}
+
+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 intervation. The offset will be adjusted
+ // below.
+ mJoinedOffset =
+ GetJoinNodesDirection() == JoinNodesDirection::LeftNodeIntoRightNode
+ ? mRemovedContent->Length()
+ : 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.
+ EditorDOMPoint joinNodesPoint =
+ GetJoinNodesDirection() == JoinNodesDirection::LeftNodeIntoRightNode
+ ? EditorDOMPoint(keepingContent, 0u)
+ : EditorDOMPoint::AtEndOf(keepingContent);
+ {
+ AutoTrackDOMPoint trackJoinNodePoint(htmlEditor->RangeUpdaterRef(),
+ &joinNodesPoint);
+ rv = htmlEditor->DoJoinNodes(keepingContent, removingContent,
+ GetJoinNodesDirection());
+ 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, GetSplitNodeDirection());
+ 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..a355838b03
--- /dev/null
+++ b/editor/libeditor/JoinNodesTransaction.h
@@ -0,0 +1,112 @@
+/* -*- 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;
+
+ // Note that we don't support join/split node direction switching per
+ // transaction.
+ [[nodiscard]] SplitNodeDirection GetSplitNodeDirection() const;
+ [[nodiscard]] JoinNodesDirection GetJoinNodesDirection() const;
+
+ /**
+ * 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/JoinSplitNodeDirection.h b/editor/libeditor/JoinSplitNodeDirection.h
new file mode 100644
index 0000000000..fd50759168
--- /dev/null
+++ b/editor/libeditor/JoinSplitNodeDirection.h
@@ -0,0 +1,51 @@
+/* -*- 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 JoinSplitNodeDirection_h
+#define JoinSplitNodeDirection_h
+
+#include <iostream>
+
+namespace mozilla {
+
+// JoinNodesDirection is also affected to which one is new node at splitting
+// a node because a couple of undo/redo.
+enum class JoinNodesDirection {
+ LeftNodeIntoRightNode,
+ RightNodeIntoLeftNode,
+};
+
+static inline std::ostream& operator<<(std::ostream& aStream,
+ JoinNodesDirection aJoinNodesDirection) {
+ if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
+ return aStream << "JoinNodesDirection::LeftNodeIntoRightNode";
+ }
+ if (aJoinNodesDirection == JoinNodesDirection::RightNodeIntoLeftNode) {
+ return aStream << "JoinNodesDirection::RightNodeIntoLeftNode";
+ }
+ return aStream << "Invalid value";
+}
+
+// SplitNodeDirection is also affected to which one is removed at joining a
+// node because a couple of undo/redo.
+enum class SplitNodeDirection {
+ LeftNodeIsNewOne,
+ RightNodeIsNewOne,
+};
+
+static inline std::ostream& operator<<(std::ostream& aStream,
+ SplitNodeDirection aSplitNodeDirection) {
+ if (aSplitNodeDirection == SplitNodeDirection::LeftNodeIsNewOne) {
+ return aStream << "SplitNodeDirection::LeftNodeIsNewOne";
+ }
+ if (aSplitNodeDirection == SplitNodeDirection::RightNodeIsNewOne) {
+ return aStream << "SplitNodeDirection::RightNodeIsNewOne";
+ }
+ return aStream << "Invalid value";
+}
+
+} // namespace mozilla
+
+#endif // JoinSplitNodeDirection_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..063b941c67
--- /dev/null
+++ b/editor/libeditor/SelectionState.cpp
@@ -0,0 +1,621 @@
+/* -*- 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 "JoinSplitNodeDirection.h" // for JoinNodesDirection, SplitNodeDirection
+
+#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,
+ SplitNodeDirection aSplitNodeDirection) {
+ 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()) {
+ if (aSplitNodeDirection == SplitNodeDirection::LeftNodeIsNewOne) {
+ // When we create a left node, we insert it before the right node.
+ // In this case,
+ // - `{}<right/>` should become `{}<left/><right/>` (0 -> 0)
+ // - `<right/>{}` should become `<left/><right/>{}` (1 -> 2)
+ // - `{<right/>}` should become `{<left/><right/>}` (0 -> 0, 1 -> 2}
+ // Therefore, we need to increate the offset only when the offset is
+ // larger than the offset at the left node.
+ if (aOffset > atNewNode.Offset()) {
+ aOffset++;
+ }
+ } else {
+ // 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 (aSplitNodeDirection == SplitNodeDirection::LeftNodeIsNewOne) {
+ if (aOffset > aSplitOffset) {
+ aOffset -= aSplitOffset;
+ } else {
+ aContainer = &aNewContent;
+ }
+ } else 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,
+ JoinNodesDirection aJoinNodesDirection) {
+ 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();
+ if (aJoinNodesDirection == JoinNodesDirection::RightNodeIntoLeftNode) {
+ 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();
+ }
+ } else if (aContainer == aStartOfRightContent.GetContainer()) {
+ // If the point is in joined node, and removed content is moved to
+ // start of the joined node, we need to adjust the offset.
+ if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
+ 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..d62f2a7e7b
--- /dev/null
+++ b/editor/libeditor/SelectionState.h
@@ -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/. */
+
+#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 "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_MAIN_THREAD_ONLY_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.
+ * @param aSplitNodeDirection Whether aNewNode was inserted before or after
+ * aOriginalContent.
+ */
+ nsresult SelAdjSplitNode(nsIContent& aOriginalContent, uint32_t aSplitOffset,
+ nsIContent& aNewContent,
+ SplitNodeDirection aSplitNodeDirection);
+
+ /**
+ * 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,
+ JoinNodesDirection aJoinNodesDirection);
+ 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())) {
+ mRangeItem->mStartContainer = *mNode;
+ mRangeItem->mEndContainer = *mNode;
+ mRangeItem->mStartOffset = *mOffset;
+ mRangeItem->mEndOffset = *mOffset;
+ 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())) {
+ 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();
+ 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 (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;
+ }
+
+ 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;
+ bool mIsTracking = true;
+};
+
+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) {
+ (*mRangeRefPtr)
+ ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
+ mEndPoint.ToRawRangeBoundary());
+ return;
+ }
+ if (mRangeOwningNonNull) {
+ (*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..42d127407f
--- /dev/null
+++ b/editor/libeditor/SplitNodeTransaction.cpp
@@ -0,0 +1,241 @@
+/* -*- 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 "JoinSplitNodeDirection.h" // for SplitNodeDirection
+#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()
+ << ", GetSplitNodeDirection()="
+ << aTransaction.GetSplitNodeDirection() << " }";
+ 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)
+
+SplitNodeDirection SplitNodeTransaction::GetSplitNodeDirection() const {
+ return MOZ_LIKELY(mHTMLEditor) ? mHTMLEditor->GetSplitNodeDirection()
+ : SplitNodeDirection::LeftNodeIsNewOne;
+}
+
+JoinNodesDirection SplitNodeTransaction::GetJoinNodesDirection() const {
+ return MOZ_LIKELY(mHTMLEditor) ? mHTMLEditor->GetJoinNodesDirection()
+ : JoinNodesDirection::LeftNodeIntoRightNode;
+}
+
+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, GetSplitNodeDirection());
+ 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;
+ nsresult rv;
+ 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 the direction.
+ Maybe<AutoTrackDOMPoint> trackJoinedPoint;
+ if (GetJoinNodesDirection() == JoinNodesDirection::LeftNodeIntoRightNode) {
+ joinedPoint.Set(keepingContent, 0u);
+ trackJoinedPoint.emplace(htmlEditor->RangeUpdaterRef(), &joinedPoint);
+ }
+ rv = htmlEditor->DoJoinNodes(keepingContent, removingContent,
+ GetJoinNodesDirection());
+ }
+ 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..edc2ddbecd
--- /dev/null
+++ b/editor/libeditor/SplitNodeTransaction.h
@@ -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/. */
+
+#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;
+
+ // Note that we don't support join/split node direction switching per
+ // transaction.
+ [[nodiscard]] SplitNodeDirection GetSplitNodeDirection() const;
+ [[nodiscard]] JoinNodesDirection GetJoinNodesDirection() const;
+
+ 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..17fae74f2a
--- /dev/null
+++ b/editor/libeditor/TextEditSubActionHandler.cpp
@@ -0,0 +1,817 @@
+/* -*- 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()));
+ NS_WARNING_ASSERTION(
+ point.IsSet(),
+ "EditorBase::FindBetterInsertionPoint() failed, but ignored");
+ if (point.IsSet()) {
+ SetSpellCheckRestartPoint(point);
+ return;
+ }
+ }
+ 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();
+
+ 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();
+ }
+
+ nsAutoString insertionString(aInsertionString);
+ if (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);
+ }
+ }
+
+ // 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(),
+ "EditorBase::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..d5901850ad
--- /dev/null
+++ b/editor/libeditor/TextEditor.cpp
@@ -0,0 +1,1140 @@
+/* -*- 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_editor.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TextServicesDocument.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;
+
+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;
+ }
+ // Our widget shouldn't set `\r` to `mCharCode`, 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`).
+ char16_t charCode =
+ static_cast<char16_t>(aKeyboardEvent->mCharCode) == nsCRT::CR
+ ? nsCRT::LF
+ : static_cast<char16_t>(aKeyboardEvent->mCharCode);
+ aKeyboardEvent->PreventDefault();
+ nsAutoString str(charCode);
+ 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;
+ }
+
+ // Get the Data from the clipboard
+ clipboard->GetData(trans, aClipboardType);
+
+ // 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);
+}
+
+// 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..77df2dabba
--- /dev/null
+++ b/editor/libeditor/TextEditor.h
@@ -0,0 +1,614 @@
+/* -*- 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; }
+
+ /**
+ * 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;
+
+ /**
+ * 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 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..1ca11e7adb
--- /dev/null
+++ b/editor/libeditor/TextEditorDataTransfer.cpp
@@ -0,0 +1,273 @@
+/* -*- 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.
+ rv = clipboard->GetData(transferable, aClipboardType);
+ 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..3d4554ae73
--- /dev/null
+++ b/editor/libeditor/WSRunObject.cpp
@@ -0,0 +1,4390 @@
+/* -*- 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/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);
+template WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorRawDOMPoint& aPoint, const Element* aEditingHost);
+template WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorDOMPointInText& aPoint, const Element* aEditingHost);
+
+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));
+ 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);
+ 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));
+ 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::GetComputedWhiteSpaceStyle(aLeftBlockElement) ==
+ EditorUtils::GetComputedWhiteSpaceStyle(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);
+ 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);
+ 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);
+ 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);
+ 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);
+ 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);
+ }
+ Result<CaretPoint, nsresult> caretPointOrError = WhiteSpaceVisibilityKeeper::
+ MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange(
+ aHTMLEditor, EditorDOMRange(atContent, atContent.NextPoint()),
+ aEditingHost);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::"
+ "MakeSureToKeepVisibleStateOfWhiteSpacesAroundDeletingRange() failed");
+ return caretPointOrError;
+ }
+
+ nsCOMPtr<nsIContent> previousEditableSibling =
+ HTMLEditUtils::GetPreviousSibling(
+ aContentToDelete, {WalkTreeOption::IgnoreNonEditableNode});
+ // Delete the node, and join like nodes if appropriate
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContentToDelete);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+ 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 caretPointOrError;
+ }
+
+ nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling(
+ *previousEditableSibling, {WalkTreeOption::IgnoreNonEditableNode});
+ if (aCaretPoint.GetContainer() != nextEditableSibling) {
+ return caretPointOrError;
+ }
+
+ caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
+
+ EditorDOMPoint atFirstChildOfRightNode;
+ rv = aHTMLEditor.JoinNearestEditableNodesWithTransaction(
+ *previousEditableSibling, MOZ_KnownLive(*aCaretPoint.ContainerAs<Text>()),
+ &atFirstChildOfRightNode);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::JoinNearestEditableNodesWithTransaction() failed");
+ return Err(rv);
+ }
+ if (!atFirstChildOfRightNode.IsSet()) {
+ NS_WARNING(
+ "HTMLEditor::JoinNearestEditableNodesWithTransaction() didn't return "
+ "right node position");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return CaretPoint(std::move(atFirstChildOfRightNode));
+}
+
+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);
+ }
+
+ // 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);
+ }
+ 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);
+ }
+ }
+
+ // 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());
+ }
+ return WSScanResult(TextFragmentDataAtStartRef().StartRef(),
+ TextFragmentDataAtStartRef().StartRawReason());
+}
+
+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);
+ }
+
+ // 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);
+ }
+ 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);
+ }
+ }
+
+ // 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());
+ }
+ return WSScanResult(TextFragmentDataAtStartRef().EndRef(),
+ TextFragmentDataAtStartRef().EndRawReason());
+}
+
+template <typename EditorDOMPointType>
+WSRunScanner::TextFragmentData::TextFragmentData(
+ const EditorDOMPointType& aPoint, const Element* aEditingHost)
+ : mEditingHost(aEditingHost) {
+ 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);
+ if (!editableBlockElementOrInlineEditingHost) {
+ NS_WARNING(
+ "HTMLEditUtils::GetInclusiveAncestorElement(HTMLEditUtils::"
+ "ClosestEditableBlockElementOrInlineEditingHost) couldn't find "
+ "editing host");
+ return;
+ }
+
+ mStart = BoundaryData::ScanCollapsibleWhiteSpaceStartFrom(
+ mScanStartPoint, *editableBlockElementOrInlineEditingHost, mEditingHost,
+ &mNBSPData);
+ MOZ_ASSERT_IF(mStart.IsNonCollapsibleCharacters(),
+ !mStart.PointRef().IsPreviousCharPreformattedNewLine());
+ MOZ_ASSERT_IF(mStart.IsPreformattedLineBreak(),
+ mStart.PointRef().IsPreviousCharPreformattedNewLine());
+ mEnd = BoundaryData::ScanCollapsibleWhiteSpaceEndFrom(
+ mScanStartPoint, *editableBlockElementOrInlineEditingHost, mEditingHost,
+ &mNBSPData);
+ 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) {
+ 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& aEditableBlockParentOrTopmostEditableInlineContent,
+ const Element* aEditingHost, NoBreakingSpaceData* aNBSPData) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (aPoint.IsInTextNode() && !aPoint.IsStartOfContainer()) {
+ Maybe<BoundaryData> startInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(aPoint,
+ aNBSPData);
+ 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),
+ aEditableBlockParentOrTopmostEditableInlineContent, aEditingHost,
+ aNBSPData);
+ }
+
+ // Then, we need to check previous leaf node.
+ nsIContent* previousLeafContentOrBlock =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ aPoint, aEditableBlockParentOrTopmostEditableInlineContent,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, aEditingHost);
+ if (!previousLeafContentOrBlock) {
+ // no prior node means we exhausted
+ // aEditableBlockParentOrTopmostEditableInlineContent
+ // mReasonContent can be either a block element or any non-editable
+ // content in this case.
+ return BoundaryData(aPoint,
+ const_cast<Element&>(
+ aEditableBlockParentOrTopmostEditableInlineContent),
+ WSType::CurrentBlockBoundary);
+ }
+
+ if (HTMLEditUtils::IsBlockElement(*previousLeafContentOrBlock)) {
+ 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),
+ aEditableBlockParentOrTopmostEditableInlineContent, aEditingHost,
+ aNBSPData);
+ }
+
+ Maybe<BoundaryData> startInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceStartInTextNode(
+ EditorDOMPointInText::AtEndOf(*previousLeafContentOrBlock->AsText()),
+ aNBSPData);
+ 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),
+ aEditableBlockParentOrTopmostEditableInlineContent, aEditingHost,
+ aNBSPData);
+}
+
+// static
+template <typename EditorDOMPointType>
+Maybe<WSRunScanner::TextFragmentData::BoundaryData> WSRunScanner::
+ TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(
+ const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData) {
+ 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) {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (aPoint.IsInTextNode() && !aPoint.IsEndOfContainer()) {
+ Maybe<BoundaryData> endInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(aPoint, aNBSPData);
+ 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);
+ }
+
+ // Then, we need to check next leaf node.
+ nsIContent* nextLeafContentOrBlock =
+ HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ aPoint, aEditableBlockParentOrTopmostEditableInlineElement,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, 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)) {
+ // 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);
+ }
+
+ Maybe<BoundaryData> endInTextNode =
+ BoundaryData::ScanCollapsibleWhiteSpaceEndInTextNode(
+ EditorDOMPointInText(nextLeafContentOrBlock->AsText(), 0), aNBSPData);
+ 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);
+}
+
+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);
+ if (NS_WARN_IF(!textFragmentDataAtStart.IsInitialized())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ TextFragmentData textFragmentDataAtEnd(rangeToDelete.EndRef(), &aEditingHost);
+ 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
+ // starts with another node if the following text node starts with white-
+ // spaces.
+ MOZ_ASSERT_IF(rangeToDelete.EndRef().IsInTextNode() &&
+ rangeToDelete.EndRef().IsEndOfContainer(),
+ rangeToDelete.EndRef() == replaceRangeDataAtEnd.StartRef() ||
+ 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);
+ textFragmentDataAtEnd =
+ TextFragmentData(rangeToDelete.EndRef(), &aEditingHost);
+ }
+ }
+ 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());
+ 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)
+ : 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}, mEditingHost);
+ nextContent;
+ nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ *nextContent, *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, 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)
+ : 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}, mEditingHost);
+ previousContent;
+ previousContent =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ *previousContent, *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode}, 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);
+ 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);
+ 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);
+ 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>());
+ 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)
+ : 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);
+ 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);
+ 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)
+ : textFragmentDataAtCaret;
+ TextFragmentData textFragmentDataAtEnd =
+ rangeToDelete.EndRef() != aPoint
+ ? TextFragmentData(rangeToDelete.EndRef(), &aEditingHost)
+ : 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);
+ 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)
+ : textFragmentDataAtCaret;
+ TextFragmentData textFragmentDataAtEnd =
+ rangeToDelete.EndRef() != aPoint
+ ? TextFragmentData(rangeToDelete.EndRef(), &aEditingHost)
+ : 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);
+ 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)) {
+ // 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);
+ 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);
+ 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);
+ 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);
+ 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);
+ 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);
+ 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) !=
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *aRange.GetEndContainer()->AsContent(),
+ HTMLEditUtils::ClosestEditableBlockElementExceptHRElement)) {
+ 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);
+ 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);
+ 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..0894244cff
--- /dev/null
+++ b/editor/libeditor/WSRunObject.h
@@ -0,0 +1,1633 @@
+/* -*- 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 "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)
+ : mContent(aContent), mReason(aReason) {
+ AssertIfInvalidData();
+ }
+ MOZ_NEVER_INLINE_DEBUG WSScanResult(const EditorDOMPoint& aPoint,
+ WSType aReason)
+ : mContent(aPoint.GetContainerAs<nsIContent>()),
+ mOffset(Some(aPoint.Offset())),
+ mReason(aReason) {
+ AssertIfInvalidData();
+ }
+
+ MOZ_NEVER_INLINE_DEBUG void AssertIfInvalidData() 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))));
+ MOZ_ASSERT_IF(mReason == WSType::OtherBlockBoundary,
+ mContent && HTMLEditUtils::IsBlockElement(*mContent));
+ // 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.
+ MOZ_ASSERT_IF(
+ mReason == WSType::CurrentBlockBoundary,
+ !mContent || !mContent->GetParentElement() ||
+ HTMLEditUtils::IsBlockElement(*mContent) ||
+ HTMLEditUtils::IsBlockElement(*mContent->GetParentElement()) ||
+ !mContent->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; }
+
+ /**
+ * 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)
+ : mScanStartPoint(aScanStartPoint.template To<EditorDOMPoint>()),
+ mEditingHost(const_cast<Element*>(aEditingHost)),
+ mTextFragmentDataAtStart(mScanStartPoint, mEditingHost) {}
+
+ // 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) {
+ return WSRunScanner(aEditingHost, aPoint)
+ .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) {
+ return WSRunScanner(aEditingHost, aPoint)
+ .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) {
+ if (aPoint.IsInTextNode() && !aPoint.IsEndOfContainer() &&
+ HTMLEditUtils::IsSimplyEditableNode(
+ *aPoint.template ContainerAs<Text>())) {
+ return EditorDOMPointType(aPoint.template ContainerAs<Text>(),
+ aPoint.Offset());
+ }
+ return WSRunScanner(aEditingHost, aPoint)
+ .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) {
+ if (aPoint.IsInTextNode() && !aPoint.IsStartOfContainer() &&
+ HTMLEditUtils::IsSimplyEditableNode(
+ *aPoint.template ContainerAs<Text>())) {
+ return EditorDOMPointType(aPoint.template ContainerAs<Text>(),
+ aPoint.Offset() - 1);
+ }
+ return WSRunScanner(aEditingHost, aPoint)
+ .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) {
+ 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);
+ 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);
+
+ /**
+ * 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);
+
+ BoundaryData() : mReason(WSType::NotInitialized) {}
+ 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);
+ template <typename EditorDOMPointType>
+ static Maybe<BoundaryData> ScanCollapsibleWhiteSpaceEndInTextNode(
+ const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData);
+
+ nsCOMPtr<nsIContent> mReasonContent;
+ EditorDOMPoint mPoint;
+ // Must be one of WSType::NotInitialized,
+ // WSType::NonCollapsibleCharacters, WSType::SpecialContent,
+ // WSType::BRElement, WSType::CurrentBlockBoundary or
+ // WSType::OtherBlockBoundary.
+ WSType mReason;
+ };
+
+ 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 EditorDOMPointType& aPoint,
+ const Element* aEditingHost);
+
+ 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;
+ };
+
+ 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;
+
+ 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..72ce9afced
--- /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.import(
+ "resource://reftest/AsyncSpellCheckTestHelper.jsm"
+ );
+ 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..935ebfb7a2
--- /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.ini",
+ "tests/mochitest.ini",
+]
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.ini"]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"]
+
+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/.eslintrc.js b/editor/libeditor/tests/.eslintrc.js
new file mode 100644
index 0000000000..85c68598a9
--- /dev/null
+++ b/editor/libeditor/tests/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ plugins: ["no-unsanitized"],
+
+ rules: {
+ "no-unsanitized/property": "off",
+ },
+};
diff --git a/editor/libeditor/tests/browser.ini b/editor/libeditor/tests/browser.ini
new file mode 100644
index 0000000000..f072cb1309
--- /dev/null
+++ b/editor/libeditor/tests/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+skip-if = toolkit == '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/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..de9abd86a9
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js
@@ -0,0 +1,1867 @@
+/**
+ * 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_BQ-1_SC-dM": true,
+ "QV-Proposed-FB_BQ-1_SC-body": true,
+ "QV-Proposed-FB_BQ-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_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.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": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-body": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-div": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-dM": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-body": true,
+ "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-div": 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_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-B_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-B_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", 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-B_I-1_SL-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-B_I-1_SL-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-B_I-1_SL-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-I_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-I_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-I_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-U_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-U_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-U_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-S_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-S_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-S_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-SUB_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-SUB_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-SUB_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-SUP_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-SUP_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-SUP_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-CL:url_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-CL:url_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-CL:url_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-BC:blue_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-BC:blue_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-BC:blue_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-FC:blue_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-FC:blue_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-FC:blue_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-HC:blue_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-HC:blue_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-HC:blue_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-FN:a_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-FN:a_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-FN:a_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-FS:2_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-FS:2_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "A-Proposed-FS:2_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", 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-B_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-B_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-B_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-I_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-I_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-I_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-U_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-U_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-U_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-S_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-S_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-S_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", 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-BC:blue_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-BC:blue_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-BC:blue_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-FC:blue_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-FC:blue_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-FC:blue_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-HC:blue_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-HC:blue_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-HC:blue_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-FN:a_TEXT-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-FN:a_TEXT-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "AC-Proposed-FN:a_TEXT-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", 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,
+ "C-Proposed-I_I-1_SL-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-I_I-1_SL-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-I_I-1_SL-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-I_B-I-1_SO-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-I_B-I-1_SO-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-I_B-I-1_SO-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-U_U-1_SO-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-U_U-1_SO-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-U_U-1_SO-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-U_U-1_SL-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-U_U-1_SL-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-U_U-1_SL-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-U_S-U-1_SO-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-U_S-U-1_SO-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "C-Proposed-U_S-U-1_SO-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", 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_I-1_SL-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-I_I-1_SL-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-I_I-1_SL-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-I_B-1_SL-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-I_B-1_SL-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-I_B-1_SL-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-I_B-1_SW-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-I_B-1_SW-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-I_B-1_SW-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", 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-1_SO-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "U-Proposed-U_U-S-1_SO-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "U-Proposed-U_U-S-1_SO-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", 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-S_S-U-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "U-Proposed-S_S-U-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "U-Proposed-S_S-U-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "U-Proposed-S_U-S-1_SI-dM": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "U-Proposed-S_U-S-1_SI-body": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", true),
+ "U-Proposed-S_U-S-1_SI-div": !SpecialPowers.getBoolPref("editor.inline_style.range.compatible_with_the_other_browsers", 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.ini b/editor/libeditor/tests/browserscope/mochitest.ini
new file mode 100644
index 0000000000..2f198fa853
--- /dev/null
+++ b/editor/libeditor/tests/browserscope/mochitest.ini
@@ -0,0 +1,59 @@
+[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_richtext2.html]
+skip-if = os == 'android' # Bug 1202045
+[test_richtext.html]
+
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.ini b/editor/libeditor/tests/chrome.ini
new file mode 100644
index 0000000000..52955f048e
--- /dev/null
+++ b/editor/libeditor/tests/chrome.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+skip-if = os == 'android'
+
+[test_bug1397412.xhtml]
+[test_bug489202.xhtml]
+[test_bug599983.xhtml]
+[test_bug607584.xhtml]
+[test_bug616590.xhtml]
+[test_bug780908.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..92a0c76f35
--- /dev/null
+++ b/editor/libeditor/tests/file_bug549262.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <a href="">test</a>
+ <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.ini b/editor/libeditor/tests/mochitest.ini
new file mode 100644
index 0000000000..d3243700b4
--- /dev/null
+++ b/editor/libeditor/tests/mochitest.ini
@@ -0,0 +1,353 @@
+[DEFAULT]
+prefs =
+ apz.zoom-to-focused-input.enabled=false
+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_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]
+skip-if = headless
+[test_bug1248128.html]
+[test_bug1248185.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_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 = toolkit == 'android'
+[test_bug410986.html]
+skip-if = headless
+[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_bug46555.html]
+[test_bug471319.html]
+[test_bug471722.html]
+[test_bug478725.html]
+skip-if = headless
+[test_bug480647.html]
+[test_bug480972.html]
+skip-if = headless
+[test_bug483651.html]
+[test_bug490879.html]
+skip-if =
+ toolkit == 'android' # bug 1299578
+ headless
+[test_bug502673.html]
+[test_bug514156.html]
+[test_bug520189.html]
+skip-if = headless
+[test_bug525389.html]
+skip-if = headless
+[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]
+skip-if = headless
+[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_bug620906.html]
+skip-if = toolkit == 'android' #TIMED_OUT
+[test_bug622371.html]
+[test_bug625452.html]
+[test_bug629172.html]
+skip-if =
+ toolkit == '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 = toolkit == 'android'
+support-files =
+ file_bug674770-1.html
+[test_bug674770-2.html]
+skip-if = toolkit == 'android'
+[test_bug674861.html]
+[test_bug676401.html]
+[test_bug677752.html]
+[test_bug681229.html]
+skip-if = headless
+[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 = toolkit == '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_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_focus.html]
+[test_cut_copy_delete_command_enabled.html]
+[test_cut_copy_password.html]
+[test_defaultParagraphSeparatorBR_between_blocks.html]
+[test_dom_input_event_on_htmleditor.html]
+[test_dom_input_event_on_texteditor.html]
+[test_dragdrop.html]
+skip-if = os == 'android'
+[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_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_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_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_documentCharacterSet.html]
+[test_nsIEditor_documentIsEmpty.html]
+[test_nsIEditor_insertLineBreak.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]
+skip-if = headless # 1686012
+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 = toolkit == '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 = toolkit == '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..19cde54125
--- /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);
+ 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_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..de93cdbeca
--- /dev/null
+++ b/editor/libeditor/tests/test_bug520189.html
@@ -0,0 +1,620 @@
+<!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(http://example.com/) {};</style>baz";
+const invalidStyle9Payload = "foo<style>@-moz-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("@-moz-keyframes"), -1, "Should not have retained the @-moz-keyframes rule"); },
+ },
+ {
+ id: "vv",
+ payload: invalidStyle9Payload,
+ rootElement() { return document.getElementById("vv"); },
+ checkResult(html) { is(html.indexOf("@-moz-keyframes"), -1, "Should not have retained the @-moz-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() {
+ SpecialPowers.pushPrefEnv(
+ { "set": [["layout.css.moz-document.content.enabled", true]]},
+ 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..f547b8c492
--- /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);
+ 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_bug620906.html b/editor/libeditor/tests/test_bug620906.html
new file mode 100644
index 0000000000..e5a7138e18
--- /dev/null
+++ b/editor/libeditor/tests/test_bug620906.html
@@ -0,0 +1,50 @@
+<!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();
+addLoadEvent(function() {
+ var iframe = document.querySelector("iframe");
+ is(iframe.contentWindow.scrollY, 0, "Sanity check");
+ var rect = iframe.getBoundingClientRect();
+ setTimeout(function() {
+ var onscroll = function() {
+ iframe.contentWindow.removeEventListener("scroll", onscroll);
+ isnot(iframe.contentWindow.scrollY, 0, "The scrollbar should work");
+ SimpleTest.finish();
+ };
+ iframe.contentWindow.addEventListener("scroll", onscroll);
+ synthesizeMouse(iframe, rect.width - 5, rect.height / 2, {});
+ }, 0);
+});
+
+</script>
+</pre>
+</body>
+</html>
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..20c41915ee
--- /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 style=\"background-color: rgb(255, 0, 0);\" contenteditable=\"\"><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 style=\"background-color: rgb(0, 255, 0);\" contenteditable=\"\">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 style=\"background-color: rgb(255, 0, 0);\" contenteditable=\"\"><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 style=\"background-color: rgb(255, 0, 0);\" contenteditable=\"\"><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 style=\"background-color: rgb(255, 0, 0);\" contenteditable=\"\"><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 style=\"background-color: rgb(255, 0, 0);\" contenteditable=\"\"><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 style=\"background-color: rgb(255, 0, 0);\" contenteditable=\"\"><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 style=\"background-color: rgb(255, 0, 0);\" contenteditable=\"\"><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 style=\"background-color: rgb(255, 0, 0);\" contenteditable=\"\"><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 style=\"background-color: rgb(0, 255, 0);\" contenteditable=\"\">" +
+ "<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_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..cbf97d8b12
--- /dev/null
+++ b/editor/libeditor/tests/test_contenteditable_text_input_handling.html
@@ -0,0 +1,315 @@
+<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 kLF = !navigator.platform.indexOf("Win") ? "\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_dom_input_event_on_htmleditor.html b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
new file mode 100644
index 0000000000..fdb4dc135d
--- /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) {
+ try {
+ body.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");
+ // XXX If editing host is a descendant of `<body>`, this must be a bug.
+ if (aTestData.cancelBeforeInput) {
+ is(body.getAttribute("dir"), null,
+ `${aDescription}dir attribute of the element shouldn't have been set by ${aTestData.action}`);
+ } else {
+ is(body.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 {
+ body.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) {
+ try {
+ body.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");
+ // XXX If editing host is a descendant of `<body>`, this must be a bug.
+ let expectedDirValue = aTestData.cancelBeforeInput ? "rtl" : "ltr";
+ is(body.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 {
+ body.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..f46e2a3767
--- /dev/null
+++ b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html
@@ -0,0 +1,676 @@
+<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");
+
+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
+ 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);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {osKey: true});
+ check(aDescription + "OS+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);
+
+ reset("");
+ synthesizeKey("KEY_Delete", {osKey: true});
+ check(aDescription + "OS+Delete", true, true, kIsMac);
+
+ // 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");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {osKey: true});
+ check(aDescription + "OS+Return", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "OS+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)");
+
+ reset("a");
+ synthesizeKey("KEY_Tab", {osKey: true});
+ check(aDescription + "OS+Tab", true, true, false);
+ is(aElement.innerHTML, "a", aDescription + "OS+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+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)");
+
+ resetForIndent("<ul><li id=\"target\">ul list item</li></ul>");
+ synthesizeKey("KEY_Tab", {osKey: true});
+ check(aDescription + "OS+Tab on UL", true, true, false);
+ is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>",
+ aDescription + "OS+Tab on UL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+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)");
+
+ resetForIndent("<ol><li id=\"target\">ol list item</li></ol>");
+ synthesizeKey("KEY_Tab", {osKey: true});
+ check(aDescription + "OS+Tab on OL", true, true, false);
+ is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>",
+ aDescription + "OS+Tab on OL");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+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)");
+
+ resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>");
+ synthesizeKey("KEY_Tab", {osKey: true});
+ check(aDescription + "OS+Tab on TD", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>",
+ aDescription + "OS+Tab on TD");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+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)");
+
+ resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>");
+ synthesizeKey("KEY_Tab", {osKey: true});
+ check(aDescription + "OS+Tab on TH", true, true, false);
+ is(aElement.innerHTML,
+ "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>",
+ aDescription + "OS+Tab on TH");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+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);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {osKey: true});
+ check(aDescription + "OS+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 \"");
+ }
+
+ 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;
+ doTest(htmlEditor, "readonly HTML editor", true, true, false);
+
+ // non-tabbable
+ editor.flags = flags & ~(nsIEditor.eEditorAllowInteraction);
+ doTest(htmlEditor, "non-tabbable HTML editor", false, false, false);
+
+ // readonly and non-tabbable
+ editor.flags =
+ (flags | nsIEditor.eEditorReadonlyMask) &
+ ~(nsIEditor.eEditorAllowInteraction);
+ doTest(htmlEditor, "readonly and non-tabbable HTML editor",
+ true, false, false);
+
+ // plaintext
+ editor.flags = flags | nsIEditor.eEditorPlaintextMask;
+ doTest(htmlEditor, "HTML editor but plaintext mode", false, true, true);
+
+ // plaintext and non-tabbable
+ editor.flags = (flags | nsIEditor.eEditorPlaintextMask) &
+ ~(nsIEditor.eEditorAllowInteraction);
+ doTest(htmlEditor, "non-tabbable HTML editor but plaintext mode",
+ false, false, true);
+
+
+ // readonly and plaintext
+ editor.flags = flags | nsIEditor.eEditorPlaintextMask |
+ nsIEditor.eEditorReadonlyMask;
+ 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);
+ 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_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..67bad26965
--- /dev/null
+++ b/editor/libeditor/tests/test_inlineTableEditing.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>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="editable1" contenteditable="true">
+<table id="table1">
+<tr id="tr1"><td>ABCDEFG</td><td>HIJKLMN</td></tr>
+<tr id="tr2"><td>ABCDEFG</td><td>HIJKLMN</td></tr>
+<tr id="tr3"><td>ABCDEFG</td><td>HIJKLMN</td></tr>
+</table>
+</div>
+<pre id="test">
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ document.execCommand("enableObjectResizing", false, "false");
+ document.execCommand("enableInlineTableEditing", false, "true");
+
+ let tableNode = document.getElementById("table1");
+ synthesizeMouseAtCenter(tableNode, {});
+ isnot(tableNode.getAttribute("_moz_resizing"), "true",
+ "_moz_resizing attribute shouldn't be true without object resizing");
+
+ let tr2 = document.getElementById("tr2");
+ ok(tr2, "id=tr2 should exist");
+ synthesizeMouse(tr2, 0, tr2.clientHeight / 2, {});
+ ok(!document.getElementById("tr2"),
+ "id=tr2 should be removed by a click in the row");
+
+ 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..ecc8dfd884
--- /dev/null
+++ b/editor/libeditor/tests/test_insertParagraph_in_h2_and_li.html
@@ -0,0 +1,172 @@
+<!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;
+const isTraditionalSplitDirection =!SpecialPowers.getBoolPref(
+ "editor.join_split_direction.compatible_with_the_other_browsers"
+);
+
+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 isTraditionalSplitDirection
+ ? element.previousElementSibling
+ : 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_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..79faab4806
--- /dev/null
+++ b/editor/libeditor/tests/test_join_split_node_direction_change_command.html
@@ -0,0 +1,197 @@
+<!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();
+ await SpecialPowers.pushPrefEnv({
+ set: [["editor.join_split_direction.compatible_with_the_other_browsers", false]],
+ });
+ (function test_command_when_legacy_behavior_is_enabled_by_default() {
+ iframe.contentDocument.body.innerHTML = "<div contenteditable><br></div>";
+ ok(
+ iframe.contentDocument.queryCommandSupported("enableCompatibleJoinSplitDirection"),
+ "test_command_when_legacy_behavior_is_enabled_by_default: command should be supported"
+ );
+ ok(
+ iframe.contentDocument.queryCommandEnabled("enableCompatibleJoinSplitDirection"),
+ "test_command_when_legacy_behavior_is_enabled_by_default: command should be enabled"
+ );
+ ok(
+ !iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"),
+ "test_command_when_legacy_behavior_is_enabled_by_default: command state should be false"
+ );
+ is(
+ iframe.contentDocument.queryCommandValue("enableCompatibleJoinSplitDirection"),
+ "",
+ "test_command_when_legacy_behavior_is_enabled_by_default: command value should be empty string"
+ );
+ ok(
+ iframe.contentDocument.execCommand("enableCompatibleJoinSplitDirection", false, "true"),
+ "test_command_when_legacy_behavior_is_enabled_by_default: command to enable it should return true"
+ );
+ })();
+
+ (function test_command_when_enabling_new_behavior_when_legacy_one_is_enabled_by_default() {
+ ok(
+ iframe.contentDocument.queryCommandSupported("enableCompatibleJoinSplitDirection"),
+ "test_command_when_enabling_new_behavior_when_legacy_one_is_enabled_by_default: command should be supported"
+ );
+ ok(
+ iframe.contentDocument.queryCommandEnabled("enableCompatibleJoinSplitDirection"),
+ "test_command_when_enabling_new_behavior_when_legacy_one_is_enabled_by_default: command should be enabled"
+ );
+ ok(
+ iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"),
+ "test_command_when_enabling_new_behavior_when_legacy_one_is_enabled_by_default: command state should be true"
+ );
+ is(
+ iframe.contentDocument.queryCommandValue("enableCompatibleJoinSplitDirection"),
+ "",
+ "test_command_when_enabling_new_behavior_when_legacy_one_is_enabled_by_default: command value should be empty string"
+ );
+
+ ok(
+ iframe.contentDocument.execCommand("enableCompatibleJoinSplitDirection", false, "false"),
+ "test_command_when_enabling_new_behavior_when_legacy_one_is_enabled_by_default: command to disable it should return true"
+ );
+ })();
+
+ (function test_command_when_disabling_new_behavior_when_the_legacy_one_is_enabled_by_default() {
+ ok(
+ iframe.contentDocument.queryCommandSupported("enableCompatibleJoinSplitDirection"),
+ "test_command_when_disabling_new_behavior_when_the_legacy_one_is_enabled_by_default: command should be supported"
+ );
+ ok(
+ iframe.contentDocument.queryCommandEnabled("enableCompatibleJoinSplitDirection"),
+ "test_command_when_disabling_new_behavior_when_the_legacy_one_is_enabled_by_default: command should be enabled"
+ );
+ ok(
+ !iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"),
+ "test_command_when_disabling_new_behavior_when_the_legacy_one_is_enabled_by_default: command state should be false"
+ );
+ is(
+ iframe.contentDocument.queryCommandValue("enableCompatibleJoinSplitDirection"),
+ "",
+ "test_command_when_disabling_new_behavior_when_the_legacy_one_is_enabled_by_default: command value should be empty string"
+ );
+ })();
+
+ await resetIframe();
+ await SpecialPowers.pushPrefEnv({
+ set: [["editor.join_split_direction.compatible_with_the_other_browsers", true]],
+ });
+ (function test_command_when_new_behavior_is_enabled_by_default() {
+ iframe.contentDocument.body.innerHTML = "<div contenteditable><br></div>";
+ ok(
+ iframe.contentDocument.queryCommandSupported("enableCompatibleJoinSplitDirection"),
+ "test_command_when_new_behavior_is_enabled_by_default: command should be supported"
+ );
+ ok(
+ iframe.contentDocument.queryCommandEnabled("enableCompatibleJoinSplitDirection"),
+ "test_command_when_new_behavior_is_enabled_by_default: command should be enabled"
+ );
+ ok(
+ iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"),
+ "test_command_when_new_behavior_is_enabled_by_default: command state should be true"
+ );
+ is(
+ iframe.contentDocument.queryCommandValue("enableCompatibleJoinSplitDirection"),
+ "",
+ "test_command_when_new_behavior_is_enabled_by_default: command value should be empty string"
+ );
+ ok(
+ !iframe.contentDocument.execCommand("enableCompatibleJoinSplitDirection", false, "false"),
+ "test_command_when_new_behavior_is_enabled_by_default: command to disable it should return false"
+ );
+ ok(
+ iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"),
+ "test_command_when_new_behavior_is_enabled_by_default: command state should be true even after executing the command to disable it"
+ );
+ })();
+
+ await resetIframe();
+ await SpecialPowers.pushPrefEnv({
+ set: [["editor.join_split_direction.compatible_with_the_other_browsers", false]],
+ });
+ (function test_command_disabled_after_joining_nodes() {
+ iframe.contentDocument.body.innerHTML = "<div contenteditable><p>abc</p><p>def</p></div>";
+ iframe.contentWindow.getSelection().collapse(iframe.contentDocument.querySelector("p + p").firstChild, 0);
+ iframe.contentDocument.execCommand("delete");
+ ok(
+ !iframe.contentDocument.execCommand("enableCompatibleJoinSplitDirection", false, "true"),
+ "test_command_disabled_after_joining_nodes: command should return false"
+ );
+ ok(
+ !iframe.contentDocument.queryCommandEnabled("enableCompatibleJoinSplitDirection"),
+ "test_command_when_new_behavior_is_enabled_by_default: command should be disabled"
+ );
+ ok(
+ !iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"),
+ "test_command_when_new_behavior_is_enabled_by_default: command state should be false"
+ );
+ })();
+
+ await resetIframe();
+ await SpecialPowers.pushPrefEnv({
+ set: [["editor.join_split_direction.compatible_with_the_other_browsers", false]],
+ });
+ (function test_command_disabled_after_splitting_node() {
+ iframe.contentDocument.body.innerHTML = "<div contenteditable><p>abcdef</p></div>";
+ iframe.contentWindow.getSelection().collapse(iframe.contentDocument.querySelector("p").firstChild, "abc".length);
+ iframe.contentDocument.execCommand("insertParagraph");
+ ok(
+ !iframe.contentDocument.execCommand("enableCompatibleJoinSplitDirection", false, "true"),
+ "test_command_disabled_after_splitting_node: command should return false"
+ );
+ ok(
+ !iframe.contentDocument.queryCommandEnabled("enableCompatibleJoinSplitDirection"),
+ "test_command_disabled_after_splitting_node: command should be disabled"
+ );
+ ok(
+ !iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"),
+ "test_command_disabled_after_splitting_node: command state should be false"
+ );
+ })();
+
+ await resetIframe();
+ await SpecialPowers.pushPrefEnv({
+ set: [["editor.join_split_direction.compatible_with_the_other_browsers", false]],
+ });
+ (function test_split_direction_after_enabling_new_direction() {
+ iframe.contentDocument.body.innerHTML = "<div contenteditable><p>abc</p><p>def</p></div>";
+ const rightP = iframe.contentDocument.querySelector("p + p");
+ iframe.contentWindow.getSelection().collapse(rightP.firstChild, 0);
+ iframe.contentDocument.execCommand("delete");
+ iframe.contentDocument.execCommand("enableCompatibleJoinSplitDirection", false, "true");
+ is(
+ iframe.contentDocument.querySelector("p"),
+ rightP,
+ "test_split_direction_after_enabling_new_direction: left paragraph should be deleted and right paragraph should be alive"
+ );
+ })();
+
+ 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_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_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_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..afb441dc43
--- /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 rowspan="2" valign="top"><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..77671666c7
--- /dev/null
+++ b/editor/libeditor/tests/test_paste_no_formatting.html
@@ -0,0 +1,218 @@
+<!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 });
+
+ const isHeadless = await SpecialPowers.spawnChrome([], () => {
+ return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless;
+ });
+
+ 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,
+ isHeadless ? expectedText : 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,
+ isHeadless ? "" : 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..9319a34139
--- /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.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ 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..f0d37d8781
--- /dev/null
+++ b/editor/libeditor/tests/test_texteditor_keyevent_handling.html
@@ -0,0 +1,349 @@
+<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");
+
+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);
+
+ 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);
+
+ reset("");
+ synthesizeKey("KEY_Backspace", {osKey: true});
+ check(aDescription + "OS+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);
+
+ reset("");
+ synthesizeKey("KEY_Delete", {osKey: true});
+ check(aDescription + "OS+Delete",
+ true, true, kIsMac);
+
+ // 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");
+
+ reset("a");
+ synthesizeKey("KEY_Enter", {osKey: true});
+ check(aDescription + "OS+Return", true, true, false);
+ is(aElement.value, "a", aDescription + "OS+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)");
+
+ reset("a");
+ synthesizeKey("KEY_Tab", {osKey: true});
+ check(aDescription + "OS+Tab", true, true, false);
+ is(aElement.value, "a", aDescription + "OS+Tab");
+ is(SpecialPowers.unwrap(fm.focusedElement), aElement,
+ aDescription + "focus moved unexpectedly (OS+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);
+
+ reset("abc");
+ synthesizeKey("KEY_Escape", {osKey: true});
+ check(aDescription + "OS+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 \"");
+ }
+
+ doTest(inputField, "<input type=\"text\">", true, false);
+
+ inputField.setAttribute("readonly", "readonly");
+ doTest(inputField, "<input type=\"text\" readonly>", true, true);
+
+ doTest(passwordField, "<input type=\"password\">", true, false);
+
+ passwordField.setAttribute("readonly", "readonly");
+ doTest(passwordField, "<input type=\"password\" readonly>", true, true);
+
+ doTest(textarea, "<textarea>", false, false);
+
+ textarea.setAttribute("readonly", "readonly");
+ 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..3cbc007009
--- /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.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ 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>
diff --git a/editor/moz.build b/editor/moz.build
new file mode 100644
index 0000000000..5b526d4f1d
--- /dev/null
+++ b/editor/moz.build
@@ -0,0 +1,41 @@
+# -*- 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/.
+
+DIRS += [
+ "libeditor",
+ "spellchecker",
+ "txmgr",
+ "composer",
+]
+
+XPIDL_SOURCES += [
+ "nsIDocumentStateListener.idl",
+ "nsIEditActionListener.idl",
+ "nsIEditor.idl",
+ "nsIEditorMailSupport.idl",
+ "nsIEditorSpellCheck.idl",
+ "nsIHTMLAbsPosEditor.idl",
+ "nsIHTMLEditor.idl",
+ "nsIHTMLInlineTableEditor.idl",
+ "nsIHTMLObjectResizer.idl",
+ "nsITableEditor.idl",
+]
+
+XPIDL_MODULE = "editor"
+
+TESTING_JS_MODULES += [
+ "AsyncSpellCheckTestHelper.sys.mjs",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Editor")
+
+SPHINX_TREES["/editor"] = "docs"
+
+with Files("docs/**"):
+ SCHEDULES.exclusive = ["docs"]
+
+CXXFLAGS += CONFIG["MOZ_TRIVIAL_AUTO_VAR_INIT"]
diff --git a/editor/nsIDocumentStateListener.idl b/editor/nsIDocumentStateListener.idl
new file mode 100644
index 0000000000..ab1d1cdf2f
--- /dev/null
+++ b/editor/nsIDocumentStateListener.idl
@@ -0,0 +1,32 @@
+/* -*- 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 "nsISupports.idl"
+
+/**
+ * Due to the historical reason, this listener interface says "document state",
+ * but this listener listens to HTML editor state.
+ */
+[scriptable, uuid(050cdc00-3b8e-11d3-9ce4-a458f454fcbc)]
+interface nsIDocumentStateListener : nsISupports
+{
+ /**
+ * NotifyDocumentWillBeDestroyed() is called when HTML editor instance is
+ * being destroyed. Note that related objects may have already gone when
+ * this is called because that may cause destroying HTML editor.
+ */
+ [can_run_script]
+ void NotifyDocumentWillBeDestroyed();
+
+ /**
+ * NotifyDocumentStateChanged() is called when dirty state of HTML editor
+ * is changed.
+ *
+ * @param aNowDirty if true, this is called when the HTML editor becomes
+ * dirty. Otherwise, called when it becomes not dirty.
+ */
+ [can_run_script]
+ void NotifyDocumentStateChanged(in boolean aNowDirty);
+};
diff --git a/editor/nsIEditActionListener.idl b/editor/nsIEditActionListener.idl
new file mode 100644
index 0000000000..d959c19e4f
--- /dev/null
+++ b/editor/nsIEditActionListener.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+webidl CharacterData;
+webidl Node;
+webidl Range;
+
+%{C++
+#include "mozilla/EditorDOMPoint.h"
+class nsINode;
+class nsIContent;
+%}
+
+[ref] native EditorRawDOMPointRef(mozilla::EditorDOMPointBase<nsINode*, nsIContent*>);
+
+/*
+Editor Action Listener interface to outside world
+*/
+
+
+/**
+ * A generic editor action listener interface.
+ * <P>
+ * nsIEditActionListener is the interface used by applications wishing to be notified
+ * when the editor modifies the DOM tree.
+ *
+ * Note: this is the wrong class to implement if you are interested in generic
+ * change notifications. For generic notifications, you should implement
+ * nsIDocumentObserver.
+ */
+[scriptable, uuid(b22907b1-ee93-11d2-8d50-000064657374)]
+interface nsIEditActionListener : nsISupports
+{
+ /**
+ * Called after the editor deletes a node.
+ * @param aChild The node to delete
+ * @param aResult The result of the delete node operation.
+ */
+ void DidDeleteNode(in Node aChild, in nsresult aResult);
+
+ /**
+ * Called after the editor joins 2 nodes.
+ * @param aJoinedPoint The joined point. If aLeftNodeWasRemoved is true,
+ * it points after inserted left node content in the
+ * right node. Otherwise, it points start of inserted
+ * right node content in the left node.
+ * @param aRemovedNode The removed node.
+ * @param aLeftNodeWasRemoved
+ * true if left node is removed and its contents were
+ * moved into start of the right node.
+ * false if right node is removed and its contents were
+ * moved into end of the left node.
+ */
+ [noscript]
+ void DidJoinContents([const] in EditorRawDOMPointRef aJoinedPoint,
+ [const] in Node aRemovedNode,
+ in bool aLeftNodeWasRemoved);
+
+ /**
+ * Called after the editor inserts text.
+ * @param aTextNode This node getting inserted text.
+ * @param aOffset The offset in aTextNode to insert at.
+ * @param aString The string that gets inserted.
+ * @param aResult The result of the insert text operation.
+ */
+ void DidInsertText(in CharacterData aTextNode,
+ in long aOffset,
+ in AString aString,
+ in nsresult aResult);
+
+ /**
+ * Called before the editor deletes text.
+ * @param aTextNode This node getting text deleted.
+ * @param aOffset The offset in aTextNode to delete at.
+ * @param aLength The amount of text to delete.
+ */
+ void WillDeleteText(in CharacterData aTextNode,
+ in long aOffset,
+ in long aLength);
+
+ /**
+ * Called before the editor deletes the ranges.
+ * @param aRangesToDelete The ranges to be deleted.
+ */
+ void WillDeleteRanges(in Array<Range> aRangesToDelete);
+};
diff --git a/editor/nsIEditor.idl b/editor/nsIEditor.idl
new file mode 100644
index 0000000000..c32b06c605
--- /dev/null
+++ b/editor/nsIEditor.idl
@@ -0,0 +1,678 @@
+/* -*- 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/. */
+
+#include "nsISupports.idl"
+#include "domstubs.idl"
+
+%{C++
+#include "mozilla/Debug.h"
+%}
+
+interface nsISelectionController;
+interface nsIDocumentStateListener;
+interface nsIEditActionListener;
+interface nsIInlineSpellChecker;
+interface nsITransferable;
+
+webidl Document;
+webidl Element;
+webidl Node;
+webidl Selection;
+
+%{C++
+namespace mozilla {
+class EditorBase;
+class HTMLEditor;
+class TextEditor;
+} // namespace mozilla
+%}
+
+[scriptable, builtinclass, uuid(094be624-f0bf-400f-89e2-6a84baab9474)]
+interface nsIEditor : nsISupports
+{
+%{C++
+ typedef short EDirection;
+ typedef short EStripWrappers;
+%}
+ const short eNone = 0;
+ const short eNext = 1;
+ const short ePrevious = 2;
+ const short eNextWord = 3;
+ const short ePreviousWord = 4;
+ const short eToBeginningOfLine = 5;
+ const short eToEndOfLine = 6;
+
+%{C++
+ static bool EDirectionIsValid(EDirection aDirectionAndAmount) {
+ return aDirectionAndAmount == nsIEditor::eNone ||
+ aDirectionAndAmount == nsIEditor::eNext ||
+ aDirectionAndAmount == nsIEditor::ePrevious ||
+ aDirectionAndAmount == nsIEditor::eNextWord ||
+ aDirectionAndAmount == nsIEditor::ePreviousWord ||
+ aDirectionAndAmount == nsIEditor::eToBeginningOfLine ||
+ aDirectionAndAmount == nsIEditor::eToEndOfLine;
+ }
+ static bool EDirectionIsValidExceptNone(EDirection aDirectionAndAmount) {
+ return aDirectionAndAmount != nsIEditor::eNone &&
+ EDirectionIsValid(aDirectionAndAmount);
+ }
+
+ /**
+ * Return true if nsIEditor::EDirection value means the direction of pressing
+ * `Backspace` key.
+ */
+ [[nodiscard]] static bool DirectionIsBackspace(
+ EDirection aDirectionAndAmount) {
+ MOZ_ASSERT(EDirectionIsValid(aDirectionAndAmount));
+ return aDirectionAndAmount == nsIEditor::ePrevious ||
+ aDirectionAndAmount == nsIEditor::ePreviousWord ||
+ aDirectionAndAmount == nsIEditor::eToBeginningOfLine;
+ }
+
+ /**
+ * Return true if nsIEditor::EDirection value means the direction of pressing
+ * `Delete` key (forwardDelete).
+ */
+ [[nodiscard]] static bool DirectionIsDelete(
+ EDirection aDirectionAndAmount) {
+ MOZ_ASSERT(EDirectionIsValid(aDirectionAndAmount));
+ return aDirectionAndAmount == nsIEditor::eNext ||
+ aDirectionAndAmount == nsIEditor::eNextWord ||
+ aDirectionAndAmount == nsIEditor::eToEndOfLine;
+ }
+%}
+
+ const short eStrip = 0;
+ const short eNoStrip = 1;
+
+ // If you want an HTML editor to behave as a plaintext editor, specify this
+ // flag. Note that this is always set if the instance is a text editor.
+ const long eEditorPlaintextMask = 0x0001;
+ // We don't support single line editor mode with HTML editors. Therefore,
+ // don't specify this for HTML editor.
+ const long eEditorSingleLineMask = 0x0002;
+ // We don't support password editor mode with HTML editors. Therefore,
+ // don't specify this for HTML editor.
+ const long eEditorPasswordMask = 0x0004;
+ // When the editor should be in readonly mode (currently, same as "disabled"),
+ // you can specify this flag with any editor instances.
+ // NOTE: Setting this flag does not change the style of editor. This just
+ // changes the internal editor's readonly state.
+ // NOTE: The readonly mode does NOT block XPCOM APIs which modify the editor
+ // content. This just blocks edit operations from user input and editing
+ // commands (both HTML Document.execCommand and the XUL commands).
+ // FIXME: XPCOM methods of TextEditor may be blocked by this flag. If you
+ // find it, file a bug.
+ const long eEditorReadonlyMask = 0x0008;
+ // If you want an HTML editor to work as an email composer, specify this flag.
+ // And you can specify this to text editor too for making spellchecker for
+ // the text editor should work exactly same as email composer's.
+ const long eEditorMailMask = 0x0020;
+ // allow the editor to set font: monospace on the root node
+ const long eEditorEnableWrapHackMask = 0x0040;
+ // If you want to move focus from an HTML editor with tab navigation,
+ // specify this flag. This is not available with text editors becase
+ // it's always tabbable.
+ // Note that if this is not specified, link navigation is also enabled in
+ // the editable content.
+ const long eEditorAllowInteraction = 0x0200;
+ // when this flag is set, the internal direction of the editor is RTL.
+ // if neither of the direction flags are set, the direction is determined
+ // from the text control's content node.
+ const long eEditorRightToLeft = 0x0800;
+ // when this flag is set, the internal direction of the editor is LTR.
+ const long eEditorLeftToRight = 0x1000;
+ // when this flag is set, the editor's text content is not spell checked.
+ const long eEditorSkipSpellCheck = 0x2000;
+
+ /*
+ * The valid values for newlines handling.
+ * Can't change the values unless we remove
+ * use of the pref.
+ */
+ const long eNewlinesPasteIntact = 0;
+ const long eNewlinesPasteToFirst = 1;
+ const long eNewlinesReplaceWithSpaces = 2;
+ const long eNewlinesStrip = 3;
+ const long eNewlinesReplaceWithCommas = 4;
+ const long eNewlinesStripSurroundingWhitespace = 5;
+
+ readonly attribute Selection selection;
+
+ [can_run_script]
+ void setAttributeOrEquivalent(in Element element,
+ in AString sourceAttrName,
+ in AString sourceAttrValue,
+ in boolean aSuppressTransaction);
+ [can_run_script]
+ void removeAttributeOrEquivalent(in Element element,
+ in AString sourceAttrName,
+ in boolean aSuppressTransaction);
+
+ /** edit flags for this editor. May be set at any time. */
+ [setter_can_run_script] attribute unsigned long flags;
+
+ /**
+ * the MimeType of the document
+ */
+ attribute AString contentsMIMEType;
+
+ /** Returns true if we have a document that is not marked read-only */
+ readonly attribute boolean isDocumentEditable;
+
+ /** Returns true if the current selection anchor is editable */
+ readonly attribute boolean isSelectionEditable;
+
+ /**
+ * the DOM Document this editor is associated with, refcounted.
+ */
+ readonly attribute Document document;
+
+ /** the body element, i.e. the root of the editable document.
+ */
+ readonly attribute Element rootElement;
+
+ /**
+ * the selection controller for the current presentation, refcounted.
+ */
+ readonly attribute nsISelectionController selectionController;
+
+
+ /* ------------ Selected content removal -------------- */
+
+ /**
+ * DeleteSelection removes all nodes in the current selection.
+ * @param aDir if eNext, delete to the right (for example, the DEL key)
+ * if ePrevious, delete to the left (for example, the BACKSPACE key)
+ * @param stripWrappers If eStrip, strip any empty inline elements left
+ * behind after the deletion; if eNoStrip, don't. If in
+ * doubt, pass eStrip -- eNoStrip is only for if you're
+ * about to insert text or similar right after.
+ */
+ [can_run_script]
+ void deleteSelection(in short action, in short stripWrappers);
+
+
+ /* ------------ Document info and file methods -------------- */
+
+ /** Returns true if the document has no *meaningful* content */
+ readonly attribute boolean documentIsEmpty;
+
+ /** Returns true if the document is modifed and needs saving */
+ readonly attribute boolean documentModified;
+
+ /**
+ * Sets document's character set. This is available only when the editor
+ * instance is an HTMLEditor since it's odd to change character set of
+ * parent document of `<input>` and `<textarea>`.
+ */
+ [setter_can_run_script]
+ attribute ACString documentCharacterSet;
+
+ /** to be used ONLY when we need to override the doc's modification
+ * state (such as when it's saved).
+ */
+ [can_run_script]
+ void resetModificationCount();
+
+ /** Gets the modification count of the document we are editing.
+ * @return the modification count of the document being edited.
+ * Zero means unchanged.
+ */
+ long getModificationCount();
+
+ /** called each time we modify the document.
+ * Increments the modification count of the document.
+ * @param aModCount the number of modifications by which
+ * to increase or decrease the count
+ */
+ [can_run_script]
+ void incrementModificationCount(in long aModCount);
+
+ /* ------------ Transaction methods -------------- */
+
+ /** turn the undo system on or off
+ * @param aEnable if PR_TRUE, the undo system is turned on if available
+ * if PR_FALSE the undo system is turned off if it
+ * was previously on
+ * @return if aEnable is PR_TRUE, returns NS_OK if
+ * the undo system could be initialized properly
+ * if aEnable is PR_FALSE, returns NS_OK.
+ */
+ void enableUndo(in boolean enable);
+
+ /**
+ * Returns true when undo/redo is enabled (by default).
+ */
+ [infallible] readonly attribute boolean undoRedoEnabled;
+
+ /**
+ * Retruns true when undo/redo is enabled and there is one or more transaction
+ * in the undo stack.
+ */
+ [infallible] readonly attribute boolean canUndo;
+
+ /**
+ * Returns true when undo/redo is enabled and there is one or more transaction
+ * in the redo stack.
+ */
+ [infallible] readonly attribute boolean canRedo;
+
+ /**
+ * Clears the transactions both for undo and redo.
+ * This may fail if you call this while editor is handling something, i.e.,
+ * don't call this from a legacy mutation event listeners, then, you won't
+ * see any exceptions.
+ */
+ [binaryname(ClearUndoRedoXPCOM)]
+ void clearUndoRedo();
+
+ /**
+ * Undo the topmost transaction in the undo stack.
+ * This may throw exception when this is called while editor is handling
+ * transactions.
+ */
+ [can_run_script]
+ void undo();
+
+ /**
+ * Undo all transactions in the undo stack.
+ * This may throw exception when this is called while editor is handling
+ * transactions.
+ */
+ [can_run_script]
+ void undoAll();
+
+ /**
+ * Redo the topmost transaction in the redo stack.
+ * This may throw exception when this is called while editor is handling
+ * transactions.
+ */
+ [can_run_script]
+ void redo();
+
+ /** beginTransaction is a signal from the caller to the editor that
+ * the caller will execute multiple updates to the content tree
+ * that should be treated as a single logical operation,
+ * in the most efficient way possible.<br>
+ * All transactions executed between a call to beginTransaction and
+ * endTransaction will be undoable as an atomic action.<br>
+ * endTransaction must be called after beginTransaction.<br>
+ * Calls to beginTransaction can be nested, as long as endTransaction
+ * is called once per beginUpdate.
+ */
+ [can_run_script]
+ void beginTransaction();
+
+ /** endTransaction is a signal to the editor that the caller is
+ * finished updating the content model.<br>
+ * beginUpdate must be called before endTransaction is called.<br>
+ * Calls to beginTransaction can be nested, as long as endTransaction
+ * is called once per beginTransaction.
+ */
+ [can_run_script]
+ void endTransaction();
+
+ /**
+ * While setting the flag with this method to false, DeleteRangeTransaction,
+ * DeleteTextTransaction, InsertNodeTransaction, InsertTextTransaction and
+ * SplitNodeTransaction won't change Selection after modifying the DOM tree.
+ * Note that calling this with false does not guarantee that Selection won't
+ * be changed because other transaction may ignore this flag, editor itself
+ * may change selection, and current selection may become invalid after
+ * changing the DOM tree, etc.
+ * After calling this method with true, the caller should guarantee that
+ * Selection should be positioned where user expects.
+ *
+ * @param should false if you don't want above transactions to modify
+ * Selection automatically after modifying the DOM tree.
+ * Note that calling this with false does not guarantee
+ * that Selection is never changed.
+ */
+ void setShouldTxnSetSelection(in boolean should);
+
+ /* ------------ Inline Spell Checking methods -------------- */
+
+ /** Returns the inline spell checker associated with this object. The spell
+ * checker is lazily created, so this function may create the object for
+ * you during this call.
+ * @param autoCreate If true, this will create a spell checker object
+ * if one does not exist yet for this editor. If false
+ * and the object has not been created, this function
+ * WILL RETURN NULL.
+ */
+ nsIInlineSpellChecker getInlineSpellChecker(in boolean autoCreate);
+
+ /** Called when the user manually overrides the spellchecking state for this
+ * editor.
+ * @param enable The new state of spellchecking in this editor, as
+ * requested by the user.
+ */
+ void setSpellcheckUserOverride(in boolean enable);
+
+ /* ------------ Clipboard methods -------------- */
+
+ /** cut the currently selected text, putting it into the OS clipboard
+ * What if no text is selected?
+ * What about mixed selections?
+ * What are the clipboard formats?
+ */
+ [can_run_script]
+ void cut();
+
+ /**
+ * canCut() returns true if selected content is allowed to be copied to the
+ * clipboard and to be removed.
+ * Note that this always returns true if the editor is in a non-chrome
+ * HTML/XHTML document.
+ * FYI: Current user in script is only BlueGriffon.
+ */
+ [can_run_script]
+ boolean canCut();
+
+ /** copy the currently selected text, putting it into the OS clipboard
+ * What if no text is selected?
+ * What about mixed selections?
+ * What are the clipboard formats?
+ */
+ [can_run_script]
+ void copy();
+
+ /**
+ * canCopy() returns true if selected content is allowed to be copied to
+ * the clipboard.
+ * Note that this always returns true if the editor is in a non-chrome
+ * HTML/XHTML document.
+ * FYI: Current user in script is only BlueGriffon.
+ */
+ [can_run_script]
+ boolean canCopy();
+
+ /** paste the text in the OS clipboard at the cursor position, replacing
+ * the selected text (if any)
+ */
+ [can_run_script]
+ void paste(in long aClipboardType);
+
+ /** Paste the text in |aTransferable| at the cursor position, replacing the
+ * selected text (if any).
+ */
+ [can_run_script]
+ void pasteTransferable(in nsITransferable aTransferable);
+
+ /** Can we paste? True if the doc is modifiable, and we have
+ * pasteable data in the clipboard.
+ */
+ boolean canPaste(in long aClipboardType);
+
+ /* ------------ Selection methods -------------- */
+
+ /** sets the document selection to the entire contents of the document */
+ [can_run_script]
+ void selectAll();
+
+ /**
+ * Collapses selection at start of the document. If it's an HTML editor,
+ * collapses selection at start of current editing host (<body> element if
+ * it's in designMode) instead. If there is a non-editable node before any
+ * editable text nodes or inline elements which can have text nodes as their
+ * children, collapses selection at start of the editing host. If there is
+ * an editable text node which is not collapsed, collapses selection at
+ * 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.
+ */
+ [can_run_script]
+ void beginningOfDocument();
+
+ /**
+ * Sets the selection to the end of the last leaf child/descendant or the root
+ * element.
+ */
+ [can_run_script]
+ void endOfDocument();
+
+ /* ------------ Node manipulation methods -------------- */
+
+ /**
+ * setAttribute() sets the attribute of aElement.
+ * No checking is done to see if aAttribute is a legal attribute of the node,
+ * or if aValue is a legal value of aAttribute.
+ *
+ * @param aElement the content element to operate on
+ * @param aAttribute the string representation of the attribute to set
+ * @param aValue the value to set aAttribute to
+ */
+ [can_run_script]
+ void setAttribute(in Element aElement, in AString attributestr,
+ in AString attvalue);
+
+ /**
+ * removeAttribute() deletes aAttribute from the attribute list of aElement.
+ * If aAttribute is not an attribute of aElement, nothing is done.
+ *
+ * @param aElement the content element to operate on
+ * @param aAttribute the string representation of the attribute to get
+ */
+ [can_run_script]
+ void removeAttribute(in Element aElement,
+ in AString aAttribute);
+
+ /**
+ * cloneAttributes() is similar to Node::cloneNode(),
+ * it assures the attribute nodes of the destination are identical
+ * with the source node by copying all existing attributes from the
+ * source and deleting those not in the source.
+ * This is used when the destination element already exists
+ *
+ * @param aDestNode the destination element to operate on
+ * @param aSourceNode the source element to copy attributes from
+ */
+ [can_run_script]
+ void cloneAttributes(in Element aDestElement, in Element aSourceElement);
+
+ /**
+ * insertNode inserts aNode into aParent at aPosition.
+ * No checking is done to verify the legality of the insertion.
+ * That is the responsibility of the caller.
+ * @param aNode The DOM Node to insert.
+ * @param aParent The node to insert the new object into
+ * @param aPosition The place in aParent to insert the new node
+ * 0=first child, 1=second child, etc.
+ * any number > number of current children = last child
+ */
+ [can_run_script]
+ void insertNode(in Node node,
+ in Node parent,
+ in unsigned long aPosition);
+
+
+ /**
+ * deleteNode removes aChild from aParent.
+ * @param aChild The node to delete
+ */
+ [can_run_script]
+ void deleteNode(in Node child);
+
+/* ------------ Output methods -------------- */
+
+ /**
+ * Output methods:
+ * aFormatType is a mime type, like text/plain.
+ */
+ AString outputToString(in AString formatType,
+ in unsigned long flags);
+
+ /* ------------ Various listeners methods --------------
+ * nsIEditor holds strong references to the editor observers, action listeners
+ * and document state listeners.
+ */
+
+ /** add an EditActionListener to the editors list of listeners. */
+ void addEditActionListener(in nsIEditActionListener listener);
+
+ /** Remove an EditActionListener from the editor's list of listeners. */
+ void removeEditActionListener(in nsIEditActionListener listener);
+
+ /** Add a DocumentStateListener to the editors list of doc state listeners. */
+ void addDocumentStateListener(in nsIDocumentStateListener listener);
+
+ /** Remove a DocumentStateListener to the editors list of doc state listeners. */
+ void removeDocumentStateListener(in nsIDocumentStateListener listener);
+
+ /**
+ * forceCompositionEnd() force the composition end
+ */
+ void forceCompositionEnd();
+
+ /**
+ * whether this editor has active IME transaction
+ */
+ readonly attribute boolean composing;
+
+ /**
+ * unmask() is available only when the editor is a passwrod field. This
+ * unmasks characters in specified by aStart and aEnd. If there have
+ * already unmasked characters, they are masked when this is called.
+ * Note that if you calls this without non-zero `aTimeout`, you bear
+ * responsibility for masking password with calling `mask()`. I.e.,
+ * user inputting password won't be masked automacitally. If user types
+ * a new character and echo is enabled, unmasked range is expanded to
+ * including it.
+ *
+ * @param aStart Optional, first index to show the character. If you
+ * specify middle of a surrogate pair, this expands the
+ * range to include the prceding high surrogate
+ * automatically.
+ * If omitted, it means that all characters of the
+ * password becomes unmasked.
+ * @param aEnd Optional, next index of last unmasked character. If
+ * you specify middle of a surrogate pair, the expands
+ * the range to include the following low surrogate.
+ * If omitted or negative value, it means unmasking all
+ * characters after aStart. Specifying same index
+ * throws an exception.
+ * @param aTimeout Optional, specify milliseconds to hide the unmasked
+ * characters if you want to show them temporarily.
+ * If omitted or 0, it means this won't mask the characters
+ * automatically.
+ */
+ [can_run_script, optional_argc] void unmask(
+ [optional] in unsigned long aStart,
+ [optional] in long long aEnd,
+ [optional] in unsigned long aTimeout);
+
+ /**
+ * mask() is available only when the editor is a password field. This masks
+ * all unmasked characters immediately.
+ */
+ [can_run_script] void mask();
+
+ /**
+ * These attributes are available only when the editor is a password field.
+ * unmaskedStart is first unmasked character index, or 0 if there is no
+ * unmasked characters.
+ * unmaskedEnd is next index of the last unmasked character. 0 means there
+ * is no unmasked characters.
+ */
+ readonly attribute unsigned long unmaskedStart;
+ readonly attribute unsigned long unmaskedEnd;
+
+ /**
+ * autoMaskingEnabled is true if unmasked range and newly inputted characters
+ * are masked automatically. That's the default state. If false, until
+ * `mask()` is called, unmasked range and newly inputted characters are
+ * unmasked.
+ */
+ readonly attribute boolean autoMaskingEnabled;
+
+ /**
+ * passwordMask attribute is a mask character which is used to mask password.
+ */
+ readonly attribute AString passwordMask;
+
+ /**
+ * The length of the contents in characters.
+ */
+ readonly attribute unsigned long textLength;
+
+ /** Get and set the body wrap width.
+ *
+ * Special values:
+ * 0 = wrap to window width
+ * -1 = no wrap at all
+ */
+ attribute long wrapWidth;
+
+ /** Get and set newline handling.
+ *
+ * Values are the constants defined above.
+ */
+ attribute long newlineHandling;
+
+ /**
+ * Inserts a string at the current location,
+ * given by the selection.
+ * If the selection is not collapsed, the selection is deleted
+ * and the insertion takes place at the resulting collapsed selection.
+ *
+ * @param aString the string to be inserted
+ */
+ [can_run_script]
+ void insertText(in AString aStringToInsert);
+
+ /**
+ * Insert a line break into the content model.
+ * The interpretation of a break is up to the implementation:
+ * it may enter a character, split a node in the tree, etc.
+ * This may be more efficient than calling InsertText with a newline.
+ */
+ [can_run_script]
+ void insertLineBreak();
+
+%{C++
+ inline bool IsHTMLEditor() const;
+ inline bool IsTextEditor() const;
+
+ /**
+ * AsEditorBase() returns a pointer to EditorBase class.
+ *
+ * In order to avoid circular dependency issues, this method is defined
+ * in mozilla/EditorBase.h. Consumers need to #include that header.
+ */
+ inline mozilla::EditorBase* AsEditorBase();
+ inline const mozilla::EditorBase* AsEditorBase() const;
+
+ /**
+ * AsTextEditor() and GetTextEditor() return a pointer to TextEditor class.
+ * AsTextEditor() does not check the concrete class type. So, it never
+ * returns nullptr. GetAsTextEditor() does check the concrete class type.
+ * So, it may return nullptr.
+ *
+ * In order to avoid circular dependency issues, this method is defined
+ * in mozilla/TextEditor.h. Consumers need to #include that header.
+ */
+ inline mozilla::TextEditor* AsTextEditor();
+ inline const mozilla::TextEditor* AsTextEditor() const;
+ inline mozilla::TextEditor* GetAsTextEditor();
+ inline const mozilla::TextEditor* GetAsTextEditor() const;
+
+ /**
+ * AsHTMLEditor() and GetHTMLEditor() return a pointer to HTMLEditor class.
+ * AsHTMLEditor() does not check the concrete class type. So, it never
+ * returns nullptr. GetAsHTMLEditor() does check the concrete class type.
+ * So, it may return nullptr.
+ *
+ * In order to avoid circular dependency issues, this method is defined
+ * in mozilla/HTMLEditor.h. Consumers need to #include that header.
+ */
+ inline mozilla::HTMLEditor* AsHTMLEditor();
+ inline const mozilla::HTMLEditor* AsHTMLEditor() const;
+ inline mozilla::HTMLEditor* GetAsHTMLEditor();
+ inline const mozilla::HTMLEditor* GetAsHTMLEditor() const;
+%}
+};
diff --git a/editor/nsIEditorMailSupport.idl b/editor/nsIEditorMailSupport.idl
new file mode 100644
index 0000000000..64c443115f
--- /dev/null
+++ b/editor/nsIEditorMailSupport.idl
@@ -0,0 +1,48 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIArray;
+
+webidl Node;
+
+[scriptable, builtinclass, uuid(fdf23301-4a94-11d3-9ce4-9960496c41bc)]
+interface nsIEditorMailSupport : nsISupports
+{
+ /** Insert a string as quoted text
+ * (whose representation is dependant on the editor type),
+ * replacing the selected text (if any),
+ * including, if possible, a "cite" attribute.
+ * @param aQuotedText The actual text to be quoted
+ * @param aCitation The "mid" URL of the source message
+ * @param aInsertHTML Insert as html? (vs plaintext)
+ * @return The node which was inserted
+ */
+ [can_run_script]
+ Node insertAsCitedQuotation(in AString aQuotedText,
+ in AString aCitation,
+ in boolean aInsertHTML);
+
+ /**
+ * Rewrap the selected part of the document, re-quoting if necessary.
+ * @param aRespectNewlines Try to maintain newlines in the original?
+ */
+ [can_run_script]
+ void rewrap(in boolean aRespectNewlines);
+
+ /**
+ * Inserts a plaintext string at the current location,
+ * with special processing for lines beginning with ">",
+ * which will be treated as mail quotes and inserted
+ * as plaintext quoted blocks.
+ * If the selection is not collapsed, the selection is deleted
+ * and the insertion takes place at the resulting collapsed selection.
+ *
+ * @param aString the string to be inserted
+ */
+ [can_run_script]
+ void insertTextWithQuotations(in AString aStringToInsert);
+};
diff --git a/editor/nsIEditorSpellCheck.idl b/editor/nsIEditorSpellCheck.idl
new file mode 100644
index 0000000000..0849956c7a
--- /dev/null
+++ b/editor/nsIEditorSpellCheck.idl
@@ -0,0 +1,165 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIEditor;
+interface nsIEditorSpellCheckCallback;
+
+[scriptable, uuid(a171c25f-e4a8-4d08-adef-b797e6377bdc)]
+interface nsIEditorSpellCheck : nsISupports
+{
+
+ /**
+ * Returns true if we can enable spellchecking. If there are no available
+ * dictionaries, this will return false.
+ */
+ boolean canSpellCheck();
+
+ /**
+ * Turns on the spell checker for the given editor. enableSelectionChecking
+ * set means that we only want to check the current selection in the editor,
+ * (this controls the behavior of GetNextMisspelledWord). For spellchecking
+ * clients with no modal UI (such as inline spellcheckers), this flag doesn't
+ * matter. Initialization is asynchronous and is not complete until the given
+ * callback is called.
+ */
+ void InitSpellChecker(in nsIEditor editor, in boolean enableSelectionChecking,
+ [optional] in nsIEditorSpellCheckCallback callback);
+
+ /**
+ * When interactively spell checking the document, this will return the
+ * value of the next word that is misspelled. This also computes the
+ * suggestions which you can get by calling GetSuggestedWord.
+ *
+ * @see mozSpellChecker::GetNextMisspelledWord
+ */
+ [can_run_script]
+ AString GetNextMisspelledWord();
+
+ /**
+ * Used to get suggestions for the last word that was checked and found to
+ * be misspelled. The first call will give you the first (best) suggestion.
+ * Subsequent calls will iterate through all the suggestions, allowing you
+ * to build a list. When there are no more suggestions, an empty string
+ * (not a null pointer) will be returned.
+ *
+ * @see mozSpellChecker::GetSuggestedWord
+ */
+ AString GetSuggestedWord();
+
+ /**
+ * Check a given word. In spite of the name, this function checks the word
+ * you give it, returning true if the word is misspelled. If the word is
+ * misspelled, it will compute the suggestions which you can get from
+ * GetSuggestedWord().
+ *
+ * @see mozSpellChecker::CheckCurrentWord
+ */
+ boolean CheckCurrentWord(in AString suggestedWord);
+
+ /**
+ * Check a given word then returns suggestion words via Promise if a given
+ * word is misspelled. If not misspelled, returns empty string array.
+ */
+ [implicit_jscontext]
+ Promise suggest(in AString aCheckingWorkd, in unsigned long aMaxCount);
+
+ /**
+ * Use when modally checking the document to replace a word.
+ *
+ * @see mozSpellChecker::CheckCurrentWord
+ */
+ [can_run_script]
+ void ReplaceWord(in AString misspelledWord, in AString replaceWord, in boolean allOccurrences);
+
+ /**
+ * @see mozSpellChecker::IgnoreAll
+ */
+ void IgnoreWordAllOccurrences(in AString word);
+
+ /**
+ * Fills an internal list of words added to the personal dictionary. These
+ * words can be retrieved using GetPersonalDictionaryWord()
+ *
+ * @see mozSpellChecker::GetPersonalDictionary
+ * @see GetPersonalDictionaryWord
+ */
+ void GetPersonalDictionary();
+
+ /**
+ * Used after you call GetPersonalDictionary() to iterate through all the
+ * words added to the personal dictionary. Will return the empty string when
+ * there are no more words.
+ */
+ AString GetPersonalDictionaryWord();
+
+ /**
+ * Adds a word to the current personal dictionary.
+ *
+ * @see mozSpellChecker::AddWordToDictionary
+ */
+ void AddWordToDictionary(in AString word);
+
+ /**
+ * Removes a word from the current personal dictionary.
+ *
+ * @see mozSpellChecker::RemoveWordFromPersonalDictionary
+ */
+ void RemoveWordFromDictionary(in AString word);
+
+ /**
+ * Retrieves a list of the currently available dictionaries. The strings will
+ * typically be language IDs, like "en-US".
+ *
+ * @see mozISpellCheckingEngine::GetDictionaryList
+ */
+ Array<ACString> GetDictionaryList();
+
+ /**
+ * @see mozSpellChecker::GetCurrentDictionaries
+ */
+ Array<ACString> getCurrentDictionaries();
+
+ /**
+ * @see mozSpellChecker::SetCurrentDictionaries
+ */
+ [implicit_jscontext]
+ Promise setCurrentDictionaries(in Array<ACString> dictionaries);
+
+ /**
+ * Call this to free up the spell checking object. It will also save the
+ * current selected language as the default for future use.
+ *
+ * If you have called CanSpellCheck but not InitSpellChecker, you can still
+ * call this function to clear the cached spell check object, and no
+ * preference saving will happen.
+ */
+ void UninitSpellChecker();
+
+ const unsigned long FILTERTYPE_NORMAL = 1;
+ const unsigned long FILTERTYPE_MAIL = 2;
+
+ /**
+ * Used to filter the content (for example, to skip blockquotes in email from
+ * spellchecking. Call this before calling InitSpellChecker; calling it
+ * after initialization will have no effect.
+ */
+ void setFilterType(in unsigned long filterType);
+
+ /**
+ * Update the dictionary in use to be sure it corresponds to what the editor
+ * needs. The update is asynchronous and is not complete until the given
+ * callback is called.
+ */
+ void UpdateCurrentDictionary([optional] in nsIEditorSpellCheckCallback callback);
+
+};
+
+[scriptable, function, uuid(5f0a4bab-8538-4074-89d3-2f0e866a1c0b)]
+interface nsIEditorSpellCheckCallback : nsISupports
+{
+ void editorSpellCheckDone();
+};
diff --git a/editor/nsIHTMLAbsPosEditor.idl b/editor/nsIHTMLAbsPosEditor.idl
new file mode 100644
index 0000000000..e04c13a3b5
--- /dev/null
+++ b/editor/nsIHTMLAbsPosEditor.idl
@@ -0,0 +1,31 @@
+/* -*- 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 "nsISupports.idl"
+#include "domstubs.idl"
+
+[scriptable, builtinclass, uuid(91375f52-20e6-4757-9835-eb04fabe5498)]
+interface nsIHTMLAbsPosEditor : nsISupports
+{
+ /**
+ * true if Absolute Positioning handling is enabled in the editor
+ */
+ [setter_can_run_script]
+ attribute boolean absolutePositioningEnabled;
+
+
+ /* Utility methods */
+
+ /**
+ * true if Snap To Grid is enabled in the editor.
+ */
+ attribute boolean snapToGridEnabled;
+
+ /**
+ * sets the grid size in pixels.
+ * @param aSizeInPixels [IN] the size of the grid in pixels
+ */
+ attribute unsigned long gridSize;
+};
diff --git a/editor/nsIHTMLEditor.idl b/editor/nsIHTMLEditor.idl
new file mode 100644
index 0000000000..fd79f0b868
--- /dev/null
+++ b/editor/nsIHTMLEditor.idl
@@ -0,0 +1,342 @@
+/* -*- 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 "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsIContent;
+
+webidl Document;
+webidl Element;
+webidl Node;
+webidl Selection;
+
+[scriptable, builtinclass, uuid(87ee993e-985f-4a43-a974-0d9512da2fb0)]
+interface nsIHTMLEditor : nsISupports
+{
+%{C++
+ typedef short EAlignment;
+%}
+
+ // used by GetAlignment()
+ const short eLeft = 0;
+ const short eCenter = 1;
+ const short eRight = 2;
+ const short eJustify = 3;
+
+
+ /* ------------ Inline property methods -------------- */
+
+ /**
+ * SetInlineProperty() sets the aggregate properties on the current selection
+ *
+ * @param aProperty the property to set on the selection
+ * @param aAttribute the attribute of the property, if applicable.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color"
+ * @param aValue if aAttribute is not null, the value of the attribute.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color",
+ * aValue="0x00FFFF"
+ */
+ [can_run_script]
+ void setInlineProperty(in AString aProperty,
+ in AString aAttribute,
+ in AString aValue);
+
+ /**
+ * getInlinePropertyWithAttrValue() 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.
+ *
+ * @param aProperty the property to get on the selection
+ * @param aAttribute the attribute of the property, if applicable.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color"
+ * @param aValue if aAttribute is not null, the value of the attribute.
+ * May be null.
+ * Example: aProperty="font", aAttribute="color",
+ * aValue="0x00FFFF"
+ * @param aFirst [OUT] PR_TRUE if the first text node in the
+ * selection has the property
+ * @param aAny [OUT] PR_TRUE if any of the text nodes in the
+ * selection have the property
+ * @param aAll [OUT] PR_TRUE if all of the text nodes in the
+ * selection have the property
+ */
+ [can_run_script]
+ AString getInlinePropertyWithAttrValue(in AString aProperty,
+ in AString aAttribute,
+ in AString aValue,
+ out boolean aFirst,
+ out boolean aAny,
+ out boolean aAll);
+
+ /**
+ * removeInlineProperty() removes a property which changes inline style of
+ * text. E.g., bold, italic, super and sub.
+ *
+ * @param aProperty Tag name whcih represents the inline style you want to
+ * remove. E.g., "strong", "b", etc.
+ * If "href", <a> element which has href attribute will be
+ * removed.
+ * If "name", <a> element which has non-empty name
+ * attribute will be removed.
+ * @param aAttribute If aProperty is "font", aAttribute should be "face",
+ * "size", "color" or "bgcolor".
+ */
+ [can_run_script]
+ void removeInlineProperty(in AString aProperty, in AString aAttribute);
+
+ /* ------------ HTML content methods -------------- */
+
+ /**
+ * Tests if a node is a BLOCK element according the the HTML 4.0 DTD.
+ * This does NOT consider CSS effect on display type
+ *
+ * @param aNode the node to test
+ */
+ boolean nodeIsBlock(in Node node);
+
+ /**
+ * Insert some HTML source at the current location
+ *
+ * @param aInputString the string to be inserted
+ */
+ [can_run_script]
+ void insertHTML(in AString aInputString);
+
+ /**
+ * Rebuild the entire document from source HTML
+ * Needed to be able to edit HEAD and other outside-of-BODY content
+ *
+ * @param aSourceString HTML source string of the entire new document
+ */
+ [can_run_script]
+ void rebuildDocumentFromSource(in AString aSourceString);
+
+ /**
+ * Insert an element, which may have child nodes, at the selection
+ * Used primarily to insert a new element for various insert element dialogs,
+ * but it enforces the HTML 4.0 DTD "CanContain" rules, so it should
+ * be useful for other elements.
+ *
+ * @param aElement The element to insert
+ * @param aDeleteSelection Delete the selection before inserting
+ * If aDeleteSelection is PR_FALSE, then the element is inserted
+ * after the end of the selection for all element except
+ * Named Anchors, which insert before the selection
+ */
+ [can_run_script]
+ void insertElementAtSelection(in Element aElement,
+ in boolean aDeleteSelection);
+
+ /**
+ * Set the BaseURL for the document to the current URL
+ * but only if the page doesn't have a <base> tag
+ * This should be done after the document URL has changed,
+ * such as after saving a file
+ * This is used as base for relativizing link and image urls
+ */
+ void updateBaseURL();
+
+
+ /* ------------ Selection manipulation -------------- */
+ /* Should these be moved to Selection? */
+
+ /**
+ * Set the selection at the suppled element
+ *
+ * @param aElement An element in the document
+ */
+ [can_run_script]
+ void selectElement(in Element aElement);
+
+ /**
+ * getParagraphState returns what block tag paragraph format is in
+ * the selection.
+ * @param aMixed True if there is more than one format
+ * @return Name of block tag. "" is returned for none.
+ */
+ AString getParagraphState(out boolean aMixed);
+
+ /**
+ * getFontFaceState returns what font face is in the selection.
+ * @param aMixed True if there is more than one font face
+ * @return Name of face. Note: "tt" is returned for
+ * tt tag. "" is returned for none.
+ */
+ [can_run_script]
+ AString getFontFaceState(out boolean aMixed);
+
+ /**
+ * getHighlightColorState returns what the highlight color of the selection.
+ * @param aMixed True if there is more than one font color
+ * @return Color string. "" is returned for none.
+ */
+ [can_run_script]
+ AString getHighlightColorState(out boolean aMixed);
+
+ /**
+ * getListState returns what list type is in the selection.
+ * @param aMixed True if there is more than one type of list, or
+ * if there is some list and non-list
+ * @param aOL The company that employs me. No, really, it's
+ * true if an "ol" list is selected.
+ * @param aUL true if an "ul" list is selected.
+ * @param aDL true if a "dl" list is selected.
+ */
+ void getListState(out boolean aMixed, out boolean aOL, out boolean aUL,
+ out boolean aDL);
+
+ /**
+ * getListItemState returns what list item type is in the selection.
+ * @param aMixed True if there is more than one type of list item, or
+ * if there is some list and non-list
+ * XXX This ignores `<li>` element selected state.
+ * For example, even if `<li>` and `<dt>` are selected,
+ * this is set to false.
+ * @param aLI true if "li" list items are selected.
+ * @param aDT true if "dt" list items are selected.
+ * @param aDD true if "dd" list items are selected.
+ */
+ void getListItemState(out boolean aMixed, out boolean aLI,
+ out boolean aDT, out boolean aDD);
+
+ /**
+ * getAlignment returns what alignment is in the selection.
+ * @param aMixed Always returns false.
+ * @param aAlign enum value for first encountered alignment
+ * (left/center/right)
+ */
+ [can_run_script]
+ void getAlignment(out boolean aMixed, out short aAlign);
+
+ /**
+ * Document me!
+ *
+ */
+ [can_run_script]
+ void makeOrChangeList(in AString aListType, in boolean entireList,
+ in AString aBulletType);
+
+ /**
+ * removeList removes list items (<li>, <dd>, and <dt>) and list structures
+ * (<ul>, <ol>, and <dl>).
+ *
+ * @param aListType Unused.
+ */
+ [can_run_script]
+ void removeList(in AString aListType);
+
+ /**
+ * GetElementOrParentByTagName() returns an inclusive ancestor element whose
+ * name matches aTagName from aNode or anchor node of Selection to <body>
+ * element or null if there is no element matching with aTagName.
+ *
+ * @param aTagName The tag name which you want to look for.
+ * Must not be empty string.
+ * If "list", the result may be <ul>, <ol> or <dl>
+ * element.
+ * If "td", the result may be <td> or <th>.
+ * If "href", the result may be <a> element
+ * which has "href" attribute with non-empty value.
+ * If "anchor", the result may be <a> which has
+ * "name" attribute with non-empty value.
+ * @param aNode If non-null, this starts to look for the result
+ * from it. Otherwise, i.e., null, starts from
+ * anchor node of Selection.
+ * @return If an element which matches aTagName, returns
+ * an Element. Otherwise, nullptr.
+ */
+ Element getElementOrParentByTagName(in AString aTagName,
+ in Node aNode);
+
+ /**
+ * 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 "href", this thinks that an <a>
+ * element which has non-empty "href" attribute includes the range, the
+ * <a> element is selected.
+ *
+ * @param aTagName Case-insensitive element name.
+ * If empty string, this returns any element node or null.
+ * If "href", this returns an <a> element which has
+ * non-empty "href" attribute or null.
+ * If "anchor", this returns an <a> element which has
+ * non-empty "name" attribute or null.
+ * Otherwise, returns an element node whose name is
+ * same as aTagName or null.
+ * @return A "selected" element.
+ */
+ nsISupports getSelectedElement(in AString aTagName);
+
+ /**
+ * Return a new element with default attribute values
+ *
+ * This does not rely on the selection, and is not sensitive to context.
+ *
+ * Used primarily to supply new element for various insert element dialogs
+ * (Image, Link, NamedAnchor, Table, and HorizontalRule
+ * are the only returned elements as of 7/25/99)
+ *
+ * @param aTagName The HTML tagname
+ * Special input values for Links and Named anchors:
+ * Use "href" to get a link node
+ * (an "A" tag with the "href" attribute set)
+ * Use "anchor" or "namedanchor" to get a named anchor node
+ * (an "A" tag with the "name" attribute set)
+ * @return The new element created.
+ */
+ [can_run_script]
+ Element createElementWithDefaults(in AString aTagName);
+
+ /**
+ * Insert an link element as the parent of the current selection
+ *
+ * @param aElement An "A" element with a non-empty "href" attribute
+ */
+ [can_run_script]
+ void insertLinkAroundSelection(in Element aAnchorElement);
+
+ /**
+ * Set the value of the "bgcolor" attribute on the document's <body> element
+ *
+ * @param aColor The HTML color string, such as "#ffccff" or "yellow"
+ */
+ [can_run_script]
+ void setBackgroundColor(in AString aColor);
+
+ /**
+ * A boolean which is true is the HTMLEditor has been instantiated
+ * with CSS knowledge and if the CSS pref is currently checked
+ *
+ * @return true if CSS handled and enabled
+ */
+ [setter_can_run_script] attribute boolean isCSSEnabled;
+
+ /**
+ * checkSelectionStateForAnonymousButtons() may refresh editing UI such as
+ * resizers, inline-table-editing UI, absolute positioning UI for current
+ * Selection and focus state. When this method shows or hides UI, the
+ * editor (and/or its document/window) could be broken by mutation observers.
+ * FYI: Current user in script is only BlueGriffon.
+ */
+ [can_run_script]
+ void checkSelectionStateForAnonymousButtons();
+
+ boolean isAnonymousElement(in Element aElement);
+
+ /**
+ * A boolean indicating if a return key pressed in a paragraph creates
+ * another paragraph or just inserts a <br> at the caret
+ *
+ * @return true if CR in a paragraph creates a new paragraph
+ */
+ attribute boolean returnInParagraphCreatesNewParagraph;
+};
diff --git a/editor/nsIHTMLInlineTableEditor.idl b/editor/nsIHTMLInlineTableEditor.idl
new file mode 100644
index 0000000000..479dbb2b30
--- /dev/null
+++ b/editor/nsIHTMLInlineTableEditor.idl
@@ -0,0 +1,20 @@
+/* -*- 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 "nsISupports.idl"
+#include "domstubs.idl"
+
+[scriptable, builtinclass, uuid(eda2e65c-a758-451f-9b05-77cb8de74ed2)]
+interface nsIHTMLInlineTableEditor : nsISupports
+{
+ /**
+ * boolean indicating if inline table editing is enabled in the editor.
+ * When inline table editing is enabled, and when the selection is
+ * contained in a table cell, special buttons allowing to add/remove
+ * a line/column are available on the cell's border.
+ */
+ [setter_can_run_script]
+ attribute boolean inlineTableEditingEnabled;
+};
diff --git a/editor/nsIHTMLObjectResizer.idl b/editor/nsIHTMLObjectResizer.idl
new file mode 100644
index 0000000000..120fa06b84
--- /dev/null
+++ b/editor/nsIHTMLObjectResizer.idl
@@ -0,0 +1,37 @@
+/* -*- 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 "nsISupports.idl"
+#include "domstubs.idl"
+
+webidl Element;
+
+[scriptable, builtinclass, uuid(8b396020-69d3-451f-80c1-1a96a7da25a9)]
+interface nsIHTMLObjectResizer : nsISupports
+{
+%{C++
+ typedef short EResizerLocation;
+%}
+ const short eTopLeft = 0;
+ const short eTop = 1;
+ const short eTopRight = 2;
+ const short eLeft = 3;
+ const short eRight = 4;
+ const short eBottomLeft = 5;
+ const short eBottom = 6;
+ const short eBottomRight = 7;
+
+ /**
+ * a boolean indicating if object resizing is enabled in the editor
+ */
+ [setter_can_run_script]
+ attribute boolean objectResizingEnabled;
+
+ /**
+ * Hide resizers if they are visible. If this is called while there is no
+ * visible resizers, this does not throw exception, just does nothing.
+ */
+ void hideResizers();
+};
diff --git a/editor/nsITableEditor.idl b/editor/nsITableEditor.idl
new file mode 100644
index 0000000000..cf5e3bf8b0
--- /dev/null
+++ b/editor/nsITableEditor.idl
@@ -0,0 +1,460 @@
+/* -*- 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 "nsISupports.idl"
+
+webidl Element;
+webidl Node;
+webidl Range;
+
+[scriptable, builtinclass, uuid(4805e684-49b9-11d3-9ce4-ed60bd6cb5bc)]
+interface nsITableEditor : nsISupports
+{
+ const short eNoSearch = 0;
+ const short ePreviousColumn = 1;
+ const short ePreviousRow = 2;
+
+ /**
+ * insertTableCell() inserts <td> elements before or after a cell element
+ * containing first selection range. I.e., if the cell spans columns and
+ * aInsertPosition is true, new columns will be inserted after the
+ * right-most column which contains the cell. 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 a 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.
+ * If first selection range is not in table cell element, this does nothing
+ * without exception.
+ *
+ * @param aNumberOfCellssToInsert Number of cells to insert.
+ * @param aInsertAfterSelectedCell true if new cells should be inserted
+ * before current cell. Otherwise, will
+ * be inserted after the cell.
+ */
+ [can_run_script]
+ void insertTableCell(in long aNumberOfColumnsToInsert,
+ in boolean aInsertAfterSelectedCell);
+
+ /**
+ * insertTableColumn() inserts columns before or after a cell element
+ * containing first selection range. I.e., if the cell spans columns and
+ * aInsertAfterSelectedCell is tre, new columns will be inserted after the
+ * right-most column which contains the cell. If first selection range is
+ * not in table cell element, this does nothing without exception.
+ *
+ * @param aNumberOfColumnsToInsert Number of columns to insert.
+ * @param aInsertAfterSelectedCell true if new columns will be inserted
+ * before current cell. Otherwise, will
+ * be inserted after the cell.
+ */
+ [can_run_script]
+ void insertTableColumn(in long aNumberOfColumnsToInsert,
+ in boolean aInsertAfterSelectedCell);
+
+ /*
+ * insertTableRow() inserts <tr> elements before or after a <td> element
+ * containing first selection range. I.e., if the cell spans rows and
+ * aInsertAfterSelectedCell is true, new rows will be inserted after the
+ * bottom-most row which contains the cell. If first selection range is
+ * not in table cell element, this does nothing without exception.
+ *
+ * @param aNumberOfRowsToInsert Number of rows to insert.
+ * @param aInsertAfterSelectedCell true if new rows will be inserted
+ * before current cell. Otherwise, will
+ * be inserted after the cell.
+ */
+ [can_run_script]
+ void insertTableRow(in long aNumberOfRowsToInsert,
+ in boolean aInsertAfterSelectedCell);
+
+ /** Delete table methods
+ * Delete starting at the selected cell or the
+ * cell (or table) enclosing the selection anchor
+ * The selection is collapsed and is left in the
+ * cell at the same row,col location as
+ * the previous selection anchor, if possible,
+ * else in the closest neighboring cell
+ *
+ * @param aNumber Number of items to insert/delete
+ */
+ [can_run_script]
+ void deleteTable();
+
+ /**
+ * deleteTableCellContents() 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 nothing without exception if selection
+ * is not in cell element.
+ */
+ [can_run_script]
+ void deleteTableCellContents();
+
+ /**
+ * deleteTableCell() 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 nothing without exception if selection is
+ * not in cell element.
+ *
+ * @param aNumberOfCellsToDelete Number of cells to remove. This is ignored
+ * if 2 or more cells are selected.
+ */
+ [can_run_script]
+ void deleteTableCell(in long aNumberOfCellsToDelete);
+
+ /**
+ * deleteTableColumn() 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, throws exception.
+ * If selection is not in a cell element, just does nothing without
+ * throwing exception.
+ * 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.
+ */
+ [can_run_script]
+ void deleteTableColumn(in long aNumberOfColumnsToDelete);
+
+ /**
+ * deleteTableRow() 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, throws exception.
+ * If selection is not in a cell element, just does nothing without
+ * throwing exception.
+ *
+ * @param aNumberOfRowsToDelete Number of rows to remove. This is ignored
+ * if 2 or more cells are selected.
+ */
+ [can_run_script]
+ void deleteTableRow(in long aNumberOfRowsToDelete);
+
+ /** Table Selection methods
+ * Selecting a row or column actually
+ * selects all cells (not TR in the case of rows)
+ */
+ [can_run_script]
+ void selectTableCell();
+
+ [can_run_script]
+ void selectTableRow();
+ [can_run_script]
+ void selectTableColumn();
+ [can_run_script]
+ void selectTable();
+ [can_run_script]
+ void selectAllTableCells();
+
+ /** Create a new TD or TH element, the opposite type of the supplied aSourceCell
+ * 1. Copy all attributes from aSourceCell to the new cell
+ * 2. Move all contents of aSourceCell to the new cell
+ * 3. Replace aSourceCell in the table with the new cell
+ *
+ * @param aSourceCell The cell to be replaced
+ * @return The new cell that replaces aSourceCell
+ */
+ [can_run_script]
+ Element switchTableCellHeaderType(in Element aSourceCell);
+
+ /** Merges contents of all selected cells
+ * for selected cells that are adjacent,
+ * this will result in a larger cell with appropriate
+ * rowspan and colspan, and original cells are deleted
+ * The resulting cell is in the location of the
+ * cell at the upper-left corner of the adjacent
+ * block of selected cells
+ *
+ * @param aMergeNonContiguousContents:
+ * If true:
+ * Non-contiguous cells are not deleted,
+ * but their contents are still moved
+ * to the upper-left cell
+ * If false: contiguous cells are ignored
+ *
+ * If there are no selected cells,
+ * and selection or caret is in a cell,
+ * that cell and the one to the right
+ * are merged
+ */
+ [can_run_script]
+ void joinTableCells(in boolean aMergeNonContiguousContents);
+
+ /** Split a cell that has rowspan and/or colspan > 0
+ * into cells such that all new cells have
+ * rowspan = 1 and colspan = 1
+ * All of the contents are not touched --
+ * they will appear to be in the upper-left cell
+ */
+ [can_run_script]
+ void splitTableCell();
+
+ /** Scan through all rows and add cells as needed so
+ * all locations in the cellmap are occupied.
+ * Used after inserting single cells or pasting
+ * a collection of cells that extend past the
+ * previous size of the table
+ * If aTable is null, it uses table enclosing the selection anchor
+ * This doesn't doesn't change the selection,
+ * thus it can be used to fixup all tables
+ * in a page independent of the selection
+ */
+ [can_run_script]
+ void normalizeTable(in Element aTable);
+
+ /**
+ * getCellIndexes() computes row index and column index of a table cell.
+ * Note that this depends on layout information. Therefore, all pending
+ * layout should've been flushed before calling this.
+ *
+ * @param aCellElement If not null, this computes indexes of the cell.
+ * If null, this computes indexes of a cell which
+ * contains anchor of Selection.
+ * @param aRowIndex Must be an object, whose .value will be set
+ * to row index of the cell. 0 is the first row.
+ * If rowspan is set to 2 or more, the start
+ * row index is used.
+ * @param aColumnIndex Must be an object, whose .value will be set
+ * to column index of the cell. 0 is the first
+ * column. If colspan is set to 2 or more, the
+ * start column index is used.
+ */
+ [can_run_script]
+ void getCellIndexes(in Element aCellElement,
+ out long aRowIndex, out long aColumnIndex);
+
+ /**
+ * getTableSize() computes number of rows and columns.
+ * Note that this depends on layout information. Therefore, all pending
+ * layout should've been flushed before calling this.
+ *
+ * @param aTableOrElementInTable If a <table> element, this computes number
+ * of rows and columns of it.
+ * If another element and in a <table>, this
+ * computes number of rows and columns of
+ * the nearest ancestor <table> element.
+ * If element is not in <table> element,
+ * throwing an exception.
+ * If null, this looks for nearest ancestor
+ * <table> element containing anchor of
+ * Selection. If found, computes the number
+ * of rows and columns of the <table>.
+ * Otherwise, throwing an exception.
+ * @param aRowCount Number of *actual* row count.
+ * I.e., rowspan does NOT increase this value.
+ * @param aColumnCount Number of column count.
+ * I.e., if colspan is specified with bigger
+ * number than actual, the value is used
+ * as this.
+ */
+ [can_run_script]
+ void getTableSize(in Element aTableOrElementInTable,
+ out long aRowCount, out long aColCount);
+
+ /**
+ * getCellAt() returns a <td> or <th> element in a <table> if there is a
+ * cell at the indexes.
+ *
+ * @param aTableElement If not null, must be a <table> element.
+ * If null, looks for the nearest ancestor <table>
+ * to look for a cell.
+ * @param aRowIndex Row index of the cell.
+ * @param aColumnIndex Column index of the cell.
+ * @return Returns a <td> or <th> element if there is.
+ * Otherwise, returns null without throwing
+ * exception.
+ * If aTableElement is not null and not a <table>
+ * element, throwing an exception.
+ * If aTableElement is null and anchor of Selection
+ * is not in any <table> element, throwing an
+ * exception.
+ */
+ [can_run_script]
+ Element getCellAt(in Element aTableElement,
+ in long aRowIndex, in long aColumnIndex);
+
+ /**
+ * Get cell element and its various information from <table> element and
+ * indexes in it. If aTableElement is null, this looks for an ancestor
+ * <table> element of anchor of Selection. If there is no <table> element
+ * at that point, this throws exception. Note that this requires layout
+ * information. So, you need to flush the layout after changing the DOM
+ * tree.
+ * If there is no cell element at the indexes, this throws exception.
+ * XXX Perhaps, this is wrong behavior, this should return null without
+ * exception since the caller cannot distinguish whether the exception
+ * is caused by "not found" or other unexpected situation.
+ *
+ * @param aTableElement A <table> element. If this is null, this
+ * uses ancestor of anchor of Selection.
+ * @param aRowIndex Row index in aTableElement. Starting from 0.
+ * @param aColumnIndex Column index in aTableElement. Starting from
+ * 0.
+ * @param aCellElement [OUT] The cell element at the indexes.
+ * @param aStartRowIndex [OUT] First row index which contains
+ * aCellElement. E.g., if the cell's rowspan is
+ * not 1, this returns its first row index.
+ * I.e., this can be smaller than aRowIndex.
+ * @param aStartColumnIndex [OUT] First column index which contains the
+ * aCellElement. E.g., if the cell's colspan is
+ * larger than 1, this returns its first column
+ * index. I.e., this can be smaller than
+ * aColumIndex.
+ * @param aRowSpan [OUT] rowspan attribute value in most cases.
+ * If the specified value is invalid, this
+ * returns 1. Only when the document is written
+ * in HTML5 or later, this can be 0.
+ * @param aColSpan [OUT] colspan attribute value in most cases.
+ * If the specified value is invalid, this
+ * returns 1.
+ * @param aEffectiveRowSpan [OUT] Effective rowspan value at aRowIndex.
+ * This is same as:
+ * aRowSpan - (aRowIndex - aStartRowIndex)
+ * @param aEffectiveColSpan [OUT] Effective colspan value at aColumnIndex.
+ * This is same as:
+ * aColSpan - (aColumnIndex - aStartColumnIndex)
+ * @param aIsSelected [OUT] Returns true if aCellElement or its
+ * <tr> or <table> element is selected.
+ * Otherwise, e.g., aCellElement just contains
+ * selection range, returns false.
+ */
+ [can_run_script]
+ void getCellDataAt(in Element aTableElement,
+ in long aRowIndex, in long aColumnIndex,
+ out Element aCellElement,
+ out long aStartRowIndex, out long aStartColumnIndex,
+ out long aRowSpan, out long aColSpan,
+ out long aEffectiveRowSpan, out long aEffectiveColSpan,
+ out boolean aIsSelected);
+
+ /**
+ * getFirstRow() returns first <tr> element in a <table> element.
+ *
+ * @param aTableOrElementInTable If a <table> element, returns its first
+ * <tr> element.
+ * If another element, looks for nearest
+ * ancestor <table> element first. Then,
+ * return its first <tr> element.
+ * @return <tr> element in the <table> element.
+ * If <table> element is not found, this
+ * throws an exception.
+ * If there is a <table> element but it
+ * does not have <tr> elements, returns
+ * null without throwing exception.
+ * Note that this may return anonymous <tr>
+ * element if <table> has one or more cells
+ * but <tr> element is not in the source.
+ */
+ [can_run_script]
+ Element getFirstRow(in Element aTableElement);
+
+ /** Preferred direction to search for neighboring cell
+ * when trying to locate a cell to place caret in after
+ * a table editing action.
+ * Used for aDirection param in SetSelectionAfterTableEdit
+ */
+
+ /**
+ * getSelectedOrParentTableElement() returns a <td>, <th>, <tr> or <table>.
+ * If first selection range selects a <td> or <th>, returns it. aTagName
+ * is set to "td" even if the result is a <th> and aCount is set to
+ * Selection.rangeCount.
+ * If first selection range does not select <td> nor <th>, but selection
+ * anchor refers <table>, returns it. aTagName is set to "table" and
+ * aCount is set to 1.
+ * If first selection range does not select <td> nor <th>, but selection
+ * anchor refers <tr>, returns it. aTagName is set to "tr" and aCount is
+ * set to 1.
+ * If first selection range does not select <td> nor <th>, but selection
+ * anchor refers <td> (not include <th>!), returns it. aTagName is set to
+ * "td" and aCount is set to 0.
+ * Otherwise, if container of selection anchor is in a <td> or <th>,
+ * returns it. aTagName is set to "td" but aCount is set to 0.
+ * Otherwise, returns null, aTagName is set to empty string and aCount is
+ * set to 0. I.e., does not throw exception even if a cell is not found.
+ * NOTE: Calling this resets internal counter of getFirstSelectedCell()
+ * and getNextSelectedCell(). I.e., getNextSelectedCell() will
+ * return second selected cell element.
+ */
+ [can_run_script]
+ Element getSelectedOrParentTableElement(out AString aTagName,
+ out long aCount);
+
+ /** Generally used after GetSelectedOrParentTableElement
+ * to test if selected cells are complete rows or columns
+ *
+ * @param aElement Any table or cell element or any element
+ * inside a table
+ * Used to get enclosing table.
+ * If null, selection's anchorNode is used
+ *
+ * @return
+ * 0 aCellElement was not a cell
+ * (returned result = NS_ERROR_FAILURE)
+ * TableSelectionMode::Cell There are 1 or more cells selected but
+ * complete rows or columns are not selected
+ * TableSelectionMode::Row All cells are in 1 or more rows
+ * and in each row, all cells selected
+ * Note: This is the value if all rows
+ * (thus all cells) are selected
+ * TableSelectionMode::Column All cells are in 1 or more columns
+ * and in each column, all cells are selected
+ */
+ [can_run_script]
+ uint32_t getSelectedCellsType(in Element aElement);
+
+ /**
+ * getFirstSelectedCellInTable() returns a cell element, its row index and
+ * its column index if first range of Selection selects a cell. Note that
+ * that "selects a cell" means that the range container is a <tr> element
+ * and endOffset is startOffset + 1. So, even if first range of Selection
+ * is in a cell element, this treats the range does not select a cell.
+ * NOTE: Calling this resets internal counter of getFirstSelectedCell()
+ * and getNextSelectedCell(). I.e., getNextSelectedCell() will
+ * return second selected cell element.
+ *
+ * @param aRowIndex [OUT} Returns row index of the found cell. If not
+ * found, returns 0.
+ * @param aColumnIndex [OUT] Returns column index of the found cell. If
+ * not found, returns 0.
+ * @return The cell element which is selected by the first
+ * range of Selection. Even if this is not found,
+ * this returns null, not throwing exception.
+ */
+ [can_run_script]
+ Element getFirstSelectedCellInTable(out long aRowIndex, out long aColIndex);
+
+ /**
+ * getSelectedCells() returns an array of `<td>` and `<th>` elements which
+ * are selected in **any** `<table>` elements (i.e., some cells may be
+ * in different `<table>` element).
+ * If first range does not select a table cell element, this returns empty
+ * array because editor considers that selection is not in table cell
+ * selection mode.
+ * If second or later ranges do not select only a table cell element, this
+ * ignores the ranges.
+ */
+ [can_run_script]
+ Array<Element> getSelectedCells();
+};
diff --git a/editor/reftests/1088158-ref.html b/editor/reftests/1088158-ref.html
new file mode 100644
index 0000000000..be4592e69b
--- /dev/null
+++ b/editor/reftests/1088158-ref.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<textarea placeholder="placeholder"></textarea>
diff --git a/editor/reftests/1088158.html b/editor/reftests/1088158.html
new file mode 100644
index 0000000000..435aa3f638
--- /dev/null
+++ b/editor/reftests/1088158.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<script>
+ onload = function() {
+ var t = document.createElement('textarea');
+ t.placeholder = "placeholder";
+ document.body.appendChild(t.cloneNode(true));
+ }
+</script>
diff --git a/editor/reftests/1443902-1-ref.html b/editor/reftests/1443902-1-ref.html
new file mode 100644
index 0000000000..5c15f399bb
--- /dev/null
+++ b/editor/reftests/1443902-1-ref.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function init()
+{
+ document.getElementById("t1").focus();
+ document.getElementById("t1").setSelectionRange(4, 4);
+}
+</script>
+</head>
+<body onload="init()">
+<textarea id=t1 contenteditable=true>ABCD</textarea>
+</body>
+</html>
diff --git a/editor/reftests/1443902-1.html b/editor/reftests/1443902-1.html
new file mode 100644
index 0000000000..e6c133d0a4
--- /dev/null
+++ b/editor/reftests/1443902-1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function init()
+{
+ document.getElementById("t1").focus();
+ document.getElementById("t1").setSelectionRange(4, 4);
+ document.getElementById("t1").setAttribute("contentEditable", "false");
+}
+</script>
+</head>
+<body onload="init()">
+<textarea id=t1 contenteditable=true>ABCD</textarea>
+</body>
+</html>
diff --git a/editor/reftests/1443902-2-ref.html b/editor/reftests/1443902-2-ref.html
new file mode 100644
index 0000000000..727ed76ea9
--- /dev/null
+++ b/editor/reftests/1443902-2-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function init()
+{
+ document.getElementById("t1").focus();
+ document.getElementById("t1").setSelectionRange(4, 4);
+}
+</script>
+</head>
+<body onload="init()">
+<div id="d1">
+<input type="text" id=t1 value="ABCD">
+</div>
+</body>
+</html>
diff --git a/editor/reftests/1443902-2.html b/editor/reftests/1443902-2.html
new file mode 100644
index 0000000000..125057fcec
--- /dev/null
+++ b/editor/reftests/1443902-2.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function init()
+{
+ document.getElementById("t1").focus();
+ document.getElementById("t1").setSelectionRange(4, 4);
+ document.getElementById("d1").setAttribute("contentEditable", "false");
+}
+</script>
+</head>
+<body onload="init()">
+<div contenteditable=true id="d1">
+<input type="text" id=t1 value="ABCD">
+</div>
+</body>
+</html>
diff --git a/editor/reftests/1443902-3-ref.html b/editor/reftests/1443902-3-ref.html
new file mode 100644
index 0000000000..1dd669016b
--- /dev/null
+++ b/editor/reftests/1443902-3-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function init()
+{
+ document.getElementById("t1").focus();
+ document.getElementById("t1").setSelectionRange(0, 1);
+}
+</script>
+</head>
+<body onload="init()">
+<div>
+<input type="text" id=t1 value="ABCD" readonly>
+</div>
+</body>
+</html>
diff --git a/editor/reftests/1443902-3.html b/editor/reftests/1443902-3.html
new file mode 100644
index 0000000000..9fffb644b7
--- /dev/null
+++ b/editor/reftests/1443902-3.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function init()
+{
+ document.getElementById("t1").focus();
+ document.getElementById("t1").setSelectionRange(0, 1);
+ document.getElementById("d1").setAttribute("contentEditable", "false");
+}
+</script>
+</head>
+<body onload="init()">
+<div contenteditable=true id="d1">
+<input type="text" id=t1 value="ABCD" readonly>
+</div>
+</body>
+</html>
diff --git a/editor/reftests/1443902-4-ref.html b/editor/reftests/1443902-4-ref.html
new file mode 100644
index 0000000000..ab360866cc
--- /dev/null
+++ b/editor/reftests/1443902-4-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function init()
+{
+ document.getElementById("t1").focus();
+ document.getElementById("t1").setSelectionRange(4, 4);
+}
+</script>
+</head>
+<body onload="init()">
+<div id="d1">
+<input type="text">
+</div>
+<input type="text" id=t1 value="ABCD">
+</body>
+</html>
diff --git a/editor/reftests/1443902-4.html b/editor/reftests/1443902-4.html
new file mode 100644
index 0000000000..6aacfd6356
--- /dev/null
+++ b/editor/reftests/1443902-4.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function init()
+{
+ document.getElementById("t1").focus();
+ document.getElementById("t1").setSelectionRange(4, 4);
+ document.getElementById("d1").setAttribute("contentEditable", "false");
+}
+</script>
+</head>
+<body onload="init()">
+<div contenteditable=true id="d1">
+<input type="text">
+</div>
+<input type="text" id=t1 value="ABCD">
+</body>
+</html>
diff --git a/editor/reftests/338427-1-ref.html b/editor/reftests/338427-1-ref.html
new file mode 100644
index 0000000000..d645ad9fb1
--- /dev/null
+++ b/editor/reftests/338427-1-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="false" lang="testing-XX">strangeimpossibleword</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/338427-1.html b/editor/reftests/338427-1.html
new file mode 100644
index 0000000000..7a645a2247
--- /dev/null
+++ b/editor/reftests/338427-1.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <!-- invalid language will default to en-US, but no spell check since element is not focussed -->
+ <textarea lang="testing-XX">strangeimpossibleword</textarea>
+</body>
+</html>
diff --git a/editor/reftests/338427-2-ref.html b/editor/reftests/338427-2-ref.html
new file mode 100644
index 0000000000..273f82f9c7
--- /dev/null
+++ b/editor/reftests/338427-2-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+ var editor = document.getElementById('editor');
+ editor.addEventListener("focus", function() {
+ window.setTimeout(function() {
+ document.documentElement.className = '';
+ }, 0);
+ });
+ editor.focus();
+}
+</script>
+<body onload="init()">
+ <!-- invalid language will default to en-US -->
+ <div id="editor" lang="testing-XX" contenteditable="true" spellcheck="false">good possible word</div>
+</body>
+</html>
+
diff --git a/editor/reftests/338427-2.html b/editor/reftests/338427-2.html
new file mode 100644
index 0000000000..dc60977ac4
--- /dev/null
+++ b/editor/reftests/338427-2.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+ var editor = document.getElementById('editor');
+ editor.addEventListener("focus", function() {
+ window.setTimeout(function() {
+ document.documentElement.className = '';
+ }, 0);
+ });
+ editor.focus();
+}
+</script>
+<body onload="init()">
+ <!-- invalid language will default to en-US -->
+ <div id="editor" lang="testing-XX" contenteditable="true">good possible word</div>
+</body>
+</html>
diff --git a/editor/reftests/338427-3-ref.html b/editor/reftests/338427-3-ref.html
new file mode 100644
index 0000000000..14b993cffd
--- /dev/null
+++ b/editor/reftests/338427-3-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+ var editor = document.getElementById('editor');
+ // invalid language will default to en-US
+ editor.setAttribute('lang', 'testing-XX');
+ editor.addEventListener("focus", function() {
+ window.setTimeout(function() {
+ document.documentElement.className = '';
+ }, 0);
+ });
+ editor.focus();
+}
+</script>
+<body onload="init()">
+ <textarea id="editor" spellcheck="false" lang="en-US">good possible word</textarea>
+</body>
+</html>
diff --git a/editor/reftests/338427-3.html b/editor/reftests/338427-3.html
new file mode 100644
index 0000000000..ca994d80dd
--- /dev/null
+++ b/editor/reftests/338427-3.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+ var editor = document.getElementById('editor');
+ // invalid language will default to en-US
+ editor.setAttribute('lang', 'testing-XX');
+ editor.addEventListener("focus", function() {
+ window.setTimeout(function() {
+ document.documentElement.className = '';
+ }, 0);
+ });
+ editor.focus();
+}
+</script>
+<body onload="init()">
+ <textarea id="editor" lang="en-US">good possible word</textarea>
+</body>
+</html>
diff --git a/editor/reftests/388980-1-ref.html b/editor/reftests/388980-1-ref.html
new file mode 100644
index 0000000000..8b14d7e185
--- /dev/null
+++ b/editor/reftests/388980-1-ref.html
@@ -0,0 +1,25 @@
+<html>
+<head>
+<title>Reftest for bug 388980</title></html>
+<script type="text/javascript">
+
+var text = '<html><head></head><body style="font-size:16px;">'
+ + '<p><span style="background-color:red;">This paragraph should be red</span></p>'
+ + '<p><span style="background-color:blue;">This paragraph should be blue</span></p>'
+ + '<p>This paragraph should not be colored</p>'
+ + '</body></html>';
+
+function initIFrame() {
+ var doc = document.getElementById('theIFrame').contentDocument;
+ doc.designMode = 'on';
+ doc.open('text/html');
+ doc.write(text);
+ doc.close();
+}
+</script>
+</head>
+<body onload="initIFrame()" >
+<iframe id="theIFrame">
+</iframe>
+</body>
+</html>
diff --git a/editor/reftests/388980-1.html b/editor/reftests/388980-1.html
new file mode 100644
index 0000000000..f2e7d0de0e
--- /dev/null
+++ b/editor/reftests/388980-1.html
@@ -0,0 +1,43 @@
+<html>
+<head>
+<title>Reftest for bug 388980</title></html>
+<script type="text/javascript">
+
+var text = '<html><head></head><body style="font-size:16px;">'
+ + '<p id="redpar">This paragraph should be red</p>'
+ + '<p id="bluepar">This paragraph should be blue</p>'
+ + '<p>This paragraph should not be colored</p>'
+ +'</body></html>';
+
+
+function colorPar(par, color) {
+ var doc = document.getElementById('theIFrame').contentDocument;
+ var win = document.getElementById('theIFrame').contentWindow;
+ win.getSelection().selectAllChildren(doc.getElementById(par));
+ doc.execCommand("hilitecolor", false, color);
+ win.getSelection().removeAllRanges();
+}
+
+function initIFrame() {
+ var doc = document.getElementById('theIFrame').contentDocument;
+ doc.designMode = 'on';
+ doc.open('text/html');
+ doc.write(text);
+ doc.close();
+
+ // Test hilighting with styleWithCSS, should hilight the text...
+ doc.execCommand("styleWithCSS", false, true);
+ colorPar("redpar", "red");
+
+ // Test highlighting without styleWithCSS, should also work.
+ doc.execCommand("styleWithCSS", false, false);
+ colorPar("bluepar", "blue");
+
+}
+</script>
+</head>
+<body>
+<iframe id="theIFrame" onload="initIFrame()">
+</iframe>
+</body>
+</html>
diff --git a/editor/reftests/462758-grabbers-resizers-ref.html b/editor/reftests/462758-grabbers-resizers-ref.html
new file mode 100644
index 0000000000..a8f0c8691f
--- /dev/null
+++ b/editor/reftests/462758-grabbers-resizers-ref.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function init() {
+ var editor = document.querySelector("div[contenteditable]");
+ editor.addEventListener("focus", function() {
+ setTimeout(function() {
+ document.documentElement.className = "";
+ }, 0);
+ });
+ editor.focus();
+ }
+ </script>
+ <style type="text/css">
+ html, body, div {
+ margin: 0;
+ padding: 0;
+ }
+ div {
+ border: 1px solid black;
+ margin: 50px;
+ height: 200px;
+ width: 200px;
+ }
+ </style>
+</head>
+<body onload="init()">
+ <div contenteditable>
+ this editable container should be neither draggable nor resizable.
+ </div>
+</body>
+</html>
+
diff --git a/editor/reftests/462758-grabbers-resizers.html b/editor/reftests/462758-grabbers-resizers.html
new file mode 100644
index 0000000000..15459bb6cb
--- /dev/null
+++ b/editor/reftests/462758-grabbers-resizers.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function init() {
+ var editor = document.querySelector("div[contenteditable]");
+ editor.addEventListener("focus", function() {
+ setTimeout(function() {
+ document.documentElement.className = "";
+ }, 0);
+ });
+ editor.focus();
+ }
+ </script>
+ <style type="text/css">
+ html, body, div {
+ margin: 0;
+ padding: 0;
+ }
+ div {
+ border: 1px solid black;
+ margin: 50px;
+ height: 200px;
+ width: 200px;
+ }
+ </style>
+</head>
+<body onload="init()">
+ <div contenteditable style="position: absolute">
+ this editable container should be neither draggable nor resizable.
+ </div>
+</body>
+</html>
diff --git a/editor/reftests/642800-iframe.html b/editor/reftests/642800-iframe.html
new file mode 100644
index 0000000000..bb1ab63975
--- /dev/null
+++ b/editor/reftests/642800-iframe.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <style>
+ @media only screen and (max-width: 480px) {
+ .overflow-hidden
+ {
+ overflow: hidden;
+ }
+
+ .float-left
+ {
+ float: left;
+ background: #f0f;
+ }
+ }
+ </style>
+</head>
+<body>
+ <h1>Iframe content</h1>
+ <div class="float-left">
+ <textarea>This text should be visible when window is resized </textarea>
+
+ </div>
+ <div class="overflow-hidden">
+ <textarea>This text should be visible when window is resized </textarea>
+ </div>
+</body>
+</html>
diff --git a/editor/reftests/642800-ref.html b/editor/reftests/642800-ref.html
new file mode 100644
index 0000000000..f062b145b0
--- /dev/null
+++ b/editor/reftests/642800-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body>
+ <iframe onload="document.documentElement.className=''" src="642800-iframe.html" id="iframe" style="width: 500px; height: 200px"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/642800.html b/editor/reftests/642800.html
new file mode 100644
index 0000000000..f2af589231
--- /dev/null
+++ b/editor/reftests/642800.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function reframe(node) {
+ node.style.display = "none";
+ document.body.offsetWidth;
+ node.style.display = "block";
+ document.documentElement.className='';
+ }
+ </script>
+</head>
+<body>
+ <iframe onload="reframe(this)" src="642800-iframe.html" id="iframe" style="width: 500px; height: 200px"></iframe>
+
+</body>
+</html>
+
diff --git a/editor/reftests/672709-ref.html b/editor/reftests/672709-ref.html
new file mode 100644
index 0000000000..18ce2b5d58
--- /dev/null
+++ b/editor/reftests/672709-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <body>
+ <style>
+ :read-only { color: red; }
+ :read-write { color: green; }
+ </style>
+ <script>
+ onload = function() {
+ document.designMode = "on";
+ var p = document.createElement("p");
+ p.textContent = "test";
+ document.getElementById("x").appendChild(p);
+ getSelection().removeAllRanges(); // don't need a caret
+ document.documentElement.removeAttribute("class");
+ };
+ </script>
+ <div contenteditable id="x">
+ </div>
+ more test
+ </body>
+</html>
diff --git a/editor/reftests/672709.html b/editor/reftests/672709.html
new file mode 100644
index 0000000000..d42c54b0c4
--- /dev/null
+++ b/editor/reftests/672709.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <style>
+ body { color: green; }
+ </style>
+ <div>
+ <p>test</p>
+ </div>
+ more test
+ </body>
+</html>
diff --git a/editor/reftests/674212-spellcheck-ref.html b/editor/reftests/674212-spellcheck-ref.html
new file mode 100644
index 0000000000..77ad5b9685
--- /dev/null
+++ b/editor/reftests/674212-spellcheck-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en-US" class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function init() {
+ var editor = document.querySelector("div[contenteditable]");
+ editor.addEventListener("focus", function() {
+ setTimeout(function() {
+ document.documentElement.className = "";
+ }, 0);
+ });
+ editor.focus();
+ }
+ </script>
+</head>
+<body onload="init()">
+ <div contenteditable spellcheck>This is another misspellored word.</div>
+</body>
+</html>
+
diff --git a/editor/reftests/674212-spellcheck.html b/editor/reftests/674212-spellcheck.html
new file mode 100644
index 0000000000..7477cfc4b3
--- /dev/null
+++ b/editor/reftests/674212-spellcheck.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en-US" class="reftest-wait">
+<head>
+ <script type="text/javascript">
+ function init() {
+ var editor = document.querySelector("div[contenteditable]");
+ editor.addEventListener("focus", function() {
+ editor.textContent = "This is another misspellored word.";
+ setTimeout(function() {
+ document.documentElement.className = "";
+ }, 0);
+ });
+ editor.focus();
+ }
+ </script>
+</head>
+<body onload="init()">
+ <div contenteditable spellcheck>This is a misspellored word.</div>
+</body>
+</html>
diff --git a/editor/reftests/694880-1.html b/editor/reftests/694880-1.html
new file mode 100644
index 0000000000..9a034f57cc
--- /dev/null
+++ b/editor/reftests/694880-1.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ :read-only { color: green; }
+ :read-write { color: red; }
+ </style>
+ <body onload="document.designMode='on';document.designMode='off'">
+ <div>test</div>
+ </body>
+</html>
diff --git a/editor/reftests/694880-2.html b/editor/reftests/694880-2.html
new file mode 100644
index 0000000000..f6d137d5f9
--- /dev/null
+++ b/editor/reftests/694880-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ :read-only { color: green; }
+ :read-write { color: red; }
+ </style>
+ <body onload="document.designMode='on';document.designMode='off'">
+ <div>test</div>
+ <div contenteditable></div>
+ </body>
+</html>
diff --git a/editor/reftests/694880-3.html b/editor/reftests/694880-3.html
new file mode 100644
index 0000000000..481187fff7
--- /dev/null
+++ b/editor/reftests/694880-3.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ :read-only { color: red; }
+ :read-write { color: green; }
+ </style>
+ <body onload="document.designMode='on';document.designMode='off'">
+ <div contenteditable>test</div>
+ </body>
+</html>
diff --git a/editor/reftests/694880-ref.html b/editor/reftests/694880-ref.html
new file mode 100644
index 0000000000..d5c40547ee
--- /dev/null
+++ b/editor/reftests/694880-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ div { color: green; }
+ </style>
+ <body>
+ <div>test</div>
+ </body>
+</html>
diff --git a/editor/reftests/824080-1-ref.html b/editor/reftests/824080-1-ref.html
new file mode 100644
index 0000000000..8a87864724
--- /dev/null
+++ b/editor/reftests/824080-1-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor = document.getElementById("editor");
+ document.getSelection().selectAllChildren(document.body);
+ }
+ </script>
+</head>
+<body onload="doTest();">
+<p>normal text</p>
+<div id="editor">editable text</div>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-1.html b/editor/reftests/824080-1.html
new file mode 100644
index 0000000000..2dfe7e2c68
--- /dev/null
+++ b/editor/reftests/824080-1.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.blur();
+ document.getSelection().selectAllChildren(document.body);
+ }
+ </script>
+</head>
+<body onload="doTest();">
+<p>normal text</p>
+<div id="editor" contenteditable>editable text</div>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-2-ref.html b/editor/reftests/824080-2-ref.html
new file mode 100644
index 0000000000..b1ea296794
--- /dev/null
+++ b/editor/reftests/824080-2-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.getElementById("text"));
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ editor.blur();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<iframe id="editor" onload="doTest();"
+ srcdoc="<body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-2.html b/editor/reftests/824080-2.html
new file mode 100644
index 0000000000..e0c22bd1a9
--- /dev/null
+++ b/editor/reftests/824080-2.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.getElementById("text"));
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ editor.blur();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<iframe id="editor" onload="doTest();"
+ srcdoc="<script>document.designMode='on';</script><body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-3-ref.html b/editor/reftests/824080-3-ref.html
new file mode 100644
index 0000000000..8849fc3835
--- /dev/null
+++ b/editor/reftests/824080-3-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.getElementById("text"));
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<iframe id="editor" onload="doTest();"
+ src="data:text/html,<body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-3.html b/editor/reftests/824080-3.html
new file mode 100644
index 0000000000..b9882e5bb6
--- /dev/null
+++ b/editor/reftests/824080-3.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.getElementById("text"));
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<iframe id="editor" onload="doTest();"
+ src="data:text/html,<script>document.designMode='on';</script><body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-4-ref.html b/editor/reftests/824080-4-ref.html
new file mode 100644
index 0000000000..6fd7f5928c
--- /dev/null
+++ b/editor/reftests/824080-4-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.body);
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<div>content editable</div>
+<iframe id="editor" onload="doTest();"
+ srcdoc="<body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-4.html b/editor/reftests/824080-4.html
new file mode 100644
index 0000000000..59589d3367
--- /dev/null
+++ b/editor/reftests/824080-4.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor1 = document.getElementById("editor1");
+ editor1.focus();
+ editor1.blur();
+ document.getSelection().selectAllChildren(document.body);
+ var editor2 = document.getElementById("editor2");
+ var editorBody = editor2.contentDocument.body;
+ editor2.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor2.focus();
+ editor2.blur();
+ }
+ </script>
+</head>
+<body>
+<p>normal text</p>
+<div id="editor1" contenteditable>content editable</div>
+<iframe id="editor2" onload="doTest();"
+ srcdoc="<script>document.designMode='on';</script><body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-5-ref.html b/editor/reftests/824080-5-ref.html
new file mode 100644
index 0000000000..237fea2134
--- /dev/null
+++ b/editor/reftests/824080-5-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.body);
+ var editor = document.getElementById("editor");
+ var editorBody = editor.contentDocument.body;
+ editor.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor.focus();
+ }
+ </script>
+</head>
+<body>
+<p id="text">normal text</p>
+<div>content editable</div>
+<iframe id="editor" onload="doTest();"
+ src="data:text/html,<body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-5.html b/editor/reftests/824080-5.html
new file mode 100644
index 0000000000..30771d63f3
--- /dev/null
+++ b/editor/reftests/824080-5.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor1 = document.getElementById("editor1");
+ editor1.focus();
+ editor1.blur();
+ document.getSelection().selectAllChildren(document.body);
+ var editor2 = document.getElementById("editor2");
+ var editorBody = editor2.contentDocument.body;
+ editor2.contentDocument.getSelection().selectAllChildren(editorBody);
+ editor2.focus();
+ }
+ </script>
+</head>
+<body>
+<p>normal text</p>
+<div id="editor1" contenteditable>content editable</div>
+<iframe id="editor2" onload="doTest();"
+ src="data:text/html,<script>document.designMode='on';</script><body>editable text</body>"></iframe>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-6-ref.html b/editor/reftests/824080-6-ref.html
new file mode 100644
index 0000000000..7ef193fcc7
--- /dev/null
+++ b/editor/reftests/824080-6-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.blur();
+ }
+ </script>
+</head>
+<body onload="doTest()">
+<p>normal text</p>
+<textarea id="editor" spellcheck="false">textarea</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-6.html b/editor/reftests/824080-6.html
new file mode 100644
index 0000000000..5380fb46d6
--- /dev/null
+++ b/editor/reftests/824080-6.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.body);
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.select();
+ editor.blur();
+ }
+ </script>
+</head>
+<body onload="doTest()">
+<p>normal text</p>
+<textarea id="editor" spellcheck="false">textarea</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-7-ref.html b/editor/reftests/824080-7-ref.html
new file mode 100644
index 0000000000..10162cb1f3
--- /dev/null
+++ b/editor/reftests/824080-7-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.selectionStart = 2;
+ editor.selectionEnd = 4;
+ }
+ </script>
+</head>
+<body onload="doTest()">
+<p>normal text</p>
+<textarea id="editor">textarea</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/824080-7.html b/editor/reftests/824080-7.html
new file mode 100644
index 0000000000..d09e1b5bad
--- /dev/null
+++ b/editor/reftests/824080-7.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="text/javascript">
+ function doTest()
+ {
+ document.getSelection().selectAllChildren(document.body);
+ var editor = document.getElementById("editor");
+ editor.focus();
+ editor.selectionStart = 2;
+ editor.selectionEnd = 4;
+ editor.blur();
+ editor.focus();
+ }
+ </script>
+</head>
+<body onload="doTest()">
+<p>normal text</p>
+<textarea id="editor">textarea</textarea>
+</body>
+</html>
+
diff --git a/editor/reftests/911201-ref.html b/editor/reftests/911201-ref.html
new file mode 100644
index 0000000000..323613ca26
--- /dev/null
+++ b/editor/reftests/911201-ref.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body contenteditable><div contenteditable=false>foo</div></body>
diff --git a/editor/reftests/911201.html b/editor/reftests/911201.html
new file mode 100644
index 0000000000..6b78e1cd2c
--- /dev/null
+++ b/editor/reftests/911201.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body contenteditable onload="document.body.innerHTML='<div contenteditable=false>foo</div>';"></body>
diff --git a/editor/reftests/969773-ref.html b/editor/reftests/969773-ref.html
new file mode 100644
index 0000000000..32ffae7c12
--- /dev/null
+++ b/editor/reftests/969773-ref.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <meta charset="utf-8">
+ <title>Contenteditable Selection Test Case</title>
+ <script>
+ function runTests() {
+ var text = document.getElementById("text");
+
+ text.focus();
+
+ setTimeout(function () {
+ document.body.offsetHeight;
+ document.documentElement.removeAttribute('class');
+ }, 0);
+ }
+ document.addEventListener('MozReftestInvalidate', runTests);
+ </script>
+</head>
+<body>
+ <div>This is a contenteditable.</div>
+ <div id="text" tabindex="0">This is focusable text</div>
+</body>
+</html>
diff --git a/editor/reftests/969773.html b/editor/reftests/969773.html
new file mode 100644
index 0000000000..65ab32cbb4
--- /dev/null
+++ b/editor/reftests/969773.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <meta charset="utf-8">
+ <title>Contenteditable Selection Test Case</title>
+ <script>
+ function runTests() {
+ var editable = document.getElementById("editable");
+ var text = document.getElementById("text");
+
+ editable.focus();
+
+ setTimeout(function () {
+ editable.setAttribute("contenteditable", "false");
+ text.focus();
+ setTimeout(function () {
+ document.body.offsetHeight;
+ document.documentElement.removeAttribute('class');
+ }, 0);
+ }, 0);
+ }
+ document.addEventListener('MozReftestInvalidate', runTests);
+ </script>
+</head>
+<body>
+ <div id="editable" contenteditable="true" tabindex="0" spellcheck="false">This is a contenteditable.</div>
+ <div id="text" tabindex="0">This is focusable text</div>
+</body>
+</html>
diff --git a/editor/reftests/997805-ref.html b/editor/reftests/997805-ref.html
new file mode 100644
index 0000000000..be4592e69b
--- /dev/null
+++ b/editor/reftests/997805-ref.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<textarea placeholder="placeholder"></textarea>
diff --git a/editor/reftests/997805.html b/editor/reftests/997805.html
new file mode 100644
index 0000000000..91750138b3
--- /dev/null
+++ b/editor/reftests/997805.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<textarea placeholder="placeholder"></textarea>
+<script>
+onload = function() {
+ var t = document.querySelector("textarea");
+ t.style.display = "none";
+ t.value = "test";
+ setTimeout(function() {
+ t.style.display = "";
+ t.value = "";
+ document.documentElement.className = "";
+ }, 0);
+};
+</script>
+</html>
diff --git a/editor/reftests/caret_after_reframe-ref.html b/editor/reftests/caret_after_reframe-ref.html
new file mode 100644
index 0000000000..63c49f66ce
--- /dev/null
+++ b/editor/reftests/caret_after_reframe-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input autofocus style="display:block">
+ </body>
+</html>
diff --git a/editor/reftests/caret_after_reframe.html b/editor/reftests/caret_after_reframe.html
new file mode 100644
index 0000000000..5e9c0f1330
--- /dev/null
+++ b/editor/reftests/caret_after_reframe.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <body>
+ <input onfocus="focused()" autofocus>
+ <script>
+ function focused() {
+ var i = document.querySelector("input");
+ i.style.display = "block";
+ document.offsetWidth;
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_focus-ref.html b/editor/reftests/caret_on_focus-ref.html
new file mode 100644
index 0000000000..4282ac7f55
--- /dev/null
+++ b/editor/reftests/caret_on_focus-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ div { min-height: 20px; }
+ </style>
+ </head>
+ <body onload="document.querySelector('div').focus();">
+ <div contenteditable="true"></div>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_focus.html b/editor/reftests/caret_on_focus.html
new file mode 100644
index 0000000000..6dcedb4a4a
--- /dev/null
+++ b/editor/reftests/caret_on_focus.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ div { min-height: 20px; }
+ </style>
+ </head>
+ <body onload="document.querySelector('div').focus();">
+ <div contenteditable="true"> </div>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_positioned-ref.html b/editor/reftests/caret_on_positioned-ref.html
new file mode 100644
index 0000000000..04773979dd
--- /dev/null
+++ b/editor/reftests/caret_on_positioned-ref.html
@@ -0,0 +1,8 @@
+<html><head>
+<title>caret should be visible on stack context contents</title>
+</head><body>
+<div id="d" style="width: 100px; height: 100px; background: none repeat scroll 0% 0% cyan;" contenteditable=""></div>
+<script>
+document.getElementById("d").focus();
+</script>
+</body></html> \ No newline at end of file
diff --git a/editor/reftests/caret_on_positioned.html b/editor/reftests/caret_on_positioned.html
new file mode 100644
index 0000000000..8a4a3c2f3a
--- /dev/null
+++ b/editor/reftests/caret_on_positioned.html
@@ -0,0 +1,8 @@
+<html><head>
+<title>caret should be visible on stack context contents</title>
+</head><body>
+<div id="d" style="position: absolute; width: 100px; height: 100px; background: none repeat scroll 0% 0% cyan;" contenteditable=""></div>
+<script>
+document.getElementById("d").focus();
+</script>
+</body></html> \ No newline at end of file
diff --git a/editor/reftests/caret_on_presshell_reinit-2.html b/editor/reftests/caret_on_presshell_reinit-2.html
new file mode 100644
index 0000000000..444b72dccd
--- /dev/null
+++ b/editor/reftests/caret_on_presshell_reinit-2.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <body>
+ <iframe srcdoc="<body><div></div></body>"></iframe>
+ <script type="text/javascript">
+ onload = function() {
+ var i = document.querySelector("iframe");
+ var win = i.contentWindow;
+ var doc = win.document;
+ var div = doc.querySelector("div");
+ win.getSelection().collapse(div, 0);
+ i.focus();
+ div.contentEditable = true;
+ div.focus();
+ setTimeout(function() {
+ var span = doc.createElement("span");
+ span.appendChild(doc.createTextNode("foo"));
+ div.appendChild(span);
+ div.style.outline = "none"; // remove the focus outline
+ i.style.position = "absolute";
+ document.body.clientWidth;
+ document.documentElement.removeAttribute("class");
+ }, 0);
+ };
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_presshell_reinit-ref.html b/editor/reftests/caret_on_presshell_reinit-ref.html
new file mode 100644
index 0000000000..262c67a748
--- /dev/null
+++ b/editor/reftests/caret_on_presshell_reinit-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div style="position: absolute">
+ <iframe srcdoc="<body contenteditable>foo</body>"></iframe>
+ </div>
+ <script type="text/javascript">
+ onload = function() {
+ var iframe = document.querySelector("iframe");
+ var win = iframe.contentWindow;
+ var body = win.document.body;
+ iframe.focus();
+ body.focus();
+ var sel = win.getSelection();
+ sel.collapse(body.firstChild, 0);
+ };
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_presshell_reinit.html b/editor/reftests/caret_on_presshell_reinit.html
new file mode 100644
index 0000000000..aa10cc585f
--- /dev/null
+++ b/editor/reftests/caret_on_presshell_reinit.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div>
+ <iframe srcdoc="<body contenteditable>foo</body>"></iframe>
+ </div>
+ <script type="text/javascript">
+ onload = function() {
+ var div = document.querySelector("div");
+ div.style.position = "absolute";
+ document.body.clientWidth;
+ var iframe = document.querySelector("iframe");
+ var win = iframe.contentWindow;
+ var body = win.document.body;
+ iframe.focus();
+ body.focus();
+ var sel = win.getSelection();
+ sel.collapse(body.firstChild, 0);
+ };
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/caret_on_textarea_lastline-ref.html b/editor/reftests/caret_on_textarea_lastline-ref.html
new file mode 100644
index 0000000000..9c1040255e
--- /dev/null
+++ b/editor/reftests/caret_on_textarea_lastline-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<body onload="loaded()">
+<script>
+ function loaded() {
+ var t = document.querySelector('textarea');
+ t.selectionStart = t.selectionEnd = t.value.length;
+ t.focus();
+ }
+</script>
+<textarea>foo</textarea>
+</body>
+</html>
diff --git a/editor/reftests/caret_on_textarea_lastline.html b/editor/reftests/caret_on_textarea_lastline.html
new file mode 100644
index 0000000000..24a53bd7d4
--- /dev/null
+++ b/editor/reftests/caret_on_textarea_lastline.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<body onload="loaded()">
+<script>
+ function loaded() {
+ var t = document.querySelector('textarea');
+ t.selectionStart = t.selectionEnd = t.value.length;
+ t.focus();
+ }
+</script>
+<textarea>foo
+</textarea>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-1.html b/editor/reftests/dynamic-1.html
new file mode 100644
index 0000000000..5f2b8b7dcf
--- /dev/null
+++ b/editor/reftests/dynamic-1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text">
+ <script>
+ document.getElementsByTagName("input")[0].value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-overflow-change-ref.html b/editor/reftests/dynamic-overflow-change-ref.html
new file mode 100644
index 0000000000..52e5f5bb0d
--- /dev/null
+++ b/editor/reftests/dynamic-overflow-change-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea rows="2" style="overflow: hidden;">
+ this
+ is
+ a
+ textarea
+ with
+ overflow
+ </textarea>
+ </body>
+</html>
diff --git a/editor/reftests/dynamic-overflow-change.html b/editor/reftests/dynamic-overflow-change.html
new file mode 100644
index 0000000000..57a1b8f74e
--- /dev/null
+++ b/editor/reftests/dynamic-overflow-change.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body onload="document.querySelector('textarea').style.overflow='hidden'">
+ <textarea rows="2">
+ this
+ is
+ a
+ textarea
+ with
+ overflow
+ </textarea>
+ </body>
+</html>
diff --git a/editor/reftests/dynamic-ref.html b/editor/reftests/dynamic-ref.html
new file mode 100644
index 0000000000..07882ee7a0
--- /dev/null
+++ b/editor/reftests/dynamic-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/dynamic-type-1.html b/editor/reftests/dynamic-type-1.html
new file mode 100644
index 0000000000..fb0c3ec684
--- /dev/null
+++ b/editor/reftests/dynamic-type-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="checkbox">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.type = "text";
+ i.value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-type-2.html b/editor/reftests/dynamic-type-2.html
new file mode 100644
index 0000000000..4d99ac06e2
--- /dev/null
+++ b/editor/reftests/dynamic-type-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="checkbox">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.value = "abcdef";
+ i.type = "text";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-type-3.html b/editor/reftests/dynamic-type-3.html
new file mode 100644
index 0000000000..7cf5be6abb
--- /dev/null
+++ b/editor/reftests/dynamic-type-3.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="checkbox" value="foo">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.type = "text";
+ i.value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/dynamic-type-4.html b/editor/reftests/dynamic-type-4.html
new file mode 100644
index 0000000000..7cf5be6abb
--- /dev/null
+++ b/editor/reftests/dynamic-type-4.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="checkbox" value="foo">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.type = "text";
+ i.value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/emptypasswd-1.html b/editor/reftests/emptypasswd-1.html
new file mode 100644
index 0000000000..86775633bd
--- /dev/null
+++ b/editor/reftests/emptypasswd-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password">
+</body>
+</html>
diff --git a/editor/reftests/emptypasswd-2.html b/editor/reftests/emptypasswd-2.html
new file mode 100644
index 0000000000..6e33f46b1c
--- /dev/null
+++ b/editor/reftests/emptypasswd-2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value="abcdef">
+ <script>
+ document.getElementsByTagName("input")[0].value = "";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/emptypasswd-ref.html b/editor/reftests/emptypasswd-ref.html
new file mode 100644
index 0000000000..7f09f6e8be
--- /dev/null
+++ b/editor/reftests/emptypasswd-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text">
+</body>
+</html>
diff --git a/editor/reftests/exec-command-indent-ws-ref.html b/editor/reftests/exec-command-indent-ws-ref.html
new file mode 100644
index 0000000000..0e65e0d3fb
--- /dev/null
+++ b/editor/reftests/exec-command-indent-ws-ref.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html><head>
+ <meta charset="utf-8">
+ <title>Reference for bug </title>
+<style>
+html,body {
+ color:black; background-color:white; font:10px/1 monospace; padding:0; margin:0;
+}
+
+li::before { content: " list-item counter:" counters(list-item,".") " "; }
+ol,ul { border:1px solid; margin: 0; }
+div > ul { counter-reset: list-item 7; }
+</style>
+</head>
+<body>
+
+<div contenteditable>
+<ol start=8>
+ <li>A</li>
+ <ol><li class="indent">B</li></ol>
+ <li>C</li>
+</ol>
+</div>
+
+<div contenteditable>
+<ol start=8>
+ <li>A</li>
+ <ol><li class="indent">B</li></ol>
+ <li>C</li>
+</ol>
+</div>
+
+<div contenteditable>
+<ul>
+ <li>A</li>
+ <ul><li class="indent">B</li></ul>
+ <li>C</li>
+</ul>
+</div>
+
+<div contenteditable>
+<ul>
+ <li>A</li>
+ <ul><li class="indent">B</li></ul>
+ <li>C</li>
+</ul>
+</div>
+
+<!-- now the same as above without whitespace: -->
+
+<div contenteditable><ol start=8><li>A</li><ol><li class="indent">B</li></ol><li>C</li></ol></div>
+<div contenteditable><ol start=8><li>A</li><ol><li class="indent">B</li></ol><li>C</li></ol></div>
+<div contenteditable><ul><li>A</li><ul><li class="indent">B</li></ul><li>C</li></ul></div>
+<div contenteditable><ul><li>A</li><ul><li class="indent">B</li></ul><li>C</li></ul></div>
+
+</body>
+</html>
diff --git a/editor/reftests/exec-command-indent-ws.html b/editor/reftests/exec-command-indent-ws.html
new file mode 100644
index 0000000000..00d69aaa6e
--- /dev/null
+++ b/editor/reftests/exec-command-indent-ws.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html><head>
+ <meta charset="utf-8">
+ <title>Testcase for bug </title>
+<style>
+html,body {
+ color:black; background-color:white; font:10px/1 monospace; padding:0; margin:0;
+}
+
+li::before { content: " list-item counter:" counters(list-item,".") " "; }
+ol,ul { border:1px solid; margin: 0; }
+div > ul { counter-reset: list-item 7; }
+</style>
+</head>
+<body>
+
+<div contenteditable>
+<ol start=8>
+ <li>A</li>
+ <ol></ol>
+ <li class="indent">B</li>
+ <li>C</li>
+</ol>
+</div>
+
+<div contenteditable>
+<ol start=8>
+ <li>A</li>
+ <li class="indent">B</li>
+ <ol></ol>
+ <li>C</li>
+</ol>
+</div>
+
+<div contenteditable>
+<ul>
+ <li>A</li>
+ <ul></ul>
+ <li class="indent">B</li>
+ <li>C</li>
+</ul>
+</div>
+
+<div contenteditable>
+<ul>
+ <li>A</li>
+ <li class="indent">B</li>
+ <ul></ul>
+ <li>C</li>
+</ul>
+</div>
+
+<!-- now the same as above without whitespace: -->
+
+<div contenteditable><ol start=8><li>A</li><ol></ol><li class="indent">B</li><li>C</li></ol></div>
+<div contenteditable><ol start=8><li>A</li><li class="indent">B</li><ol></ol><li>C</li></ol></div>
+<div contenteditable><ul><li>A</li><ul></ul><li class="indent">B</li><li>C</li></ul></div>
+<div contenteditable><ul><li>A</li><li class="indent">B</li><ul></ul><li>C</li></ul></div>
+
+<script>
+function test() {
+ [...document.querySelectorAll('.indent')].forEach(function(elm) {
+ var r = document.createRange();
+ r.setStart(elm.firstChild,0)
+ r.setEnd(elm.firstChild,0)
+ window.getSelection().addRange(r);
+ document.execCommand("indent");
+ window.getSelection().removeAllRanges();
+ });
+}
+
+test();
+document.activeElement.blur();
+</script>
+
+</body>
+</html>
diff --git a/editor/reftests/inline-table-editor-position-after-updating-table-size-from-input-event-listener-ref.html b/editor/reftests/inline-table-editor-position-after-updating-table-size-from-input-event-listener-ref.html
new file mode 100644
index 0000000000..d8fa8861aa
--- /dev/null
+++ b/editor/reftests/inline-table-editor-position-after-updating-table-size-from-input-event-listener-ref.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html lang="en-US" class="reftest-wait">
+<head>
+<meta charset="utf-8">
+<title>Inline table editor should be positioned correctly even if modified the table from an input event listener</title>
+<script>
+addEventListener("load", async () => {
+ const cell = document.querySelector("td + td");
+ document.body.focus();
+ getSelection().collapse(cell, 0);
+ document.execCommand("enableObjectResizing", false, "true");
+ document.execCommand("enableInlineTableEditing", false, "true");
+ requestAnimationFrame(
+ () => requestAnimationFrame(
+ () => document.documentElement.removeAttribute("class")
+ )
+ );
+}, {once: true});
+</script>
+</head>
+<body contenteditable="">
+<table border="1">
+<td>Cell</td><td style="width:100px"><br></td>
+</table>
+</body>
+</html> \ No newline at end of file
diff --git a/editor/reftests/inline-table-editor-position-after-updating-table-size-from-input-event-listener.html b/editor/reftests/inline-table-editor-position-after-updating-table-size-from-input-event-listener.html
new file mode 100644
index 0000000000..c6702c47c9
--- /dev/null
+++ b/editor/reftests/inline-table-editor-position-after-updating-table-size-from-input-event-listener.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<html lang="en-US" class="reftest-wait">
+<head>
+<meta charset="utf-8">
+<title>Inline table editor should be positioned correctly even if modified the table from an input event listener</title>
+<script>
+addEventListener("load", async () => {
+ const cell = document.querySelector("td");
+ document.body.focus();
+ getSelection().collapse(cell.firstChild, 0);
+ document.execCommand("enableObjectResizing", false, "true");
+ document.execCommand("enableInlineTableEditing", false, "true");
+ const nsITableEditor =
+ SpecialPowers.wrap(window).docShell.editingSession.
+ getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+ document.body.addEventListener("input", event => {
+ cell.nextSibling.setAttribute("style", "width:100px");
+ }, {once: true});
+ nsITableEditor.insertTableColumn(1, true);
+ requestAnimationFrame(
+ () => requestAnimationFrame(
+ () => document.documentElement.removeAttribute("class")
+ )
+ );
+}, {once: true});
+</script>
+</head>
+<body contenteditable="">
+<table border="1">
+<td>Cell</td>
+</table>
+</body>
+</html> \ No newline at end of file
diff --git a/editor/reftests/input-text-notheme-onfocus-reframe-ref.html b/editor/reftests/input-text-notheme-onfocus-reframe-ref.html
new file mode 100644
index 0000000000..e97f55f160
--- /dev/null
+++ b/editor/reftests/input-text-notheme-onfocus-reframe-ref.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+ <title>bug 536421</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<style>
+input { border:1px solid blue; }
+</style>
+</head>
+<body onload="doTest()">
+ <input value="test" id="textbox" onfocus="triggerBug();" type="text">
+ <script type="text/javascript">
+ function finishTest()
+ {
+ document.documentElement.removeAttribute("class");
+ }
+ function triggerBug()
+ {
+ finishTest();
+ }
+ function doTest()
+ {
+ var t = document.getElementById("textbox");
+ t.focus();
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/input-text-notheme-onfocus-reframe.html b/editor/reftests/input-text-notheme-onfocus-reframe.html
new file mode 100644
index 0000000000..19bc273d17
--- /dev/null
+++ b/editor/reftests/input-text-notheme-onfocus-reframe.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+ <title>bug 536421</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<style>
+input { border:1px solid blue; }
+</style>
+</head>
+<body onload="doTest()">
+ <input value="test" id="textbox" onfocus="triggerBug();" type="text">
+ <script type="text/javascript">
+ function finishTest()
+ {
+ document.documentElement.removeAttribute("class");
+ }
+ function triggerBug()
+ {
+ var t = document.getElementById("textbox");
+ t.style.display = "none";
+ document.body.offsetWidth;
+ t.style.display = "";
+ finishTest();
+ }
+ function doTest()
+ {
+ var t = document.getElementById("textbox");
+ t.focus();
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/input-text-onfocus-reframe-ref.html b/editor/reftests/input-text-onfocus-reframe-ref.html
new file mode 100644
index 0000000000..e177578978
--- /dev/null
+++ b/editor/reftests/input-text-onfocus-reframe-ref.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+ <title>bug 536421</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+</head>
+<body onload="doTest()">
+ <input value="test" id="textbox" onfocus="triggerBug();" type="text">
+ <script type="text/javascript">
+ function finishTest()
+ {
+ document.documentElement.removeAttribute("class");
+ }
+ function triggerBug()
+ {
+ finishTest();
+ }
+ function doTest()
+ {
+ var t = document.getElementById("textbox");
+ t.focus();
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/input-text-onfocus-reframe.html b/editor/reftests/input-text-onfocus-reframe.html
new file mode 100644
index 0000000000..339ef95c66
--- /dev/null
+++ b/editor/reftests/input-text-onfocus-reframe.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+ <title>bug 536421</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+</head>
+<body onload="doTest()">
+ <input value="test" id="textbox" onfocus="triggerBug();" type="text">
+ <script type="text/javascript">
+ function finishTest()
+ {
+ document.documentElement.removeAttribute("class");
+ }
+ function triggerBug()
+ {
+ var t = document.getElementById("textbox");
+ t.style.display = "none";
+ document.body.offsetWidth;
+ t.style.display = "";
+ finishTest();
+ }
+ function doTest()
+ {
+ var t = document.getElementById("textbox");
+ t.focus();
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/newline-1.html b/editor/reftests/newline-1.html
new file mode 100644
index 0000000000..5a7ce8c195
--- /dev/null
+++ b/editor/reftests/newline-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="aaa&#10;bbb">
+</body>
+</html>
diff --git a/editor/reftests/newline-2.html b/editor/reftests/newline-2.html
new file mode 100644
index 0000000000..7965bc8604
--- /dev/null
+++ b/editor/reftests/newline-2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="&#10;aaa bbb">
+</body>
+</html>
diff --git a/editor/reftests/newline-3.html b/editor/reftests/newline-3.html
new file mode 100644
index 0000000000..18760df4cf
--- /dev/null
+++ b/editor/reftests/newline-3.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="aaa bbb&#10;">
+</body>
+</html>
diff --git a/editor/reftests/newline-4.html b/editor/reftests/newline-4.html
new file mode 100644
index 0000000000..2f51eaa20b
--- /dev/null
+++ b/editor/reftests/newline-4.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="a&#10;a&#10;a&#10; &#10;b&#10;b&#10;b">
+</body>
+</html>
diff --git a/editor/reftests/newline-ref.html b/editor/reftests/newline-ref.html
new file mode 100644
index 0000000000..3630626ddc
--- /dev/null
+++ b/editor/reftests/newline-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="aaa bbb">
+</body>
+</html>
diff --git a/editor/reftests/nobogusnode-1.html b/editor/reftests/nobogusnode-1.html
new file mode 100644
index 0000000000..450d6b1e51
--- /dev/null
+++ b/editor/reftests/nobogusnode-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body contenteditable>
+ This is a test.
+</body>
+</html>
diff --git a/editor/reftests/nobogusnode-2.html b/editor/reftests/nobogusnode-2.html
new file mode 100644
index 0000000000..532e597403
--- /dev/null
+++ b/editor/reftests/nobogusnode-2.html
@@ -0,0 +1,5 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body contenteditable="true">
+ This is a test.
+</body>
+</html>
diff --git a/editor/reftests/nobogusnode-ref.html b/editor/reftests/nobogusnode-ref.html
new file mode 100644
index 0000000000..052a53b51a
--- /dev/null
+++ b/editor/reftests/nobogusnode-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ This is a test.
+</body>
+</html>
diff --git a/editor/reftests/passwd-1.html b/editor/reftests/passwd-1.html
new file mode 100644
index 0000000000..f6f21d84f9
--- /dev/null
+++ b/editor/reftests/passwd-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value="123456">
+</body>
+</html>
diff --git a/editor/reftests/passwd-2.html b/editor/reftests/passwd-2.html
new file mode 100644
index 0000000000..07882ee7a0
--- /dev/null
+++ b/editor/reftests/passwd-2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/passwd-3.html b/editor/reftests/passwd-3.html
new file mode 100644
index 0000000000..3e1e715eb9
--- /dev/null
+++ b/editor/reftests/passwd-3.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password">
+ <script>
+ document.getElementsByTagName("input")[0].value = "abcdef";
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/passwd-4.html b/editor/reftests/passwd-4.html
new file mode 100644
index 0000000000..607a22ae41
--- /dev/null
+++ b/editor/reftests/passwd-4.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<!--
+Make sure that focusing a password text element does not
+cause a non-breaking space character to show up.
+-->
+<html class="reftest-wait">
+<body onload="loaded()">
+ <input type="password">
+ <script>
+ function loaded() {
+ var i = document.getElementsByTagName("input")[0];
+ i.focus();
+ i.value += "abcdef";
+ i.blur();
+ document.documentElement.className = "";
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/passwd-5-with-Preview.html b/editor/reftests/passwd-5-with-Preview.html
new file mode 100644
index 0000000000..d95382deb3
--- /dev/null
+++ b/editor/reftests/passwd-5-with-Preview.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value=" "><!-- even only whitespace, text frame should be created -->
+</body>
+</html>
diff --git a/editor/reftests/passwd-5-with-TextEditor.html b/editor/reftests/passwd-5-with-TextEditor.html
new file mode 100644
index 0000000000..2b6c5958e8
--- /dev/null
+++ b/editor/reftests/passwd-5-with-TextEditor.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body onload="document.getElementsByTagName('input')[0].focus();
+ document.getElementsByTagName('input')[0].blur();
+ document.documentElement.removeAttribute('class');">
+ <input type="password" value=" "><!-- even only whitespace, text frame should be created -->
+</body>
+</html>
diff --git a/editor/reftests/passwd-6-ref.html b/editor/reftests/passwd-6-ref.html
new file mode 100644
index 0000000000..e66f4e0b8c
--- /dev/null
+++ b/editor/reftests/passwd-6-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value="012345678901234">
+</body>
+</html>
diff --git a/editor/reftests/passwd-6-with-Preview.html b/editor/reftests/passwd-6-with-Preview.html
new file mode 100644
index 0000000000..745ced2251
--- /dev/null
+++ b/editor/reftests/passwd-6-with-Preview.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value="&#x1f914;&#x1f9b8;&#x1f3fd;&#x200D;&#x2640;&#xfe0f;&#x8fba;&#xe0101;&#x915;&#x94d;&zwj;"
+ ><!-- Simple Emoji (a surrogate pair in UTF-16),
+ Complicate Emoji (Woman Superhero: Medium Skin Tone),
+ Kanji with IVS,
+ 2 devanÄgarÄ« characters followed by ZWJ -->
+</body>
+</html>
diff --git a/editor/reftests/passwd-6-with-TextEditor.html b/editor/reftests/passwd-6-with-TextEditor.html
new file mode 100644
index 0000000000..f9fcb18924
--- /dev/null
+++ b/editor/reftests/passwd-6-with-TextEditor.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body onload="document.getElementsByTagName('input')[0].focus();
+ document.getElementsByTagName('input')[0].blur();
+ document.documentElement.removeAttribute('class');">
+ <input type="password" value="&#x1f914;&#x1f9b8;&#x1f3fd;&#x200D;&#x2640;&#xfe0f;&#x8fba;&#xe0101;&#x915;&#x94d;&zwj;"
+ ><!-- Simple Emoji (a surrogate pair in UTF-16),
+ Complicate Emoji (Woman Superhero: Medium Skin Tone),
+ Kanji with IVS,
+ 2 devanÄgarÄ« characters followed by ZWJ -->
+</body>
+</html>
diff --git a/editor/reftests/passwd-7-with-Preview.html b/editor/reftests/passwd-7-with-Preview.html
new file mode 100644
index 0000000000..c101743ce0
--- /dev/null
+++ b/editor/reftests/passwd-7-with-Preview.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value="&#x3042;&#x6f22;&#x0410;&#x0600;&#x0E01;&#xE001;"><!-- original character shouldn't affect the mask's font -->
+</body>
+</html>
diff --git a/editor/reftests/passwd-7-with-TextEditor.html b/editor/reftests/passwd-7-with-TextEditor.html
new file mode 100644
index 0000000000..f2940db625
--- /dev/null
+++ b/editor/reftests/passwd-7-with-TextEditor.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body onload="document.getElementsByTagName('input')[0].focus();
+ document.getElementsByTagName('input')[0].blur();
+ document.documentElement.removeAttribute('class');">
+ <input type="password" value="&#x3042;&#x6f22;&#x0410;&#x0600;&#x0E01;&#xE001;"><!-- original character shouldn't affect the mask's font -->
+</body>
+</html>
diff --git a/editor/reftests/passwd-8-with-Preview.html b/editor/reftests/passwd-8-with-Preview.html
new file mode 100644
index 0000000000..2cc4ba243e
--- /dev/null
+++ b/editor/reftests/passwd-8-with-Preview.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body onload="document.getElementsByTagName('input')[0].type = 'password';
+ document.documentElement.removeAttribute('class');">
+ <input type="text" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/passwd-8-with-TextEditor.html b/editor/reftests/passwd-8-with-TextEditor.html
new file mode 100644
index 0000000000..d4f9aa7d6f
--- /dev/null
+++ b/editor/reftests/passwd-8-with-TextEditor.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body onload="document.getElementsByTagName('input')[0].focus();
+ document.getElementsByTagName('input')[0].blur();
+ document.getElementsByTagName('input')[0].type = 'password';
+ document.documentElement.removeAttribute('class');">
+ <input type="text" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/passwd-9-with-Preview.html b/editor/reftests/passwd-9-with-Preview.html
new file mode 100644
index 0000000000..528d8f8ac5
--- /dev/null
+++ b/editor/reftests/passwd-9-with-Preview.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body onload="document.getElementsByTagName('input')[0].focus();
+ document.getElementsByTagName('input')[0].blur();
+ document.getElementsByTagName('input')[0].type = 'text';
+ document.documentElement.removeAttribute('class');">
+ <input type="password" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/passwd-9-with-TextEditor.html b/editor/reftests/passwd-9-with-TextEditor.html
new file mode 100644
index 0000000000..d775c44238
--- /dev/null
+++ b/editor/reftests/passwd-9-with-TextEditor.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body onload="document.getElementsByTagName('input')[0].type = 'text';
+ document.documentElement.removeAttribute('class');">
+ <input type="password" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/passwd-ref.html b/editor/reftests/passwd-ref.html
new file mode 100644
index 0000000000..b203fa7d54
--- /dev/null
+++ b/editor/reftests/passwd-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="password" value="abcdef">
+</body>
+</html>
diff --git a/editor/reftests/readonly-editable-ref.html b/editor/reftests/readonly-editable-ref.html
new file mode 100644
index 0000000000..99f1e51017
--- /dev/null
+++ b/editor/reftests/readonly-editable-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input>
+ <input readonly>
+ <input type=password>
+ <input type=password readonly>
+ <input type=email>
+ <input type=email readonly>
+ <textarea></textarea>
+ <textarea readonly></textarea>
+ </body>
+</html>
diff --git a/editor/reftests/readonly-editable.html b/editor/reftests/readonly-editable.html
new file mode 100644
index 0000000000..d2e48f4295
--- /dev/null
+++ b/editor/reftests/readonly-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ :read-write + span {
+ display: none;
+ }
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body contenteditable>
+ <input><span>hide me</span>
+ <input readonly><span>hide me</span>
+ <input type=password><span>hide me</span>
+ <input type=password readonly><span>hide me</span>
+ <input type=email><span>hide me</span>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/readonly-non-editable-ref.html b/editor/reftests/readonly-non-editable-ref.html
new file mode 100644
index 0000000000..a91071e42c
--- /dev/null
+++ b/editor/reftests/readonly-non-editable-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body>
+ <input><span>hide me</span>
+ <input readonly>
+ <input type=password><span>hide me</span>
+ <input type=password readonly>
+ <input type=email><span>hide me</span>
+ <input type=email readonly>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea>
+ </body>
+</html>
diff --git a/editor/reftests/readonly-non-editable.html b/editor/reftests/readonly-non-editable.html
new file mode 100644
index 0000000000..42cd187133
--- /dev/null
+++ b/editor/reftests/readonly-non-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ :read-only + span {
+ display: none;
+ }
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body>
+ <input><span>hide me</span>
+ <input readonly><span>hide me</span>
+ <input type=password><span>hide me</span>
+ <input type=password readonly><span>hide me</span>
+ <input type=email><span>hide me</span>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/readwrite-editable-ref.html b/editor/reftests/readwrite-editable-ref.html
new file mode 100644
index 0000000000..99f1e51017
--- /dev/null
+++ b/editor/reftests/readwrite-editable-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input>
+ <input readonly>
+ <input type=password>
+ <input type=password readonly>
+ <input type=email>
+ <input type=email readonly>
+ <textarea></textarea>
+ <textarea readonly></textarea>
+ </body>
+</html>
diff --git a/editor/reftests/readwrite-editable.html b/editor/reftests/readwrite-editable.html
new file mode 100644
index 0000000000..d2e48f4295
--- /dev/null
+++ b/editor/reftests/readwrite-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ :read-write + span {
+ display: none;
+ }
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body contenteditable>
+ <input><span>hide me</span>
+ <input readonly><span>hide me</span>
+ <input type=password><span>hide me</span>
+ <input type=password readonly><span>hide me</span>
+ <input type=email><span>hide me</span>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/readwrite-non-editable-ref.html b/editor/reftests/readwrite-non-editable-ref.html
new file mode 100644
index 0000000000..12e1c46c0a
--- /dev/null
+++ b/editor/reftests/readwrite-non-editable-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body>
+ <input>
+ <input readonly><span>hide me</span>
+ <input type=password>
+ <input type=password readonly><span>hide me</span>
+ <input type=email>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/readwrite-non-editable.html b/editor/reftests/readwrite-non-editable.html
new file mode 100644
index 0000000000..fd4807f4f0
--- /dev/null
+++ b/editor/reftests/readwrite-non-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ :read-write + span {
+ display: none;
+ }
+ span {
+ color: transparent; /* workaround for bug 617524 */
+ outline: 1px solid green;
+ }
+ </style>
+ </head>
+ <body>
+ <input><span>hide me</span>
+ <input readonly><span>hide me</span>
+ <input type=password><span>hide me</span>
+ <input type=password readonly><span>hide me</span>
+ <input type=email><span>hide me</span>
+ <input type=email readonly><span>hide me</span>
+ <textarea></textarea><span>hide me</span>
+ <textarea readonly></textarea><span>hide me</span>
+ </body>
+</html>
diff --git a/editor/reftests/reftest.list b/editor/reftests/reftest.list
new file mode 100644
index 0000000000..0424b73c18
--- /dev/null
+++ b/editor/reftests/reftest.list
@@ -0,0 +1,157 @@
+# include the XUL reftests, except on Android which doesn't have XUL.
+skip-if(Android) include xul/reftest.list
+
+!= newline-1.html newline-ref.html
+== newline-2.html newline-ref.html
+== newline-3.html newline-ref.html
+== newline-4.html newline-ref.html
+== dynamic-1.html dynamic-ref.html
+== dynamic-type-1.html dynamic-ref.html
+== dynamic-type-2.html dynamic-ref.html
+== dynamic-type-3.html dynamic-ref.html
+== dynamic-type-4.html dynamic-ref.html
+== passwd-1.html passwd-ref.html
+!= passwd-2.html passwd-ref.html
+== passwd-3.html passwd-ref.html
+needs-focus == passwd-4.html passwd-ref.html
+== passwd-5-with-Preview.html passwd-ref.html
+needs-focus == passwd-5-with-TextEditor.html passwd-ref.html
+== passwd-6-with-Preview.html passwd-6-ref.html
+needs-focus == passwd-6-with-TextEditor.html passwd-6-ref.html
+== passwd-7-with-Preview.html passwd-ref.html
+needs-focus == passwd-7-with-TextEditor.html passwd-ref.html
+== passwd-8-with-Preview.html passwd-ref.html
+needs-focus == passwd-8-with-TextEditor.html passwd-ref.html
+== passwd-9-with-Preview.html passwd-2.html
+needs-focus == passwd-9-with-TextEditor.html passwd-2.html
+== emptypasswd-1.html emptypasswd-ref.html
+== emptypasswd-2.html emptypasswd-ref.html
+== caret_on_positioned.html caret_on_positioned-ref.html
+# Android turns off spellchecker (Bug 1541697)
+skip-if(Android) skip-if(cocoaWidget) needs-focus != spellcheck-input-disabled.html spellcheck-input-ref.html # Bug 1666056
+skip-if(Android) == spellcheck-input-attr-before.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-attr-before.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-attr-after.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-attr-after.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-attr-inherit.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-attr-inherit.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-attr-dynamic.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-attr-dynamic.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-attr-dynamic-inherit.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-attr-dynamic-inherit.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-property-dynamic.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-property-dynamic.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-property-dynamic-inherit.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-property-dynamic-inherit.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-attr-dynamic-override.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-attr-dynamic-override.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-attr-dynamic-override-inherit.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-attr-dynamic-override-inherit.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-property-dynamic-override.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-property-dynamic-override.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-input-property-dynamic-override-inherit.html spellcheck-input-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-input-property-dynamic-override-inherit.html spellcheck-input-ref.html
+skip-if(Android) == spellcheck-textarea-attr.html spellcheck-textarea-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-attr.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus == spellcheck-textarea-focused.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus == spellcheck-textarea-focused-reframe.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus == spellcheck-textarea-focused-notreadonly.html spellcheck-textarea-ref2.html
+skip-if(Android) needs-focus != spellcheck-textarea-nofocus.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-disabled.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-attr-inherit.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-attr-dynamic.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-attr-dynamic-inherit.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-property-dynamic.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-property-dynamic-inherit.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-attr-dynamic-override.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-attr-dynamic-override-inherit.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-property-dynamic-override.html spellcheck-textarea-ref.html
+skip-if(Android) needs-focus != spellcheck-textarea-property-dynamic-override-inherit.html spellcheck-textarea-ref.html
+needs-focus == caret_on_focus.html caret_on_focus-ref.html
+needs-focus fails-if(useDrawSnapshot) != caret_on_textarea_lastline.html caret_on_textarea_lastline-ref.html
+fuzzy-if(Android,0-1,0-1) needs-focus == input-text-onfocus-reframe.html input-text-onfocus-reframe-ref.html
+fuzzy(0-5,0-1) needs-focus == input-text-notheme-onfocus-reframe.html input-text-notheme-onfocus-reframe-ref.html
+needs-focus == caret_after_reframe.html caret_after_reframe-ref.html
+== nobogusnode-1.html nobogusnode-ref.html
+== nobogusnode-2.html nobogusnode-ref.html
+# Android turns off spellchecker (Bug 1541697)
+skip-if(Android) fuzzy(0-3,0-1) == spellcheck-hyphen-valid.html spellcheck-hyphen-valid-ref.html
+skip-if(Android) needs-focus != spellcheck-hyphen-invalid.html spellcheck-hyphen-invalid-ref.html
+skip-if(Android) == spellcheck-slash-valid.html spellcheck-slash-valid-ref.html
+skip-if(Android) == spellcheck-period-valid.html spellcheck-period-valid-ref.html
+skip-if(Android) == spellcheck-space-valid.html spellcheck-space-valid-ref.html
+skip-if(Android) == spellcheck-comma-valid.html spellcheck-comma-valid-ref.html
+skip-if(Android) == spellcheck-hyphen-multiple-valid.html spellcheck-hyphen-multiple-valid-ref.html
+skip-if(Android) needs-focus != spellcheck-hyphen-multiple-invalid.html spellcheck-hyphen-multiple-invalid-ref.html
+skip-if(Android) == spellcheck-dotafterquote-valid.html spellcheck-dotafterquote-valid-ref.html
+skip-if(Android) == spellcheck-url-valid.html spellcheck-url-valid-ref.html
+skip-if(Android) fuzzy(0-32,0-1) needs-focus == spellcheck-non-latin-arabic.html spellcheck-non-latin-arabic-ref.html
+skip-if(Android) needs-focus == spellcheck-non-latin-chinese-simplified.html spellcheck-non-latin-chinese-simplified-ref.html
+skip-if(Android) needs-focus == spellcheck-non-latin-chinese-traditional.html spellcheck-non-latin-chinese-traditional-ref.html
+skip-if(Android) needs-focus == spellcheck-non-latin-hebrew.html spellcheck-non-latin-hebrew-ref.html
+skip-if(Android) needs-focus == spellcheck-non-latin-japanese.html spellcheck-non-latin-japanese-ref.html
+skip-if(Android) fuzzy(0-3,0-1) needs-focus == spellcheck-non-latin-korean.html spellcheck-non-latin-korean-ref.html
+== unneeded_scroll.html unneeded_scroll-ref.html
+== caret_on_presshell_reinit.html caret_on_presshell_reinit-ref.html
+fuzzy-if(browserIsRemote,0-255,0-3) asserts-if(browserIsRemote,0-3) == caret_on_presshell_reinit-2.html caret_on_presshell_reinit-ref.html # bug 959132 for assertions
+fuzzy-if(asyncPan&&!layersGPUAccelerated,0-102,0-2824) == 642800.html 642800-ref.html
+needs-focus == selection_visibility_after_reframe.html selection_visibility_after_reframe-ref.html
+needs-focus != selection_visibility_after_reframe-2.html selection_visibility_after_reframe-ref.html
+needs-focus != selection_visibility_after_reframe-3.html selection_visibility_after_reframe-ref.html
+== 672709.html 672709-ref.html
+== 338427-1.html 338427-1-ref.html
+needs-focus == 674212-spellcheck.html 674212-spellcheck-ref.html
+needs-focus == 338427-2.html 338427-2-ref.html
+needs-focus == 338427-3.html 338427-3-ref.html
+needs-focus == 462758-grabbers-resizers.html 462758-grabbers-resizers-ref.html
+== readwrite-non-editable.html readwrite-non-editable-ref.html
+== readwrite-editable.html readwrite-editable-ref.html
+== readonly-non-editable.html readonly-non-editable-ref.html
+== readonly-editable.html readonly-editable-ref.html
+== dynamic-overflow-change.html dynamic-overflow-change-ref.html
+== 694880-1.html 694880-ref.html
+== 694880-2.html 694880-ref.html
+== 694880-3.html 694880-ref.html
+== 388980-1.html 388980-1-ref.html
+# Android turns off spellchecker (Bug 1541697)
+skip-if(Android) needs-focus == spellcheck-superscript-1.html spellcheck-superscript-1-ref.html
+skip-if(Android) needs-focus != spellcheck-superscript-2.html spellcheck-superscript-2-ref.html
+fuzzy(0-1,0-3400) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-1.html 824080-1-ref.html
+fuzzy-if(OSX,0-1,0-1) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-2.html 824080-2-ref.html #Bug 1313253
+fuzzy-if(OSX,0-1,0-1) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-3.html 824080-3-ref.html #Bug 1312951
+needs-focus != 824080-2.html 824080-3.html
+fuzzy(0-1,0-3200) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-4.html 824080-4-ref.html
+fuzzy(0-2,0-1800) needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-5.html 824080-5-ref.html
+needs-focus != 824080-4.html 824080-5.html
+needs-focus == 824080-6.html 824080-6-ref.html
+needs-focus pref(layout.accessiblecaret.enabled,false) pref(layout.accessiblecaret.enabled_on_touch,false) == 824080-7.html 824080-7-ref.html
+needs-focus != 824080-6.html 824080-7.html
+# Android turns off spell checker (Bug 1541697)
+# Bug 674927: copy spellcheck-textarea tests to contenteditable
+skip-if(Android) == spellcheck-contenteditable-attr.html spellcheck-contenteditable-nofocus-ref.html
+skip-if(Android) needs-focus != spellcheck-contenteditable-attr.html spellcheck-contenteditable-ref.html
+skip-if(Android) needs-focus == spellcheck-contenteditable-focused.html spellcheck-contenteditable-ref.html
+skip-if(Android) needs-focus == spellcheck-contenteditable-focused-reframe.html spellcheck-contenteditable-ref.html
+skip-if(Android) == spellcheck-contenteditable-nofocus-1.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-nofocus-2.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-disabled.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-disabled-partial.html spellcheck-contenteditable-disabled-partial-ref.html
+skip-if(Android) == spellcheck-contenteditable-attr-inherit.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-attr-dynamic.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-attr-dynamic-inherit.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-property-dynamic.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-property-dynamic-inherit.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-attr-dynamic-override.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-attr-dynamic-override-inherit.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-property-dynamic-override.html spellcheck-contenteditable-disabled-ref.html
+skip-if(Android) == spellcheck-contenteditable-property-dynamic-override-inherit.html spellcheck-contenteditable-disabled-ref.html
+== 911201.html 911201-ref.html
+needs-focus == 969773.html 969773-ref.html
+fuzzy(0-1,0-220) == 997805.html 997805-ref.html
+fuzzy(0-1,0-220) == 1088158.html 1088158-ref.html
+fuzzy-if(Android,0-1,0-1) needs-focus == 1443902-1.html 1443902-1-ref.html
+fuzzy-if(Android,0-1,0-1) needs-focus == 1443902-2.html 1443902-2-ref.html
+fuzzy-if(Android,0-1,0-1) needs-focus == 1443902-3.html 1443902-3-ref.html
+fuzzy-if(Android,0-1,0-1) needs-focus == 1443902-4.html 1443902-4-ref.html
+== exec-command-indent-ws.html exec-command-indent-ws-ref.html
+needs-focus == inline-table-editor-position-after-updating-table-size-from-input-event-listener.html inline-table-editor-position-after-updating-table-size-from-input-event-listener-ref.html
diff --git a/editor/reftests/selection_visibility_after_reframe-2.html b/editor/reftests/selection_visibility_after_reframe-2.html
new file mode 100644
index 0000000000..579bbedd88
--- /dev/null
+++ b/editor/reftests/selection_visibility_after_reframe-2.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input value="foo">
+ <script>
+ var i = document.querySelector("input");
+ i.focus();
+ i.selectionStart = 1;
+ i.selectionEnd = 2;
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/selection_visibility_after_reframe-3.html b/editor/reftests/selection_visibility_after_reframe-3.html
new file mode 100644
index 0000000000..1a816a88cc
--- /dev/null
+++ b/editor/reftests/selection_visibility_after_reframe-3.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input value="foo">
+ <script>
+ var i = document.querySelector("input");
+ i.focus();
+ i.selectionStart = 1;
+ i.selectionEnd = 2;
+ i.style.display = "none";
+ document.body.clientHeight;
+ i.style.display = "";
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/selection_visibility_after_reframe-ref.html b/editor/reftests/selection_visibility_after_reframe-ref.html
new file mode 100644
index 0000000000..c227b39c8f
--- /dev/null
+++ b/editor/reftests/selection_visibility_after_reframe-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input value="foo">
+ </body>
+</html>
diff --git a/editor/reftests/selection_visibility_after_reframe.html b/editor/reftests/selection_visibility_after_reframe.html
new file mode 100644
index 0000000000..b72cec8296
--- /dev/null
+++ b/editor/reftests/selection_visibility_after_reframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input value="foo">
+ <script>
+ var i = document.querySelector("input");
+ i.selectionStart = 1;
+ i.selectionEnd = 2;
+ document.body.clientHeight;
+ i.style.display = "none";
+ document.body.clientHeight;
+ i.style.display = "";
+ </script>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-comma-valid-ref.html b/editor/reftests/spellcheck-comma-valid-ref.html
new file mode 100644
index 0000000000..d5856e06fb
--- /dev/null
+++ b/editor/reftests/spellcheck-comma-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">good,nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-comma-valid.html b/editor/reftests/spellcheck-comma-valid.html
new file mode 100644
index 0000000000..768cdbcf2c
--- /dev/null
+++ b/editor/reftests/spellcheck-comma-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>good,nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-dynamic-inherit.html b/editor/reftests/spellcheck-contenteditable-attr-dynamic-inherit.html
new file mode 100644
index 0000000000..aa4e47c2ca
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-dynamic-override-inherit.html b/editor/reftests/spellcheck-contenteditable-attr-dynamic-override-inherit.html
new file mode 100644
index 0000000000..1b4a0ab3b9
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="true">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-dynamic-override.html b/editor/reftests/spellcheck-contenteditable-attr-dynamic-override.html
new file mode 100644
index 0000000000..e3a4d90772
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable spellcheck="true">blahblahblah</div>
+ <script>
+ function init() {
+ document.querySelector("div").setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-dynamic.html b/editor/reftests/spellcheck-contenteditable-attr-dynamic.html
new file mode 100644
index 0000000000..37ba9f6514
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.querySelector("div").setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr-inherit.html b/editor/reftests/spellcheck-contenteditable-attr-inherit.html
new file mode 100644
index 0000000000..6cbfcb3da3
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr-inherit.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <span spellcheck="false"><div contenteditable>blahblahblah</div></span>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-attr.html b/editor/reftests/spellcheck-contenteditable-attr.html
new file mode 100644
index 0000000000..df119a9975
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-attr.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable>blahblahblah</div>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-disabled-partial-ref.html b/editor/reftests/spellcheck-contenteditable-disabled-partial-ref.html
new file mode 100644
index 0000000000..30fe7a6bf9
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-disabled-partial-ref.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <span contenteditable>sakde</span> kreid <span contenteditable>slodv</span>
+ <script>
+ // Adding focus to the textbox should trigger a spellcheck
+ document.querySelector("span").focus();
+ document.querySelector("span + span").focus();
+ document.querySelector("span + span").blur();
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-disabled-partial.html b/editor/reftests/spellcheck-contenteditable-disabled-partial.html
new file mode 100644
index 0000000000..c7b6c427c4
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-disabled-partial.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable>sakde <span spellcheck=false>kreid</span> slodv</div>
+ <script>
+ // Adding focus to the textbox should trigger a spellcheck
+ document.querySelector("div").focus();
+ document.querySelector("div").blur();
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-disabled-ref.html b/editor/reftests/spellcheck-contenteditable-disabled-ref.html
new file mode 100644
index 0000000000..23571fa5e8
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-disabled-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div>blahblahblah</div>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-disabled.html b/editor/reftests/spellcheck-contenteditable-disabled.html
new file mode 100644
index 0000000000..3794f5767c
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-disabled.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable spellcheck="false">blahblahblah</div>
+ <script>
+ // Adding focus to the textbox should trigger a spellcheck
+ document.querySelector("div").focus();
+ document.querySelector("div").blur();
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-focused-reframe.html b/editor/reftests/spellcheck-contenteditable-focused-reframe.html
new file mode 100644
index 0000000000..733ee05bb3
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-focused-reframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <div contenteditable id="testBox" onfocus="reframe(this);">blahblahblah</div>
+ <script type="text/javascript">
+ function reframe(textbox) {
+ textbox.style.display = "none";
+ textbox.style.display = "";
+ textbox.clientWidth;
+ }
+ //Adding focus to the textbox should trigger a spellcheck
+ document.getElementById("testBox").focus();
+ document.getElementById("testBox").blur();
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-focused.html b/editor/reftests/spellcheck-contenteditable-focused.html
new file mode 100644
index 0000000000..8086673997
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-focused.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <div contenteditable id="testBox">blahblahblah</div>
+ <script type="text/javascript">
+ //Adding focus to the textbox should trigger a spellcheck
+ document.getElementById("testBox").focus();
+ document.getElementById("testBox").blur();
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-nofocus-1.html b/editor/reftests/spellcheck-contenteditable-nofocus-1.html
new file mode 100644
index 0000000000..7e88dc3e18
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-nofocus-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable>blahblahblah</div>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-nofocus-2.html b/editor/reftests/spellcheck-contenteditable-nofocus-2.html
new file mode 100644
index 0000000000..fd7cdb8827
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-nofocus-2.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<html><body><div contenteditable>blahblahblah</div></body></html> \ No newline at end of file
diff --git a/editor/reftests/spellcheck-contenteditable-nofocus-ref.html b/editor/reftests/spellcheck-contenteditable-nofocus-ref.html
new file mode 100644
index 0000000000..67241fb7f1
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-nofocus-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable spellcheck="true">blahblahblah</div>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-property-dynamic-inherit.html b/editor/reftests/spellcheck-contenteditable-property-dynamic-inherit.html
new file mode 100644
index 0000000000..feb623dbb6
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-property-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.body.spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-property-dynamic-override-inherit.html b/editor/reftests/spellcheck-contenteditable-property-dynamic-override-inherit.html
new file mode 100644
index 0000000000..26c5a42236
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-property-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="true">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.body.spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-property-dynamic-override.html b/editor/reftests/spellcheck-contenteditable-property-dynamic-override.html
new file mode 100644
index 0000000000..dd16894b89
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-property-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable spellcheck="true">blahblahblah</div>
+ <script>
+ function init() {
+ document.querySelector("div").spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-property-dynamic.html b/editor/reftests/spellcheck-contenteditable-property-dynamic.html
new file mode 100644
index 0000000000..eaf2db29a6
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-property-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <div contenteditable>blahblahblah</div>
+ <script>
+ function init() {
+ document.querySelector("div").spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-contenteditable-ref.html b/editor/reftests/spellcheck-contenteditable-ref.html
new file mode 100644
index 0000000000..d28dbcf96b
--- /dev/null
+++ b/editor/reftests/spellcheck-contenteditable-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <div contenteditable spellcheck="true">blahblahblah</div>
+ <script type="text/javascript">
+ var box = document.getElementsByTagName("div")[0];
+ box.focus(); //Bring the textbox into focus, triggering a spellcheck
+ box.blur(); //Blur in order to make things similar to other tests otherwise
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-dotafterquote-valid-ref.html b/editor/reftests/spellcheck-dotafterquote-valid-ref.html
new file mode 100644
index 0000000000..b61904400b
--- /dev/null
+++ b/editor/reftests/spellcheck-dotafterquote-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">'Apple'.</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-dotafterquote-valid.html b/editor/reftests/spellcheck-dotafterquote-valid.html
new file mode 100644
index 0000000000..1d3a605bb0
--- /dev/null
+++ b/editor/reftests/spellcheck-dotafterquote-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>'Apple'.</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-invalid-ref.html b/editor/reftests/spellcheck-hyphen-invalid-ref.html
new file mode 100644
index 0000000000..856fd840ec
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-invalid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">dddf-gggy</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-invalid.html b/editor/reftests/spellcheck-hyphen-invalid.html
new file mode 100644
index 0000000000..bc4e4e240b
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-invalid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>dddf-gggy</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-multiple-invalid-ref.html b/editor/reftests/spellcheck-hyphen-multiple-invalid-ref.html
new file mode 100644
index 0000000000..ab4cbd05a5
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-multiple-invalid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea style="width: 400px; height: 200px;" autofocus spellcheck="false">-hlloe hlloe- --hlloe --hlloe ---hlloe hlloe--- ---hlloe----</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-multiple-invalid.html b/editor/reftests/spellcheck-hyphen-multiple-invalid.html
new file mode 100644
index 0000000000..bcc3f71133
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-multiple-invalid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea style="width: 400px; height: 200px;" autofocus>-hlloe hlloe- --hlloe --hlloe ---hlloe hlloe--- ---hlloe----</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-multiple-valid-ref.html b/editor/reftests/spellcheck-hyphen-multiple-valid-ref.html
new file mode 100644
index 0000000000..324a566c49
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-multiple-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea style="width: 400px; height: 200px;" autofocus spellcheck="false">- -- --- -hello hello- --hello --hello ---hello hello--- ---hello----</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-multiple-valid.html b/editor/reftests/spellcheck-hyphen-multiple-valid.html
new file mode 100644
index 0000000000..7f0ce681cb
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-multiple-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea style="width: 400px; height: 200px;" autofocus>- -- --- -hello hello- --hello --hello ---hello hello--- ---hello----</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-valid-ref.html b/editor/reftests/spellcheck-hyphen-valid-ref.html
new file mode 100644
index 0000000000..73b507a3dc
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">scot-free</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-hyphen-valid.html b/editor/reftests/spellcheck-hyphen-valid.html
new file mode 100644
index 0000000000..2b56a6e24c
--- /dev/null
+++ b/editor/reftests/spellcheck-hyphen-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>scot-free</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-after.html b/editor/reftests/spellcheck-input-attr-after.html
new file mode 100644
index 0000000000..1e878b5d15
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-after.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="blahblahblah" spellcheck="true">
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-before.html b/editor/reftests/spellcheck-input-attr-before.html
new file mode 100644
index 0000000000..8456e6c8cd
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-before.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" spellcheck="true" value="blahblahblah">
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-dynamic-inherit.html b/editor/reftests/spellcheck-input-attr-dynamic-inherit.html
new file mode 100644
index 0000000000..c87be7c3e3
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "true");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-dynamic-override-inherit.html b/editor/reftests/spellcheck-input-attr-dynamic-override-inherit.html
new file mode 100644
index 0000000000..d7d12b78da
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="false">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "true");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-dynamic-override.html b/editor/reftests/spellcheck-input-attr-dynamic-override.html
new file mode 100644
index 0000000000..0f6095bd07
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" spellcheck="false" value="blahblahblah">
+ <script>
+ function init() {
+ document.querySelector("input").setAttribute("spellcheck", "true");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-dynamic.html b/editor/reftests/spellcheck-input-attr-dynamic.html
new file mode 100644
index 0000000000..27c8281faf
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.querySelector("input").setAttribute("spellcheck", "true");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-attr-inherit.html b/editor/reftests/spellcheck-input-attr-inherit.html
new file mode 100644
index 0000000000..c851bd189c
--- /dev/null
+++ b/editor/reftests/spellcheck-input-attr-inherit.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <span spellcheck="true"><input class="spell-checked" type="text" value="blahblahblah"></span>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-disabled.html b/editor/reftests/spellcheck-input-disabled.html
new file mode 100644
index 0000000000..f3b2f2ba97
--- /dev/null
+++ b/editor/reftests/spellcheck-input-disabled.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="blahblahblah">
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-nofocus-ref.html b/editor/reftests/spellcheck-input-nofocus-ref.html
new file mode 100644
index 0000000000..1e878b5d15
--- /dev/null
+++ b/editor/reftests/spellcheck-input-nofocus-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input type="text" value="blahblahblah" spellcheck="true">
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-property-dynamic-inherit.html b/editor/reftests/spellcheck-input-property-dynamic-inherit.html
new file mode 100644
index 0000000000..1cf839baee
--- /dev/null
+++ b/editor/reftests/spellcheck-input-property-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.body.spellcheck = true;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-property-dynamic-override-inherit.html b/editor/reftests/spellcheck-input-property-dynamic-override-inherit.html
new file mode 100644
index 0000000000..eb380dc960
--- /dev/null
+++ b/editor/reftests/spellcheck-input-property-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="false">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.body.spellcheck = true;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-property-dynamic-override.html b/editor/reftests/spellcheck-input-property-dynamic-override.html
new file mode 100644
index 0000000000..fad2fb3ed4
--- /dev/null
+++ b/editor/reftests/spellcheck-input-property-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" spellcheck="false" value="blahblahblah">
+ <script>
+ function init() {
+ document.querySelector("input").spellcheck = true;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-property-dynamic.html b/editor/reftests/spellcheck-input-property-dynamic.html
new file mode 100644
index 0000000000..dd59ec6ea7
--- /dev/null
+++ b/editor/reftests/spellcheck-input-property-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <input class="spell-checked" type="text" value="blahblahblah">
+ <script>
+ function init() {
+ document.querySelector("input").spellcheck = true;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-input-ref.html b/editor/reftests/spellcheck-input-ref.html
new file mode 100644
index 0000000000..2c2e48d6b6
--- /dev/null
+++ b/editor/reftests/spellcheck-input-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body>
+ <input type="text" value="blahblahblah" spellcheck="true">
+ <script>
+ var i = document.getElementsByTagName("input")[0];
+ i.focus(); // init the editor
+ i.blur(); // we actually don't need the focus
+
+ // Try to ensure the input element is repainted.
+ let rAFCounter = 0;
+ requestIdleCallback(function rAF() {
+ if (rAFCounter < 10) {
+ ++rAFCounter;
+ requestAnimationFrame(rAF);
+ } else {
+ document.documentElement.removeAttribute("class");
+ }
+ });
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-arabic-ref.html b/editor/reftests/spellcheck-non-latin-arabic-ref.html
new file mode 100644
index 0000000000..67850b46cb
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-arabic-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">سلام</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-arabic.html b/editor/reftests/spellcheck-non-latin-arabic.html
new file mode 100644
index 0000000000..fbbe193889
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-arabic.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>سلام</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-chinese-simplified-ref.html b/editor/reftests/spellcheck-non-latin-chinese-simplified-ref.html
new file mode 100644
index 0000000000..83ad79c265
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-chinese-simplified-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">你好</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-chinese-simplified.html b/editor/reftests/spellcheck-non-latin-chinese-simplified.html
new file mode 100644
index 0000000000..8db16489a9
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-chinese-simplified.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>你好</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-chinese-traditional-ref.html b/editor/reftests/spellcheck-non-latin-chinese-traditional-ref.html
new file mode 100644
index 0000000000..83ad79c265
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-chinese-traditional-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">你好</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-chinese-traditional.html b/editor/reftests/spellcheck-non-latin-chinese-traditional.html
new file mode 100644
index 0000000000..8db16489a9
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-chinese-traditional.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>你好</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-hebrew-ref.html b/editor/reftests/spellcheck-non-latin-hebrew-ref.html
new file mode 100644
index 0000000000..e2bd7c6d2c
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-hebrew-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">שלו×</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-hebrew.html b/editor/reftests/spellcheck-non-latin-hebrew.html
new file mode 100644
index 0000000000..9372c4e9a2
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-hebrew.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>שלו×</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-japanese-ref.html b/editor/reftests/spellcheck-non-latin-japanese-ref.html
new file mode 100644
index 0000000000..a978cd3cef
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-japanese-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">ã“ã‚“ã«ã¡ã¯</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-japanese.html b/editor/reftests/spellcheck-non-latin-japanese.html
new file mode 100644
index 0000000000..d79bb0e5ea
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-japanese.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>ã“ã‚“ã«ã¡ã¯</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-korean-ref.html b/editor/reftests/spellcheck-non-latin-korean-ref.html
new file mode 100644
index 0000000000..53d1909f38
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-korean-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus spellcheck="false">안녕하세요</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-non-latin-korean.html b/editor/reftests/spellcheck-non-latin-korean.html
new file mode 100644
index 0000000000..f0f65e82e3
--- /dev/null
+++ b/editor/reftests/spellcheck-non-latin-korean.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <textarea autofocus>안녕하세요</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-period-valid-ref.html b/editor/reftests/spellcheck-period-valid-ref.html
new file mode 100644
index 0000000000..5ee87992b6
--- /dev/null
+++ b/editor/reftests/spellcheck-period-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">good.nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-period-valid.html b/editor/reftests/spellcheck-period-valid.html
new file mode 100644
index 0000000000..aaa5aa4686
--- /dev/null
+++ b/editor/reftests/spellcheck-period-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>good.nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-slash-valid-ref.html b/editor/reftests/spellcheck-slash-valid-ref.html
new file mode 100644
index 0000000000..fddc032525
--- /dev/null
+++ b/editor/reftests/spellcheck-slash-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">good/nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-slash-valid.html b/editor/reftests/spellcheck-slash-valid.html
new file mode 100644
index 0000000000..37e8d1bf4c
--- /dev/null
+++ b/editor/reftests/spellcheck-slash-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>good/nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-space-valid-ref.html b/editor/reftests/spellcheck-space-valid-ref.html
new file mode 100644
index 0000000000..9fea33e961
--- /dev/null
+++ b/editor/reftests/spellcheck-space-valid-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus spellcheck="false">good nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-space-valid.html b/editor/reftests/spellcheck-space-valid.html
new file mode 100644
index 0000000000..575426d470
--- /dev/null
+++ b/editor/reftests/spellcheck-space-valid.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus>good nice</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-superscript-1-ref.html b/editor/reftests/spellcheck-superscript-1-ref.html
new file mode 100644
index 0000000000..35df20d70b
--- /dev/null
+++ b/editor/reftests/spellcheck-superscript-1-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<textarea spellcheck=false>&sup1; &sup2; &sup3;</textarea>
+<script>document.body.firstChild.focus()</script>
diff --git a/editor/reftests/spellcheck-superscript-1.html b/editor/reftests/spellcheck-superscript-1.html
new file mode 100644
index 0000000000..b7b317295a
--- /dev/null
+++ b/editor/reftests/spellcheck-superscript-1.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<textarea>&sup1; &sup2; &sup3;</textarea>
+<script>document.body.firstChild.focus()</script>
diff --git a/editor/reftests/spellcheck-superscript-2-ref.html b/editor/reftests/spellcheck-superscript-2-ref.html
new file mode 100644
index 0000000000..19276bd71a
--- /dev/null
+++ b/editor/reftests/spellcheck-superscript-2-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<textarea>&sup1; &sup2; &sup3; mispeled</textarea>
+<script>document.body.firstChild.focus()</script>
diff --git a/editor/reftests/spellcheck-superscript-2.html b/editor/reftests/spellcheck-superscript-2.html
new file mode 100644
index 0000000000..350d2bc8cf
--- /dev/null
+++ b/editor/reftests/spellcheck-superscript-2.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<textarea spellcheck=false>&sup1; &sup2; &sup3; mispeled</textarea>
+<script>document.body.firstChild.focus()</script>
diff --git a/editor/reftests/spellcheck-textarea-attr-dynamic-inherit.html b/editor/reftests/spellcheck-textarea-attr-dynamic-inherit.html
new file mode 100644
index 0000000000..a4a938494b
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr-dynamic-override-inherit.html b/editor/reftests/spellcheck-textarea-attr-dynamic-override-inherit.html
new file mode 100644
index 0000000000..a6ae716b5a
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="true">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.body.setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr-dynamic-override.html b/editor/reftests/spellcheck-textarea-attr-dynamic-override.html
new file mode 100644
index 0000000000..96e9566081
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea spellcheck="true">blahblahblah</textarea>
+ <script>
+ function init() {
+ document.querySelector("textarea").setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr-dynamic.html b/editor/reftests/spellcheck-textarea-attr-dynamic.html
new file mode 100644
index 0000000000..745e66c7c8
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.querySelector("textarea").setAttribute("spellcheck", "false");
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr-inherit.html b/editor/reftests/spellcheck-textarea-attr-inherit.html
new file mode 100644
index 0000000000..c261fec5a4
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr-inherit.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <span spellcheck="false"><textarea>blahblahblah</textarea></span>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-attr.html b/editor/reftests/spellcheck-textarea-attr.html
new file mode 100644
index 0000000000..223f948dc3
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-attr.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea>blahblahblah</textarea>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-disabled.html b/editor/reftests/spellcheck-textarea-disabled.html
new file mode 100644
index 0000000000..cfaf3ed53d
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-disabled.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="false">blahblahblah</textarea>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-focused-notreadonly.html b/editor/reftests/spellcheck-textarea-focused-notreadonly.html
new file mode 100644
index 0000000000..475ae60020
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-focused-notreadonly.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <textarea id="testBox" style="padding:2px;" readonly></textarea>
+ <script type="text/javascript">
+ //Adding focus to the textbox should trigger a spellcheck
+ var textbox = document.getElementById("testBox");
+ addEventListener("load", function() {
+ textbox.readOnly = false;
+ textbox.focus();
+ textbox.value = "blahblahblah";
+ textbox.selectionStart = textbox.selectionEnd = 0;
+ textbox.blur();
+ }, false);
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-focused-reframe.html b/editor/reftests/spellcheck-textarea-focused-reframe.html
new file mode 100644
index 0000000000..6e6f871dda
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-focused-reframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <textarea id="testBox" onfocus="reframe(this);">blahblahblah</textarea>
+ <script type="text/javascript">
+ function reframe(textbox) {
+ textbox.style.display = "none";
+ textbox.style.display = "";
+ textbox.clientWidth;
+ }
+ //Adding focus to the textbox should trigger a spellcheck
+ document.getElementById("testBox").focus();
+ document.getElementById("testBox").blur();
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-focused.html b/editor/reftests/spellcheck-textarea-focused.html
new file mode 100644
index 0000000000..04d689cc1a
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-focused.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+ <textarea id="testBox">blahblahblah</textarea>
+ <script type="text/javascript">
+ //Adding focus to the textbox should trigger a spellcheck
+ document.getElementById("testBox").focus();
+ document.getElementById("testBox").blur();
+ </script>
+
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-nofocus-ref.html b/editor/reftests/spellcheck-textarea-nofocus-ref.html
new file mode 100644
index 0000000000..8d993983eb
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-nofocus-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="true">blahblahblah</textarea>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-nofocus.html b/editor/reftests/spellcheck-textarea-nofocus.html
new file mode 100644
index 0000000000..a1ce1a0a98
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-nofocus.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea>blahblahblah</textarea>
+</body>
+</html> \ No newline at end of file
diff --git a/editor/reftests/spellcheck-textarea-property-dynamic-inherit.html b/editor/reftests/spellcheck-textarea-property-dynamic-inherit.html
new file mode 100644
index 0000000000..125f578bf9
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-property-dynamic-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.body.spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-property-dynamic-override-inherit.html b/editor/reftests/spellcheck-textarea-property-dynamic-override-inherit.html
new file mode 100644
index 0000000000..0e773646e4
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-property-dynamic-override-inherit.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()" spellcheck="true">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.body.spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-property-dynamic-override.html b/editor/reftests/spellcheck-textarea-property-dynamic-override.html
new file mode 100644
index 0000000000..f929d3bec9
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-property-dynamic-override.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea spellcheck="true">blahblahblah</textarea>
+ <script>
+ function init() {
+ document.querySelector("textarea").spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-property-dynamic.html b/editor/reftests/spellcheck-textarea-property-dynamic.html
new file mode 100644
index 0000000000..d0c94f68e8
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-property-dynamic.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body onload="init()">
+ <textarea>blahblahblah</textarea>
+ <script>
+ function init() {
+ document.querySelector("textarea").spellcheck = false;
+ }
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-ref.html b/editor/reftests/spellcheck-textarea-ref.html
new file mode 100644
index 0000000000..91ecd1d8e9
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="true">blahblahblah</textarea>
+ <script type="text/javascript">
+ var box = document.getElementsByTagName("textarea")[0];
+ box.focus(); //Bring the textbox into focus, triggering a spellcheck
+ box.blur(); //Blur in order to make things similar to other tests otherwise
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-textarea-ref2.html b/editor/reftests/spellcheck-textarea-ref2.html
new file mode 100644
index 0000000000..6bd588a235
--- /dev/null
+++ b/editor/reftests/spellcheck-textarea-ref2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <textarea spellcheck="true" style="padding:2px;">blahblahblah</textarea>
+ <script type="text/javascript">
+ var box = document.getElementsByTagName("textarea")[0];
+ box.focus(); //Bring the textbox into focus, triggering a spellcheck
+ box.blur(); //Blur in order to make things similar to other tests otherwise
+ </script>
+</body>
+</html>
diff --git a/editor/reftests/spellcheck-url-valid-ref.html b/editor/reftests/spellcheck-url-valid-ref.html
new file mode 100644
index 0000000000..7f9f7530dd
--- /dev/null
+++ b/editor/reftests/spellcheck-url-valid-ref.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus rows=10 cols=60 spellcheck=false>
+http://fooi.barj/bazk
+https://fooi.barj/bazk
+news://fooi.barj/bazk
+ftp://fooi.barj/bazk
+data:fooi/barj,bazk
+javascript:fooi.barj.bazk();
+fooi@barj.bazk
+</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/spellcheck-url-valid.html b/editor/reftests/spellcheck-url-valid.html
new file mode 100644
index 0000000000..f492560a2b
--- /dev/null
+++ b/editor/reftests/spellcheck-url-valid.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea autofocus rows=10 cols=60>
+http://fooi.barj/bazk
+https://fooi.barj/bazk
+news://fooi.barj/bazk
+ftp://fooi.barj/bazk
+data:fooi/barj,bazk
+javascript:fooi.barj.bazk();
+fooi@barj.bazk
+</textarea>
+ </body>
+</html>
diff --git a/editor/reftests/unneeded_scroll-ref.html b/editor/reftests/unneeded_scroll-ref.html
new file mode 100644
index 0000000000..9d6ec25bbd
--- /dev/null
+++ b/editor/reftests/unneeded_scroll-ref.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div>
+ <textarea>I
+ am
+ a
+ long
+ long
+ long
+ long
+ textarea
+ </textarea>
+ </div>
+ </body>
+</html>
diff --git a/editor/reftests/unneeded_scroll.html b/editor/reftests/unneeded_scroll.html
new file mode 100644
index 0000000000..55c0ba6ba3
--- /dev/null
+++ b/editor/reftests/unneeded_scroll.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <script>
+ document.addEventListener("DOMContentLoaded", function() {
+ var t = document.querySelector("textarea");
+ t.focus();
+ document.querySelector("#dst").appendChild(t);
+ });
+ </script>
+ <div>
+ <textarea>I
+ am
+ a
+ long
+ long
+ long
+ long
+ textarea
+ </textarea>
+ </div>
+ <div id="dst"></div>
+ </body>
+</html>
diff --git a/editor/reftests/xul/empty-ref.xhtml b/editor/reftests/xul/empty-ref.xhtml
new file mode 100644
index 0000000000..b5700fc158
--- /dev/null
+++ b/editor/reftests/xul/empty-ref.xhtml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="resource://reftest/input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input class="empty" value=" test"/>
+
+</window>
diff --git a/editor/reftests/xul/emptytextbox-4.xhtml b/editor/reftests/xul/emptytextbox-4.xhtml
new file mode 100644
index 0000000000..90ba9a004b
--- /dev/null
+++ b/editor/reftests/xul/emptytextbox-4.xhtml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <search-textbox/>
+
+</window>
diff --git a/editor/reftests/xul/emptytextbox-ref.xhtml b/editor/reftests/xul/emptytextbox-ref.xhtml
new file mode 100644
index 0000000000..eb48d52d79
--- /dev/null
+++ b/editor/reftests/xul/emptytextbox-ref.xhtml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="resource://reftest/input.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Textbox tests">
+
+ <script type="text/javascript" src="platform.js"/>
+
+ <html:input/>
+
+</window>
diff --git a/editor/reftests/xul/input.css b/editor/reftests/xul/input.css
new file mode 100644
index 0000000000..17432527b2
--- /dev/null
+++ b/editor/reftests/xul/input.css
@@ -0,0 +1,36 @@
+@namespace url('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul');
+@namespace html url('http://www.w3.org/1999/xhtml');
+
+:root > html|input {
+ margin: 2px 4px;
+ padding: 2px 2px 3px;
+ padding-inline-start: 5px;
+}
+
+#mac > html|input {
+ margin: 4px;
+ padding: 0 1px;
+}
+
+html|input {
+ font: inherit;
+}
+
+html|input.empty {
+ color: graytext;
+}
+
+@media (-moz-windows-default-theme) and (-moz-platform: windows-win7) {
+ :root:not(.winxp) html|input.empty {
+ font-style: italic;
+ }
+}
+
+html|input.num {
+ text-align: end;
+}
+
+/* .textbox-input has 1px extra padding on Linux */
+#linux html|input.num {
+ padding-inline-end: 3px;
+}
diff --git a/editor/reftests/xul/placeholder-reset.css b/editor/reftests/xul/placeholder-reset.css
new file mode 100644
index 0000000000..a2c41e69b0
--- /dev/null
+++ b/editor/reftests/xul/placeholder-reset.css
@@ -0,0 +1,8 @@
+@namespace url('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul');
+@namespace html url('http://www.w3.org/1999/xhtml');
+
+/* We need to have a non-transparent placeholder so we can test it. */
+html|input::placeholder {
+ opacity: 1.0;
+ color: graytext;
+}
diff --git a/editor/reftests/xul/platform.js b/editor/reftests/xul/platform.js
new file mode 100644
index 0000000000..f45e6d1f5c
--- /dev/null
+++ b/editor/reftests/xul/platform.js
@@ -0,0 +1,28 @@
+// The appearance of XUL elements is platform-specific, so we set the
+// style of the root element according to the platform, so that the
+// CSS code inside input.css can select the correct styles for each
+// platform.
+
+var id;
+var ua = navigator.userAgent;
+
+if (/Windows/.test(ua)) {
+ id = "win";
+ if (/NT 5\.1/.test(ua) || /NT 5\.2; Win64/.test(ua))
+ var classname = "winxp";
+}
+else if (/Linux/.test(ua))
+ id = "linux";
+else if (/SunOS/.test(ua))
+ id = "linux";
+else if (/Mac OS X/.test(ua))
+ id = "mac";
+
+if (id)
+ document.documentElement.setAttribute("id", id);
+else
+ document.documentElement.appendChild(
+ document.createTextNode("Unrecognized platform")
+ );
+if (classname)
+ document.documentElement.setAttribute("class", classname);
diff --git a/editor/reftests/xul/reftest.list b/editor/reftests/xul/reftest.list
new file mode 100644
index 0000000000..591ee8f6c7
--- /dev/null
+++ b/editor/reftests/xul/reftest.list
@@ -0,0 +1 @@
+!= chrome://reftest/content/editor/reftests/xul/emptytextbox-4.xhtml chrome://reftest/content/editor/reftests/xul/emptytextbox-ref.xhtml
diff --git a/editor/spellchecker/EditorSpellCheck.cpp b/editor/spellchecker/EditorSpellCheck.cpp
new file mode 100644
index 0000000000..35b4da06cf
--- /dev/null
+++ b/editor/spellchecker/EditorSpellCheck.cpp
@@ -0,0 +1,1179 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 sw=2 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 "EditorSpellCheck.h"
+
+#include "EditorBase.h" // for EditorBase
+#include "HTMLEditor.h" // for HTMLEditor
+#include "TextServicesDocument.h" // for TextServicesDocument
+
+#include "mozilla/Attributes.h" // for final
+#include "mozilla/dom/Element.h" // for Element
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h"
+#include "mozilla/intl/Locale.h" // for mozilla::intl::Locale
+#include "mozilla/intl/LocaleService.h" // for retrieving app locale
+#include "mozilla/intl/OSPreferences.h" // for mozilla::intl::OSPreferences
+#include "mozilla/Logging.h" // for mozilla::LazyLogModule
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "mozilla/mozSpellChecker.h" // for mozSpellChecker
+#include "mozilla/Preferences.h" // for Preferences
+
+#include "nsAString.h" // for nsAString::IsEmpty, etc
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsDependentSubstring.h" // for Substring
+#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc
+#include "nsIContent.h" // for nsIContent
+#include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc
+#include "mozilla/dom/Document.h" // for Document
+#include "nsIEditor.h" // for nsIEditor
+#include "nsILoadContext.h"
+#include "nsISupports.h" // for nsISupports
+#include "nsISupportsUtils.h" // for NS_ADDREF
+#include "nsIURI.h" // for nsIURI
+#include "nsThreadUtils.h" // for GetMainThreadSerialEventTarget
+#include "nsVariant.h" // for nsIWritableVariant, etc
+#include "nsLiteralString.h" // for NS_LITERAL_STRING, etc
+#include "nsRange.h"
+#include "nsReadableUtils.h" // for ToNewUnicode, EmptyString, etc
+#include "nsServiceManagerUtils.h" // for do_GetService
+#include "nsString.h" // for nsAutoString, nsString, etc
+#include "nsStringFwd.h" // for nsAFlatString
+#include "nsStyleUtil.h" // for nsStyleUtil
+#include "nsXULAppAPI.h" // for XRE_GetProcessType
+
+namespace mozilla {
+
+using namespace dom;
+using intl::LocaleService;
+using intl::OSPreferences;
+
+static mozilla::LazyLogModule sEditorSpellChecker("EditorSpellChecker");
+
+class UpdateDictionaryHolder {
+ private:
+ EditorSpellCheck* mSpellCheck;
+
+ public:
+ explicit UpdateDictionaryHolder(EditorSpellCheck* esc) : mSpellCheck(esc) {
+ if (mSpellCheck) {
+ mSpellCheck->BeginUpdateDictionary();
+ }
+ }
+
+ ~UpdateDictionaryHolder() {
+ if (mSpellCheck) {
+ mSpellCheck->EndUpdateDictionary();
+ }
+ }
+};
+
+#define CPS_PREF_NAME u"spellcheck.lang"_ns
+
+/**
+ * Gets the URI of aEditor's document.
+ */
+static nsIURI* GetDocumentURI(EditorBase* aEditor) {
+ MOZ_ASSERT(aEditor);
+
+ Document* doc = aEditor->AsEditorBase()->GetDocument();
+ if (NS_WARN_IF(!doc)) {
+ return nullptr;
+ }
+
+ return doc->GetDocumentURI();
+}
+
+static nsILoadContext* GetLoadContext(nsIEditor* aEditor) {
+ Document* doc = aEditor->AsEditorBase()->GetDocument();
+ if (NS_WARN_IF(!doc)) {
+ return nullptr;
+ }
+
+ return doc->GetLoadContext();
+}
+
+static nsCString DictionariesToString(
+ const nsTArray<nsCString>& aDictionaries) {
+ nsCString asString;
+ for (const auto& dictionary : aDictionaries) {
+ asString.Append(dictionary);
+ asString.Append(',');
+ }
+ return asString;
+}
+
+static void StringToDictionaries(const nsCString& aString,
+ nsTArray<nsCString>& aDictionaries) {
+ nsTArray<nsCString> asDictionaries;
+ for (const nsACString& token :
+ nsCCharSeparatedTokenizer(aString, ',').ToRange()) {
+ if (token.IsEmpty()) {
+ continue;
+ }
+ aDictionaries.AppendElement(token);
+ }
+}
+
+/**
+ * Fetches the dictionary stored in content prefs and maintains state during the
+ * fetch, which is asynchronous.
+ */
+class DictionaryFetcher final : public nsIContentPrefCallback2 {
+ public:
+ NS_DECL_ISUPPORTS
+
+ DictionaryFetcher(EditorSpellCheck* aSpellCheck,
+ nsIEditorSpellCheckCallback* aCallback, uint32_t aGroup)
+ : mCallback(aCallback), mGroup(aGroup), mSpellCheck(aSpellCheck) {}
+
+ NS_IMETHOD Fetch(nsIEditor* aEditor);
+
+ NS_IMETHOD HandleResult(nsIContentPref* aPref) override {
+ nsCOMPtr<nsIVariant> value;
+ nsresult rv = aPref->GetValue(getter_AddRefs(value));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString asString;
+ value->GetAsACString(asString);
+ StringToDictionaries(asString, mDictionaries);
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t reason) override {
+ mSpellCheck->DictionaryFetched(this);
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleError(nsresult error) override { return NS_OK; }
+
+ nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
+ uint32_t mGroup;
+ nsString mRootContentLang;
+ nsString mRootDocContentLang;
+ nsTArray<nsCString> mDictionaries;
+
+ private:
+ ~DictionaryFetcher() {}
+
+ RefPtr<EditorSpellCheck> mSpellCheck;
+};
+
+NS_IMPL_ISUPPORTS(DictionaryFetcher, nsIContentPrefCallback2)
+
+class ContentPrefInitializerRunnable final : public Runnable {
+ public:
+ ContentPrefInitializerRunnable(nsIEditor* aEditor,
+ nsIContentPrefCallback2* aCallback)
+ : Runnable("ContentPrefInitializerRunnable"),
+ mEditorBase(aEditor->AsEditorBase()),
+ mCallback(aCallback) {}
+
+ NS_IMETHOD Run() override {
+ if (mEditorBase->Destroyed()) {
+ mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!contentPrefService)) {
+ mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> docUri = GetDocumentURI(mEditorBase);
+ if (NS_WARN_IF(!docUri)) {
+ mCallback->HandleError(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ nsAutoCString docUriSpec;
+ nsresult rv = docUri->GetSpec(docUriSpec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mCallback->HandleError(rv);
+ return NS_OK;
+ }
+
+ rv = contentPrefService->GetByDomainAndName(
+ NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
+ GetLoadContext(mEditorBase), mCallback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mCallback->HandleError(rv);
+ return NS_OK;
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<EditorBase> mEditorBase;
+ nsCOMPtr<nsIContentPrefCallback2> mCallback;
+};
+
+NS_IMETHODIMP
+DictionaryFetcher::Fetch(nsIEditor* aEditor) {
+ NS_ENSURE_ARG_POINTER(aEditor);
+
+ nsCOMPtr<nsIRunnable> runnable =
+ new ContentPrefInitializerRunnable(aEditor, this);
+ NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
+ EventQueuePriority::Idle);
+
+ return NS_OK;
+}
+
+/**
+ * Stores the current dictionary for aEditor's document URL.
+ */
+static nsresult StoreCurrentDictionaries(
+ EditorBase* aEditorBase, const nsTArray<nsCString>& aDictionaries) {
+ NS_ENSURE_ARG_POINTER(aEditorBase);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
+ if (NS_WARN_IF(!docUri)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString docUriSpec;
+ rv = docUri->GetSpec(docUriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsVariant> prefValue = new nsVariant();
+
+ nsCString asString = DictionariesToString(aDictionaries);
+ prefValue->SetAsAString(NS_ConvertUTF8toUTF16(asString));
+
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
+
+ return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec),
+ CPS_PREF_NAME, prefValue,
+ GetLoadContext(aEditorBase), nullptr);
+}
+
+/**
+ * Forgets the current dictionary stored for aEditor's document URL.
+ */
+static nsresult ClearCurrentDictionaries(EditorBase* aEditorBase) {
+ NS_ENSURE_ARG_POINTER(aEditorBase);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
+ if (NS_WARN_IF(!docUri)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString docUriSpec;
+ rv = docUri->GetSpec(docUriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
+
+ return contentPrefService->RemoveByDomainAndName(
+ NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
+ GetLoadContext(aEditorBase), nullptr);
+}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorSpellCheck)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorSpellCheck)
+
+NS_INTERFACE_MAP_BEGIN(EditorSpellCheck)
+ NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditorSpellCheck)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(EditorSpellCheck)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(EditorSpellCheck, mEditor, mSpellChecker)
+
+EditorSpellCheck::EditorSpellCheck()
+ : mTxtSrvFilterType(0),
+ mSuggestedWordIndex(0),
+ mDictionaryIndex(0),
+ mDictionaryFetcherGroup(0),
+ mUpdateDictionaryRunning(false) {}
+
+EditorSpellCheck::~EditorSpellCheck() {
+ // Make sure we blow the spellchecker away, just in
+ // case it hasn't been destroyed already.
+ mSpellChecker = nullptr;
+}
+
+mozSpellChecker* EditorSpellCheck::GetSpellChecker() { return mSpellChecker; }
+
+// The problem is that if the spell checker does not exist, we can not tell
+// which dictionaries are installed. This function works around the problem,
+// allowing callers to ask if we can spell check without actually doing so (and
+// enabling or disabling UI as necessary). This just creates a spellcheck
+// object if needed and asks it for the dictionary list.
+NS_IMETHODIMP
+EditorSpellCheck::CanSpellCheck(bool* aCanSpellCheck) {
+ RefPtr<mozSpellChecker> spellChecker = mSpellChecker;
+ if (!spellChecker) {
+ spellChecker = mozSpellChecker::Create();
+ MOZ_ASSERT(spellChecker);
+ }
+ nsTArray<nsCString> dictList;
+ nsresult rv = spellChecker->GetDictionaryList(&dictList);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ *aCanSpellCheck = !dictList.IsEmpty();
+ return NS_OK;
+}
+
+// Instances of this class can be used as either runnables or RAII helpers.
+class CallbackCaller final : public Runnable {
+ public:
+ explicit CallbackCaller(nsIEditorSpellCheckCallback* aCallback)
+ : mozilla::Runnable("CallbackCaller"), mCallback(aCallback) {}
+
+ ~CallbackCaller() { Run(); }
+
+ NS_IMETHOD Run() override {
+ if (mCallback) {
+ mCallback->EditorSpellCheckDone();
+ mCallback = nullptr;
+ }
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
+};
+
+NS_IMETHODIMP
+EditorSpellCheck::InitSpellChecker(nsIEditor* aEditor,
+ bool aEnableSelectionChecking,
+ nsIEditorSpellCheckCallback* aCallback) {
+ NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
+ mEditor = aEditor->AsEditorBase();
+
+ RefPtr<Document> doc = mEditor->GetDocument();
+ if (NS_WARN_IF(!doc)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+
+ // We can spell check with any editor type
+ RefPtr<TextServicesDocument> textServicesDocument =
+ new TextServicesDocument();
+ textServicesDocument->SetFilterType(mTxtSrvFilterType);
+
+ // EditorBase::AddEditActionListener() needs to access mSpellChecker and
+ // mSpellChecker->GetTextServicesDocument(). Therefore, we need to
+ // initialize them before calling TextServicesDocument::InitWithEditor()
+ // since it calls EditorBase::AddEditActionListener().
+ mSpellChecker = mozSpellChecker::Create();
+ MOZ_ASSERT(mSpellChecker);
+ rv = mSpellChecker->SetDocument(textServicesDocument, true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Pass the editor to the text services document
+ rv = textServicesDocument->InitWithEditor(aEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aEnableSelectionChecking) {
+ // Find out if the section is collapsed or not.
+ // If it isn't, we want to spellcheck just the selection.
+
+ RefPtr<Selection> selection;
+ aEditor->GetSelection(getter_AddRefs(selection));
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (selection->RangeCount()) {
+ RefPtr<const nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_STATE(range);
+
+ if (!range->Collapsed()) {
+ // We don't want to touch the range in the selection,
+ // so create a new copy of it.
+ RefPtr<StaticRange> staticRange =
+ StaticRange::Create(range, IgnoreErrors());
+ if (NS_WARN_IF(!staticRange)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure the new range spans complete words.
+ rv = textServicesDocument->ExpandRangeToWordBoundaries(staticRange);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Now tell the text services that you only want
+ // to iterate over the text in this range.
+ rv = textServicesDocument->SetExtent(staticRange);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+ }
+ // do not fail if UpdateCurrentDictionary fails because this method may
+ // succeed later.
+ rv = UpdateCurrentDictionary(aCallback);
+ if (NS_FAILED(rv) && aCallback) {
+ // However, if it does fail, we still need to call the callback since we
+ // discard the failure. Do it asynchronously so that the caller is always
+ // guaranteed async behavior.
+ RefPtr<CallbackCaller> caller = new CallbackCaller(aCallback);
+ rv = doc->Dispatch(TaskCategory::Other, caller.forget());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetNextMisspelledWord(nsAString& aNextMisspelledWord) {
+ MOZ_LOG(sEditorSpellChecker, LogLevel::Debug, ("%s", __FUNCTION__));
+
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ DeleteSuggestedWordList();
+ // Beware! This may flush notifications via synchronous
+ // ScrollSelectionIntoView.
+ RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
+ return spellChecker->NextMisspelledWord(aNextMisspelledWord,
+ mSuggestedWordList);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetSuggestedWord(nsAString& aSuggestedWord) {
+ // XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
+ if (mSuggestedWordIndex < static_cast<int32_t>(mSuggestedWordList.Length())) {
+ aSuggestedWord = mSuggestedWordList[mSuggestedWordIndex];
+ mSuggestedWordIndex++;
+ } else {
+ // A blank string signals that there are no more strings
+ aSuggestedWord.Truncate();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::CheckCurrentWord(const nsAString& aSuggestedWord,
+ bool* aIsMisspelled) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ DeleteSuggestedWordList();
+ return mSpellChecker->CheckWord(aSuggestedWord, aIsMisspelled,
+ &mSuggestedWordList);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::Suggest(const nsAString& aSuggestedWord, uint32_t aCount,
+ JSContext* aCx, Promise** aPromise) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ mSpellChecker->Suggest(aSuggestedWord, aCount)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise](const CopyableTArray<nsString>& aSuggestions) {
+ promise->MaybeResolve(aSuggestions);
+ },
+ [promise](nsresult aError) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+RefPtr<CheckWordPromise> EditorSpellCheck::CheckCurrentWordsNoSuggest(
+ const nsTArray<nsString>& aSuggestedWords) {
+ if (NS_WARN_IF(!mSpellChecker)) {
+ return CheckWordPromise::CreateAndReject(NS_ERROR_NOT_INITIALIZED,
+ __func__);
+ }
+
+ return mSpellChecker->CheckWords(aSuggestedWords);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::ReplaceWord(const nsAString& aMisspelledWord,
+ const nsAString& aReplaceWord,
+ bool aAllOccurrences) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
+ return spellChecker->Replace(aMisspelledWord, aReplaceWord, aAllOccurrences);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::IgnoreWordAllOccurrences(const nsAString& aWord) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->IgnoreAll(aWord);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetPersonalDictionary() {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ // We can spell check with any editor type
+ mDictionaryList.Clear();
+ mDictionaryIndex = 0;
+ return mSpellChecker->GetPersonalDictionary(&mDictionaryList);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetPersonalDictionaryWord(nsAString& aDictionaryWord) {
+ // XXX This is buggy if mDictionaryList.Length() is over INT32_MAX.
+ if (mDictionaryIndex < static_cast<int32_t>(mDictionaryList.Length())) {
+ aDictionaryWord = mDictionaryList[mDictionaryIndex];
+ mDictionaryIndex++;
+ } else {
+ // A blank string signals that there are no more strings
+ aDictionaryWord.Truncate();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::AddWordToDictionary(const nsAString& aWord) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->AddWordToPersonalDictionary(aWord);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::RemoveWordFromDictionary(const nsAString& aWord) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->RemoveWordFromPersonalDictionary(aWord);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetDictionaryList(nsTArray<nsCString>& aList) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->GetDictionaryList(&aList);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetCurrentDictionaries(nsTArray<nsCString>& aDictionaries) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+ return mSpellChecker->GetCurrentDictionaries(aDictionaries);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::SetCurrentDictionaries(
+ const nsTArray<nsCString>& aDictionaries, JSContext* aCx,
+ Promise** aPromise) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
+
+ // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
+ // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
+ // is on the stack. In other words: Only do this, if the user manually
+ // selected a dictionary to use.
+ if (!mUpdateDictionaryRunning) {
+ // Ignore pending dictionary fetchers by increasing this number.
+ mDictionaryFetcherGroup++;
+
+ uint32_t flags = 0;
+ mEditor->GetFlags(&flags);
+ if (!(flags & nsIEditor::eEditorMailMask)) {
+ bool contentPrefMatchesUserPref = true;
+ // Check if aDictionaries has the same languages as mPreferredLangs.
+ if (!aDictionaries.IsEmpty()) {
+ if (aDictionaries.Length() != mPreferredLangs.Length()) {
+ contentPrefMatchesUserPref = false;
+ } else {
+ for (const auto& dictName : aDictionaries) {
+ if (mPreferredLangs.IndexOf(dictName) ==
+ nsTArray<nsCString>::NoIndex) {
+ contentPrefMatchesUserPref = false;
+ break;
+ }
+ }
+ }
+ }
+ if (!contentPrefMatchesUserPref) {
+ // When user sets dictionary manually, we store this value associated
+ // with editor url, if it doesn't match the document language exactly.
+ // For example on "en" sites, we need to store "en-GB", otherwise
+ // the language might jump back to en-US although the user explicitly
+ // chose otherwise.
+ StoreCurrentDictionaries(mEditor, aDictionaries);
+#ifdef DEBUG_DICT
+ printf("***** Writing content preferences for |%s|\n",
+ DictionariesToString(aDictionaries).Data());
+#endif
+ } else {
+ // If user sets a dictionary matching the language defined by
+ // document, we consider content pref has been canceled, and we clear
+ // it.
+ ClearCurrentDictionaries(mEditor);
+#ifdef DEBUG_DICT
+ printf("***** Clearing content preferences for |%s|\n",
+ DictionariesToString(aDictionaries).Data());
+#endif
+ }
+
+ // Also store it in as a preference, so we can use it as a fallback.
+ // We don't want this for mail composer because it uses
+ // "spellchecker.dictionary" as a preference.
+ //
+ // XXX: Prefs can only be set in the parent process, so this condition is
+ // necessary to stop libpref from throwing errors. But this should
+ // probably be handled in a better way.
+ if (XRE_IsParentProcess()) {
+ nsCString asString = DictionariesToString(aDictionaries);
+ Preferences::SetCString("spellchecker.dictionary", asString);
+#ifdef DEBUG_DICT
+ printf("***** Possibly storing spellchecker.dictionary |%s|\n",
+ asString.Data());
+#endif
+ }
+ } else {
+ MOZ_ASSERT(flags & nsIEditor::eEditorMailMask);
+ // Since the mail editor can only influence the language selection by the
+ // html lang attribute, set the content-language document to persist
+ // multi language selections.
+ // XXX Why doesn't here use the document of the editor directly?
+ nsCOMPtr<nsIContent> anonymousDivOrEditingHost;
+ if (HTMLEditor* htmlEditor = mEditor->GetAsHTMLEditor()) {
+ anonymousDivOrEditingHost = htmlEditor->ComputeEditingHost();
+ } else {
+ anonymousDivOrEditingHost = mEditor->GetRoot();
+ }
+ RefPtr<Document> ownerDoc = anonymousDivOrEditingHost->OwnerDoc();
+ Document* parentDoc = ownerDoc->GetInProcessParentDocument();
+ if (parentDoc) {
+ parentDoc->SetHeaderData(
+ nsGkAtoms::headerContentLanguage,
+ NS_ConvertUTF8toUTF16(DictionariesToString(aDictionaries)));
+ }
+ }
+ }
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ mSpellChecker->SetCurrentDictionaries(aDictionaries)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise]() { promise->MaybeResolveWithUndefined(); },
+ [promise](nsresult aError) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::UninitSpellChecker() {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ // Cleanup - kill the spell checker
+ DeleteSuggestedWordList();
+ mDictionaryList.Clear();
+ mDictionaryIndex = 0;
+ mDictionaryFetcherGroup++;
+ mSpellChecker = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::SetFilterType(uint32_t aFilterType) {
+ mTxtSrvFilterType = aFilterType;
+ return NS_OK;
+}
+
+nsresult EditorSpellCheck::DeleteSuggestedWordList() {
+ mSuggestedWordList.Clear();
+ mSuggestedWordIndex = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::UpdateCurrentDictionary(
+ nsIEditorSpellCheckCallback* aCallback) {
+ if (NS_WARN_IF(!mSpellChecker)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv;
+
+ RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
+
+ // Get language with html5 algorithm
+ const RefPtr<Element> rootEditableElement =
+ [](const EditorBase& aEditorBase) -> Element* {
+ if (!aEditorBase.IsHTMLEditor()) {
+ return aEditorBase.GetRoot();
+ }
+ if (aEditorBase.IsMailEditor()) {
+ // Shouldn't run spellcheck in a mail editor without focus
+ // (bug 1507543)
+ // XXX Why doesn't here use the document of the editor directly?
+ Element* const editingHost =
+ aEditorBase.AsHTMLEditor()->ComputeEditingHost();
+ if (!editingHost) {
+ return nullptr;
+ }
+ // Try to get topmost document's document element for embedded mail
+ // editor (bug 967494)
+ Document* parentDoc =
+ editingHost->OwnerDoc()->GetInProcessParentDocument();
+ if (!parentDoc) {
+ return editingHost;
+ }
+ return parentDoc->GetDocumentElement();
+ }
+ return aEditorBase.AsHTMLEditor()->GetFocusedElement();
+ }(*mEditor);
+
+ if (!rootEditableElement) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<DictionaryFetcher> fetcher =
+ new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup);
+ rootEditableElement->GetLang(fetcher->mRootContentLang);
+ RefPtr<Document> doc = rootEditableElement->GetComposedDoc();
+ NS_ENSURE_STATE(doc);
+ doc->GetContentLanguage(fetcher->mRootDocContentLang);
+
+ rv = fetcher->Fetch(mEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// Helper function that iterates over the list of dictionaries and sets the one
+// that matches based on a given comparison type.
+bool EditorSpellCheck::BuildDictionaryList(const nsACString& aDictName,
+ const nsTArray<nsCString>& aDictList,
+ enum dictCompare aCompareType,
+ nsTArray<nsCString>& aOutList) {
+ for (const auto& dictStr : aDictList) {
+ bool equals = false;
+ switch (aCompareType) {
+ case DICT_NORMAL_COMPARE:
+ equals = aDictName.Equals(dictStr);
+ break;
+ case DICT_COMPARE_CASE_INSENSITIVE:
+ equals = aDictName.Equals(dictStr, nsCaseInsensitiveCStringComparator);
+ break;
+ case DICT_COMPARE_DASHMATCH:
+ equals = nsStyleUtil::DashMatchCompare(
+ NS_ConvertUTF8toUTF16(dictStr), NS_ConvertUTF8toUTF16(aDictName),
+ nsCaseInsensitiveStringComparator);
+ break;
+ }
+ if (equals) {
+ // Avoid adding duplicates to aOutList.
+ if (aOutList.IndexOf(dictStr) == nsTArray<nsCString>::NoIndex) {
+ aOutList.AppendElement(dictStr);
+ }
+#ifdef DEBUG_DICT
+ printf("***** Trying |%s|.\n", dictStr.get());
+#endif
+ // We always break here. We tried to set the dictionary to an existing
+ // dictionary from the list. This must work, if it doesn't, there is
+ // no point trying another one.
+ return true;
+ }
+ }
+ return false;
+}
+
+nsresult EditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher) {
+ MOZ_ASSERT(aFetcher);
+ RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
+
+ BeginUpdateDictionary();
+
+ if (aFetcher->mGroup < mDictionaryFetcherGroup) {
+ // SetCurrentDictionary was called after the fetch started. Don't overwrite
+ // that dictionary with the fetched one.
+ EndUpdateDictionary();
+ if (aFetcher->mCallback) {
+ aFetcher->mCallback->EditorSpellCheckDone();
+ }
+ return NS_OK;
+ }
+
+ /*
+ * We try to derive the dictionary to use based on the following priorities:
+ * 1) Content preference, so the language the user set for the site before.
+ * (Introduced in bug 678842 and corrected in bug 717433.)
+ * 2) Language set by the website, or any other dictionary that partly
+ * matches that. (Introduced in bug 338427.)
+ * Eg. if the website is "en-GB", a user who only has "en-US" will get
+ * that. If the website is generic "en", the user will get one of the
+ * "en-*" installed. If application locale or system locale is "en-*",
+ * we get it. If others, it is (almost) random.
+ * However, we prefer what is stored in "spellchecker.dictionary",
+ * so if the user chose "en-AU" before, they will get "en-AU" on a plain
+ * "en" site. (Introduced in bug 682564.)
+ * If the site has multiple languages declared in its Content-Language
+ * header and there is no more specific lang tag in HTML, we try to
+ * enable a dictionary for every content language.
+ * 3) The value of "spellchecker.dictionary" which reflects a previous
+ * language choice of the user (on another site).
+ * (This was the original behaviour before the aforementioned bugs
+ * landed).
+ * 4) The user's locale.
+ * 5) Use the current dictionary that is currently set.
+ * 6) The content of the "LANG" environment variable (if set).
+ * 7) The first spell check dictionary installed.
+ */
+
+ // Get the language from the element or its closest parent according to:
+ // https://html.spec.whatwg.org/#attr-lang
+ // This is used in SetCurrentDictionaries.
+ nsCString contentLangs;
+ // Reset mPreferredLangs so we only get the current state.
+ mPreferredLangs.Clear();
+ CopyUTF16toUTF8(aFetcher->mRootContentLang, contentLangs);
+#ifdef DEBUG_DICT
+ printf("***** mPreferredLangs (element) |%s|\n", contentLangs.get());
+#endif
+ if (!contentLangs.IsEmpty()) {
+ mPreferredLangs.AppendElement(contentLangs);
+ } else {
+ // If no luck, try the "Content-Language" header.
+ CopyUTF16toUTF8(aFetcher->mRootDocContentLang, contentLangs);
+#ifdef DEBUG_DICT
+ printf("***** mPreferredLangs (content-language) |%s|\n",
+ contentLangs.get());
+#endif
+ StringToDictionaries(contentLangs, mPreferredLangs);
+ }
+
+ // We obtain a list of available dictionaries.
+ AutoTArray<nsCString, 8> dictList;
+ nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ EndUpdateDictionary();
+ if (aFetcher->mCallback) {
+ aFetcher->mCallback->EditorSpellCheckDone();
+ }
+ return rv;
+ }
+
+ // Priority 1:
+ // If we successfully fetched a dictionary from content prefs, do not go
+ // further. Use this exact dictionary.
+ // Don't use content preferences for editor with eEditorMailMask flag.
+ nsAutoCString dictName;
+ uint32_t flags;
+ mEditor->GetFlags(&flags);
+ if (!(flags & nsIEditor::eEditorMailMask)) {
+ if (!aFetcher->mDictionaries.IsEmpty()) {
+ RefPtr<EditorSpellCheck> self = this;
+ RefPtr<DictionaryFetcher> fetcher = aFetcher;
+ mSpellChecker->SetCurrentDictionaries(aFetcher->mDictionaries)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() {
+#ifdef DEBUG_DICT
+ printf("***** Assigned from content preferences |%s|\n",
+ DictionariesToString(fetcher->mDictionaries).Data());
+#endif
+ // We take an early exit here, so let's not forget to clear
+ // the word list.
+ self->DeleteSuggestedWordList();
+
+ self->EndUpdateDictionary();
+ if (fetcher->mCallback) {
+ fetcher->mCallback->EditorSpellCheckDone();
+ }
+ },
+ [self, fetcher](nsresult aError) {
+ if (aError == NS_ERROR_ABORT) {
+ return;
+ }
+ // May be dictionary was uninstalled ?
+ // Clear the content preference and continue.
+ ClearCurrentDictionaries(self->mEditor);
+
+ // Priority 2 or later will handled by the following
+ self->SetFallbackDictionary(fetcher);
+ });
+ return NS_OK;
+ }
+ }
+ SetFallbackDictionary(aFetcher);
+ return NS_OK;
+}
+
+void EditorSpellCheck::SetDictionarySucceeded(DictionaryFetcher* aFetcher) {
+ DeleteSuggestedWordList();
+ EndUpdateDictionary();
+ if (aFetcher->mCallback) {
+ aFetcher->mCallback->EditorSpellCheckDone();
+ }
+}
+
+void EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher* aFetcher) {
+ MOZ_ASSERT(mUpdateDictionaryRunning);
+
+ AutoTArray<nsCString, 6> tryDictList;
+
+ // We obtain a list of available dictionaries.
+ AutoTArray<nsCString, 8> dictList;
+ nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ EndUpdateDictionary();
+ if (aFetcher->mCallback) {
+ aFetcher->mCallback->EditorSpellCheckDone();
+ }
+ return;
+ }
+
+ // Priority 2:
+ // After checking the content preferences, we use the languages of the element
+ // or document.
+
+ // Get the preference value.
+ nsAutoCString prefDictionariesAsString;
+ Preferences::GetLocalizedCString("spellchecker.dictionary",
+ prefDictionariesAsString);
+ nsTArray<nsCString> prefDictionaries;
+ StringToDictionaries(prefDictionariesAsString, prefDictionaries);
+
+ nsAutoCString appLocaleStr;
+ // We pick one dictionary for every language that the element or document
+ // indicates it contains.
+ for (const auto& dictName : mPreferredLangs) {
+ // RFC 5646 explicitly states that matches should be case-insensitive.
+ if (BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE,
+ tryDictList)) {
+#ifdef DEBUG_DICT
+ printf("***** Trying from element/doc |%s| \n", dictName.get());
+#endif
+ continue;
+ }
+
+ // Required dictionary was not available. Try to get a dictionary
+ // matching at least language part of dictName.
+ mozilla::intl::Locale loc;
+ if (mozilla::intl::LocaleParser::TryParse(dictName, loc).isOk() &&
+ loc.Canonicalize().isOk()) {
+ Span<const char> language = loc.Language().Span();
+ nsAutoCString langCode(language.data(), language.size());
+
+ // Try dictionary.spellchecker preference, if it starts with langCode,
+ // so we don't just get any random dictionary matching the language.
+ bool didAppend = false;
+ for (const auto& dictionary : prefDictionaries) {
+ if (nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(dictionary),
+ NS_ConvertUTF8toUTF16(langCode),
+ nsTDefaultStringComparator)) {
+#ifdef DEBUG_DICT
+ printf(
+ "***** Trying preference value |%s| since it matches language "
+ "code\n",
+ dictionary.Data());
+#endif
+ if (BuildDictionaryList(dictionary, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
+ didAppend = true;
+ break;
+ }
+ }
+ }
+ if (didAppend) {
+ continue;
+ }
+
+ // Use the application locale dictionary when the required language
+ // equals applocation locale language.
+ LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
+ if (!appLocaleStr.IsEmpty()) {
+ mozilla::intl::Locale appLoc;
+ auto result =
+ mozilla::intl::LocaleParser::TryParse(appLocaleStr, appLoc);
+ if (result.isOk() && appLoc.Canonicalize().isOk() &&
+ loc.Language().Span() == appLoc.Language().Span()) {
+ if (BuildDictionaryList(appLocaleStr, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
+ continue;
+ }
+ }
+ }
+
+ // Use the system locale dictionary when the required language equlas
+ // system locale language.
+ nsAutoCString sysLocaleStr;
+ OSPreferences::GetInstance()->GetSystemLocale(sysLocaleStr);
+ if (!sysLocaleStr.IsEmpty()) {
+ mozilla::intl::Locale sysLoc;
+ auto result =
+ mozilla::intl::LocaleParser::TryParse(sysLocaleStr, sysLoc);
+ if (result.isOk() && sysLoc.Canonicalize().isOk() &&
+ loc.Language().Span() == sysLoc.Language().Span()) {
+ if (BuildDictionaryList(sysLocaleStr, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
+ continue;
+ }
+ }
+ }
+
+ // Use any dictionary with the required language.
+#ifdef DEBUG_DICT
+ printf("***** Trying to find match for language code |%s|\n",
+ langCode.get());
+#endif
+ BuildDictionaryList(langCode, dictList, DICT_COMPARE_DASHMATCH,
+ tryDictList);
+ }
+ }
+
+ RefPtr<EditorSpellCheck> self = this;
+ RefPtr<DictionaryFetcher> fetcher = aFetcher;
+ RefPtr<GenericPromise> promise;
+
+ if (tryDictList.IsEmpty()) {
+ // Proceed to priority 3 if the list of dictionaries is empty.
+ promise = GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__);
+ } else {
+ promise = mSpellChecker->SetCurrentDictionaries(tryDictList);
+ }
+
+ // If an error was thrown while setting the dictionary, just
+ // fail silently so that the spellchecker dialog is allowed to come
+ // up. The user can manually reset the language to their choice on
+ // the dialog if it is wrong.
+ promise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
+ [prefDictionaries = prefDictionaries.Clone(), dictList = dictList.Clone(),
+ self, fetcher]() {
+ // Build tryDictList with dictionaries for priorities 4 through 7.
+ // We'll use this list if there is no user preference or trying
+ // the user preference fails.
+ AutoTArray<nsCString, 6> tryDictList;
+
+ // Priority 4:
+ // As next fallback, try the current locale.
+ nsAutoCString appLocaleStr;
+ LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
+#ifdef DEBUG_DICT
+ printf("***** Trying locale |%s|\n", appLocaleStr.get());
+#endif
+ self->BuildDictionaryList(appLocaleStr, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
+
+ // Priority 5:
+ // If we have a current dictionary and we don't have no item in try
+ // list, don't try anything else.
+ nsTArray<nsCString> currentDictionaries;
+ self->GetCurrentDictionaries(currentDictionaries);
+ if (!currentDictionaries.IsEmpty() && tryDictList.IsEmpty()) {
+#ifdef DEBUG_DICT
+ printf("***** Retrieved current dict |%s|\n",
+ DictionariesToString(currentDictionaries).Data());
+#endif
+ self->EndUpdateDictionary();
+ if (fetcher->mCallback) {
+ fetcher->mCallback->EditorSpellCheckDone();
+ }
+ return;
+ }
+
+ // Priority 6:
+ // Try to get current dictionary from environment variable LANG.
+ // LANG = language[_territory][.charset]
+ char* env_lang = getenv("LANG");
+ if (env_lang) {
+ nsAutoCString lang(env_lang);
+ // Strip trailing charset, if there is any.
+ int32_t dot_pos = lang.FindChar('.');
+ if (dot_pos != -1) {
+ lang = Substring(lang, 0, dot_pos);
+ }
+
+ int32_t underScore = lang.FindChar('_');
+ if (underScore != -1) {
+ lang.Replace(underScore, 1, '-');
+#ifdef DEBUG_DICT
+ printf("***** Trying LANG from environment |%s|\n", lang.get());
+#endif
+ self->BuildDictionaryList(
+ lang, dictList, DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
+ }
+ }
+
+ // Priority 7:
+ // If it does not work, pick the first one.
+ if (!dictList.IsEmpty()) {
+ self->BuildDictionaryList(dictList[0], dictList, DICT_NORMAL_COMPARE,
+ tryDictList);
+#ifdef DEBUG_DICT
+ printf("***** Trying first of list |%s|\n", dictList[0].get());
+#endif
+ }
+
+ // Priority 3:
+ // If the document didn't supply a dictionary or the setting
+ // failed, try the user preference next.
+ if (!prefDictionaries.IsEmpty()) {
+ self->mSpellChecker->SetCurrentDictionaries(prefDictionaries)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
+ // Priority 3 failed, we'll use the list we built of
+ // priorities 4 to 7.
+ [tryDictList = tryDictList.Clone(), self, fetcher]() {
+ self->mSpellChecker
+ ->SetCurrentDictionaryFromList(tryDictList)
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() {
+ self->SetDictionarySucceeded(fetcher);
+ });
+ });
+ } else {
+ // We don't have a user preference, so we'll try the list we
+ // built of priorities 4 to 7.
+ self->mSpellChecker->SetCurrentDictionaryFromList(tryDictList)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() { self->SetDictionarySucceeded(fetcher); });
+ }
+ });
+}
+
+} // namespace mozilla
diff --git a/editor/spellchecker/EditorSpellCheck.h b/editor/spellchecker/EditorSpellCheck.h
new file mode 100644
index 0000000000..d9de5d9d40
--- /dev/null
+++ b/editor/spellchecker/EditorSpellCheck.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 mozilla_EditorSpellCheck_h
+#define mozilla_EditorSpellCheck_h
+
+#include "mozilla/mozSpellChecker.h" // for mozilla::CheckWordPromise
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsIEditorSpellCheck.h" // for NS_DECL_NSIEDITORSPELLCHECK, etc
+#include "nsISupportsImpl.h"
+#include "nsString.h" // for nsString
+#include "nsTArray.h" // for nsTArray
+#include "nscore.h" // for nsresult
+
+class mozSpellChecker;
+class nsIEditor;
+
+namespace mozilla {
+
+class DictionaryFetcher;
+class EditorBase;
+
+enum dictCompare {
+ DICT_NORMAL_COMPARE,
+ DICT_COMPARE_CASE_INSENSITIVE,
+ DICT_COMPARE_DASHMATCH
+};
+
+class EditorSpellCheck final : public nsIEditorSpellCheck {
+ friend class DictionaryFetcher;
+
+ public:
+ EditorSpellCheck();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(EditorSpellCheck)
+
+ /* Declare all methods in the nsIEditorSpellCheck interface */
+ NS_DECL_NSIEDITORSPELLCHECK
+
+ mozSpellChecker* GetSpellChecker();
+
+ /**
+ * Like CheckCurrentWord, checks the word you give it, returning true via
+ * promise if it's misspelled.
+ * This is faster than CheckCurrentWord because it does not compute
+ * any suggestions.
+ *
+ * Watch out: this does not clear any suggestions left over from previous
+ * calls to CheckCurrentWord, so there may be suggestions, but they will be
+ * invalid.
+ */
+ RefPtr<mozilla::CheckWordPromise> CheckCurrentWordsNoSuggest(
+ const nsTArray<nsString>& aSuggestedWords);
+
+ protected:
+ virtual ~EditorSpellCheck();
+
+ RefPtr<mozSpellChecker> mSpellChecker;
+ RefPtr<EditorBase> mEditor;
+
+ nsTArray<nsString> mSuggestedWordList;
+
+ // these are the words in the current personal dictionary,
+ // GetPersonalDictionary must be called to load them.
+ nsTArray<nsString> mDictionaryList;
+
+ nsTArray<nsCString> mPreferredLangs;
+
+ uint32_t mTxtSrvFilterType;
+ int32_t mSuggestedWordIndex;
+ int32_t mDictionaryIndex;
+ uint32_t mDictionaryFetcherGroup;
+
+ bool mUpdateDictionaryRunning;
+
+ nsresult DeleteSuggestedWordList();
+
+ bool BuildDictionaryList(const nsACString& aDictName,
+ const nsTArray<nsCString>& aDictList,
+ enum dictCompare aCompareType,
+ nsTArray<nsCString>& aOutList);
+
+ nsresult DictionaryFetched(DictionaryFetcher* aFetchState);
+
+ void SetDictionarySucceeded(DictionaryFetcher* aFetcher);
+ void SetFallbackDictionary(DictionaryFetcher* aFetcher);
+
+ public:
+ void BeginUpdateDictionary() { mUpdateDictionaryRunning = true; }
+ void EndUpdateDictionary() { mUpdateDictionaryRunning = false; }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_EditorSpellCheck_h
diff --git a/editor/spellchecker/FilteredContentIterator.cpp b/editor/spellchecker/FilteredContentIterator.cpp
new file mode 100644
index 0000000000..b0bfcd508a
--- /dev/null
+++ b/editor/spellchecker/FilteredContentIterator.cpp
@@ -0,0 +1,398 @@
+/* -*- 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 "FilteredContentIterator.h"
+
+#include <utility>
+
+#include "mozilla/ContentIterator.h"
+#include "mozilla/dom/AbstractRange.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/mozalloc.h"
+#include "nsAtom.h"
+#include "nsComponentManagerUtils.h"
+#include "nsComposeTxtSrvFilter.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsISupports.h"
+#include "nsISupportsUtils.h"
+#include "nsRange.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+FilteredContentIterator::FilteredContentIterator(
+ UniquePtr<nsComposeTxtSrvFilter> aFilter)
+ : mCurrentIterator(nullptr),
+ mFilter(std::move(aFilter)),
+ mDidSkip(false),
+ mIsOutOfRange(false),
+ mDirection(eDirNotSet) {}
+
+FilteredContentIterator::~FilteredContentIterator() {}
+
+NS_IMPL_CYCLE_COLLECTION(FilteredContentIterator, mPostIterator, mPreIterator,
+ mRange)
+
+nsresult FilteredContentIterator::Init(nsINode* aRoot) {
+ NS_ENSURE_ARG_POINTER(aRoot);
+ mIsOutOfRange = false;
+ mDirection = eForward;
+ mCurrentIterator = &mPreIterator;
+
+ mRange = nsRange::Create(aRoot);
+ mRange->SelectNode(*aRoot, IgnoreErrors());
+
+ nsresult rv = mPreIterator.Init(mRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mPostIterator.Init(mRange);
+}
+
+nsresult FilteredContentIterator::Init(const AbstractRange* aAbstractRange) {
+ if (NS_WARN_IF(!aAbstractRange)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!aAbstractRange->IsPositioned())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mRange = nsRange::Create(aAbstractRange, IgnoreErrors());
+ if (NS_WARN_IF(!mRange)) {
+ return NS_ERROR_FAILURE;
+ }
+ return InitWithRange();
+}
+
+nsresult FilteredContentIterator::Init(nsINode* aStartContainer,
+ uint32_t aStartOffset,
+ nsINode* aEndContainer,
+ uint32_t aEndOffset) {
+ return Init(RawRangeBoundary(aStartContainer, aStartOffset),
+ RawRangeBoundary(aEndContainer, aEndOffset));
+}
+
+nsresult FilteredContentIterator::Init(const RawRangeBoundary& aStartBoundary,
+ const RawRangeBoundary& aEndBoundary) {
+ RefPtr<nsRange> range =
+ nsRange::Create(aStartBoundary, aEndBoundary, IgnoreErrors());
+ if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ MOZ_ASSERT(range->StartRef() == aStartBoundary);
+ MOZ_ASSERT(range->EndRef() == aEndBoundary);
+
+ mRange = std::move(range);
+
+ return InitWithRange();
+}
+
+nsresult FilteredContentIterator::InitWithRange() {
+ MOZ_ASSERT(mRange);
+ MOZ_ASSERT(mRange->IsPositioned());
+
+ mIsOutOfRange = false;
+ mDirection = eForward;
+ mCurrentIterator = &mPreIterator;
+
+ nsresult rv = mPreIterator.Init(mRange);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return mPostIterator.Init(mRange);
+}
+
+nsresult FilteredContentIterator::SwitchDirections(bool aChangeToForward) {
+ nsINode* node = mCurrentIterator->GetCurrentNode();
+
+ if (aChangeToForward) {
+ mCurrentIterator = &mPreIterator;
+ mDirection = eForward;
+ } else {
+ mCurrentIterator = &mPostIterator;
+ mDirection = eBackward;
+ }
+
+ if (node) {
+ nsresult rv = mCurrentIterator->PositionAt(node);
+ if (NS_FAILED(rv)) {
+ mIsOutOfRange = true;
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+void FilteredContentIterator::First() {
+ if (!mCurrentIterator) {
+ NS_ERROR("Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eForward) {
+ mCurrentIterator = &mPreIterator;
+ mDirection = eForward;
+ mIsOutOfRange = false;
+ }
+
+ mCurrentIterator->First();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ nsINode* currentNode = mCurrentIterator->GetCurrentNode();
+
+ bool didCross;
+ CheckAdvNode(currentNode, didCross, eForward);
+}
+
+void FilteredContentIterator::Last() {
+ if (!mCurrentIterator) {
+ NS_ERROR("Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eBackward) {
+ mCurrentIterator = &mPostIterator;
+ mDirection = eBackward;
+ mIsOutOfRange = false;
+ }
+
+ mCurrentIterator->Last();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ nsINode* currentNode = mCurrentIterator->GetCurrentNode();
+
+ bool didCross;
+ CheckAdvNode(currentNode, didCross, eBackward);
+}
+
+///////////////////////////////////////////////////////////////////////////
+// ContentIsInTraversalRange: returns true if content is visited during
+// the traversal of the range in the specified mode.
+//
+static bool ContentIsInTraversalRange(nsIContent* aContent, bool aIsPreMode,
+ nsINode* aStartContainer,
+ int32_t aStartOffset,
+ nsINode* aEndContainer,
+ int32_t aEndOffset) {
+ NS_ENSURE_TRUE(aStartContainer && aEndContainer && aContent, false);
+
+ nsIContent* parentContent = aContent->GetParent();
+ if (MOZ_UNLIKELY(NS_WARN_IF(!parentContent))) {
+ return false;
+ }
+ Maybe<uint32_t> offsetInParent = parentContent->ComputeIndexOf(aContent);
+ NS_WARNING_ASSERTION(
+ offsetInParent.isSome(),
+ "Content is not in the parent, is this called during a DOM mutation?");
+ if (MOZ_UNLIKELY(NS_WARN_IF(offsetInParent.isNothing()))) {
+ return false;
+ }
+
+ if (!aIsPreMode) {
+ MOZ_ASSERT(*offsetInParent != UINT32_MAX);
+ ++(*offsetInParent);
+ }
+
+ const Maybe<int32_t> startRes = nsContentUtils::ComparePoints(
+ aStartContainer, aStartOffset, parentContent, *offsetInParent);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!startRes))) {
+ return false;
+ }
+ const Maybe<int32_t> endRes = nsContentUtils::ComparePoints(
+ aEndContainer, aEndOffset, parentContent, *offsetInParent);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!endRes))) {
+ return false;
+ }
+ return *startRes <= 0 && *endRes >= 0;
+}
+
+static bool ContentIsInTraversalRange(nsRange* aRange, nsIContent* aNextContent,
+ bool aIsPreMode) {
+ // XXXbz we have a caller below (in AdvanceNode) who passes null for
+ // aNextContent!
+ NS_ENSURE_TRUE(aNextContent && aRange, false);
+
+ return ContentIsInTraversalRange(
+ aNextContent, aIsPreMode, aRange->GetStartContainer(),
+ static_cast<int32_t>(aRange->StartOffset()), aRange->GetEndContainer(),
+ static_cast<int32_t>(aRange->EndOffset()));
+}
+
+// Helper function to advance to the next or previous node
+nsresult FilteredContentIterator::AdvanceNode(nsINode* aNode,
+ nsINode*& aNewNode,
+ eDirectionType aDir) {
+ nsCOMPtr<nsIContent> nextNode;
+ if (aDir == eForward) {
+ nextNode = aNode->GetNextSibling();
+ } else {
+ nextNode = aNode->GetPreviousSibling();
+ }
+
+ if (nextNode) {
+ // If we got here, that means we found the nxt/prv node
+ // make sure it is in our DOMRange
+ bool intersects =
+ ContentIsInTraversalRange(mRange, nextNode, aDir == eForward);
+ if (intersects) {
+ aNewNode = nextNode;
+ NS_ADDREF(aNewNode);
+ return NS_OK;
+ }
+ } else {
+ // The next node was null so we need to walk up the parent(s)
+ nsCOMPtr<nsINode> parent = aNode->GetParentNode();
+ NS_ASSERTION(parent, "parent can't be nullptr");
+
+ // Make sure the parent is in the DOMRange before going further
+ // XXXbz why are we passing nextNode, not the parent??? If this gets fixed,
+ // then ContentIsInTraversalRange can stop null-checking its second arg.
+ bool intersects =
+ ContentIsInTraversalRange(mRange, nextNode, aDir == eForward);
+ if (intersects) {
+ // Now find the nxt/prv node after/before this node
+ nsresult rv = AdvanceNode(parent, aNewNode, aDir);
+ if (NS_SUCCEEDED(rv) && aNewNode) {
+ return NS_OK;
+ }
+ }
+ }
+
+ // if we get here it pretty much means
+ // we went out of the DOM Range
+ mIsOutOfRange = true;
+
+ return NS_ERROR_FAILURE;
+}
+
+// Helper function to see if the next/prev node should be skipped
+void FilteredContentIterator::CheckAdvNode(nsINode* aNode, bool& aDidSkip,
+ eDirectionType aDir) {
+ aDidSkip = false;
+ mIsOutOfRange = false;
+
+ if (aNode && mFilter) {
+ nsCOMPtr<nsINode> currentNode = aNode;
+ while (1) {
+ if (mFilter->Skip(aNode)) {
+ aDidSkip = true;
+ // Get the next/prev node and then
+ // see if we should skip that
+ nsCOMPtr<nsINode> advNode;
+ nsresult rv = AdvanceNode(aNode, *getter_AddRefs(advNode), aDir);
+ if (NS_SUCCEEDED(rv) && advNode) {
+ aNode = advNode;
+ } else {
+ return; // fell out of range
+ }
+ } else {
+ if (aNode != currentNode) {
+ nsCOMPtr<nsIContent> content(do_QueryInterface(aNode));
+ mCurrentIterator->PositionAt(content);
+ }
+ return; // found something
+ }
+ }
+ }
+}
+
+void FilteredContentIterator::Next() {
+ if (mIsOutOfRange || !mCurrentIterator) {
+ NS_ASSERTION(mCurrentIterator, "Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eForward) {
+ nsresult rv = SwitchDirections(true);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ }
+
+ mCurrentIterator->Next();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ // If we can't get the current node then
+ // don't check to see if we can skip it
+ nsINode* currentNode = mCurrentIterator->GetCurrentNode();
+
+ CheckAdvNode(currentNode, mDidSkip, eForward);
+}
+
+void FilteredContentIterator::Prev() {
+ if (mIsOutOfRange || !mCurrentIterator) {
+ NS_ASSERTION(mCurrentIterator, "Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eBackward) {
+ nsresult rv = SwitchDirections(false);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ }
+
+ mCurrentIterator->Prev();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ // If we can't get the current node then
+ // don't check to see if we can skip it
+ nsINode* currentNode = mCurrentIterator->GetCurrentNode();
+
+ CheckAdvNode(currentNode, mDidSkip, eBackward);
+}
+
+nsINode* FilteredContentIterator::GetCurrentNode() {
+ if (mIsOutOfRange || !mCurrentIterator) {
+ return nullptr;
+ }
+
+ return mCurrentIterator->GetCurrentNode();
+}
+
+bool FilteredContentIterator::IsDone() {
+ if (mIsOutOfRange || !mCurrentIterator) {
+ return true;
+ }
+
+ return mCurrentIterator->IsDone();
+}
+
+nsresult FilteredContentIterator::PositionAt(nsINode* aCurNode) {
+ NS_ENSURE_TRUE(mCurrentIterator, NS_ERROR_FAILURE);
+ mIsOutOfRange = false;
+ return mCurrentIterator->PositionAt(aCurNode);
+}
+
+} // namespace mozilla
diff --git a/editor/spellchecker/FilteredContentIterator.h b/editor/spellchecker/FilteredContentIterator.h
new file mode 100644
index 0000000000..864cc57c2a
--- /dev/null
+++ b/editor/spellchecker/FilteredContentIterator.h
@@ -0,0 +1,82 @@
+/* -*- 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 FilteredContentIterator_h
+#define FilteredContentIterator_h
+
+#include "nsComposeTxtSrvFilter.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/UniquePtr.h"
+
+class nsAtom;
+class nsINode;
+class nsRange;
+
+namespace mozilla {
+
+namespace dom {
+class AbstractRange;
+}
+
+class FilteredContentIterator final {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(FilteredContentIterator)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(FilteredContentIterator)
+
+ explicit FilteredContentIterator(UniquePtr<nsComposeTxtSrvFilter> aFilter);
+
+ nsresult Init(nsINode* aRoot);
+ nsresult Init(const dom::AbstractRange* aAbstractRange);
+ nsresult Init(nsINode* aStartContainer, uint32_t aStartOffset,
+ nsINode* aEndContainer, uint32_t aEndOffset);
+ nsresult Init(const RawRangeBoundary& aStartBoundary,
+ const RawRangeBoundary& aEndBoundary);
+ void First();
+ void Last();
+ void Next();
+ void Prev();
+ nsINode* GetCurrentNode();
+ bool IsDone();
+ nsresult PositionAt(nsINode* aCurNode);
+
+ /* Helpers */
+ bool DidSkip() { return mDidSkip; }
+ void ClearDidSkip() { mDidSkip = false; }
+
+ protected:
+ FilteredContentIterator()
+ : mDidSkip(false), mIsOutOfRange(false), mDirection{eDirNotSet} {}
+
+ virtual ~FilteredContentIterator();
+
+ /**
+ * Callers must guarantee that mRange isn't nullptr and it's positioned.
+ */
+ nsresult InitWithRange();
+
+ // enum to give us the direction
+ typedef enum { eDirNotSet, eForward, eBackward } eDirectionType;
+ nsresult AdvanceNode(nsINode* aNode, nsINode*& aNewNode, eDirectionType aDir);
+ void CheckAdvNode(nsINode* aNode, bool& aDidSkip, eDirectionType aDir);
+ nsresult SwitchDirections(bool aChangeToForward);
+
+ ContentIteratorBase* MOZ_NON_OWNING_REF mCurrentIterator;
+ PostContentIterator mPostIterator;
+ PreContentIterator mPreIterator;
+
+ UniquePtr<nsComposeTxtSrvFilter> mFilter;
+ RefPtr<nsRange> mRange;
+ bool mDidSkip;
+ bool mIsOutOfRange;
+ eDirectionType mDirection;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef FilteredContentIterator_h
diff --git a/editor/spellchecker/TextServicesDocument.cpp b/editor/spellchecker/TextServicesDocument.cpp
new file mode 100644
index 0000000000..c348c70ad6
--- /dev/null
+++ b/editor/spellchecker/TextServicesDocument.cpp
@@ -0,0 +1,2809 @@
+/* -*- 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 "TextServicesDocument.h"
+
+#include "EditorBase.h" // for EditorBase
+#include "EditorUtils.h" // for AutoTransactionBatchExternal
+#include "FilteredContentIterator.h" // for FilteredContentIterator
+#include "HTMLEditUtils.h" // for HTMLEditUtils
+#include "JoinSplitNodeDirection.h" // for JoinNodesDirection
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/IntegerRange.h" // for IntegerRange
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/UniquePtr.h" // for UniquePtr
+#include "mozilla/dom/AbstractRange.h" // for AbstractRange
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h" // for StaticRange
+#include "mozilla/dom/Text.h"
+#include "mozilla/intl/WordBreaker.h" // for WordRange, WordBreaker
+
+#include "nsAString.h" // for nsAString::Length, etc
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsComposeTxtSrvFilter.h"
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsDependentSubstring.h" // for Substring
+#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc
+#include "nsGenericHTMLElement.h" // for nsGenericHTMLElement
+#include "nsIContent.h" // for nsIContent, etc
+#include "nsID.h" // for NS_GET_IID
+#include "nsIEditor.h" // for nsIEditor, etc
+#include "nsIEditorSpellCheck.h" // for nsIEditorSpellCheck, etc
+#include "nsINode.h" // for nsINode
+#include "nsISelectionController.h" // for nsISelectionController, etc
+#include "nsISupports.h" // for nsISupports
+#include "nsISupportsUtils.h" // for NS_IF_ADDREF, NS_ADDREF, etc
+#include "nsRange.h" // for nsRange
+#include "nsString.h" // for nsString, nsAutoString
+#include "nscore.h" // for nsresult, NS_IMETHODIMP, etc
+
+namespace mozilla {
+
+using namespace dom;
+
+/**
+ * OffsetEntry manages a range in a text node. It stores 2 offset values,
+ * one is offset in the text node, the other is offset in all text in
+ * the ancestor block of the text node. And the length is managing length
+ * in the text node, starting from the offset in text node.
+ * In other words, a text node may be managed by multiple instances of this
+ * class.
+ */
+class OffsetEntry final {
+ public:
+ OffsetEntry() = delete;
+
+ /**
+ * @param aTextNode The text node which will be manged by the instance.
+ * @param aOffsetInTextInBlock
+ * Start offset in the text node which will be managed by
+ * the instance.
+ * @param aLength Length in the text node which will be managed by the
+ * instance.
+ */
+ OffsetEntry(Text& aTextNode, uint32_t aOffsetInTextInBlock, uint32_t aLength)
+ : mTextNode(aTextNode),
+ mOffsetInTextNode(0),
+ mOffsetInTextInBlock(aOffsetInTextInBlock),
+ mLength(aLength),
+ mIsInsertedText(false),
+ mIsValid(true) {}
+
+ /**
+ * EndOffsetInTextNode() returns end offset in the text node, which is
+ * managed by the instance.
+ */
+ uint32_t EndOffsetInTextNode() const { return mOffsetInTextNode + mLength; }
+
+ /**
+ * OffsetInTextNodeIsInRangeOrEndOffset() checks whether the offset in
+ * the text node is managed by the instance or not.
+ */
+ bool OffsetInTextNodeIsInRangeOrEndOffset(uint32_t aOffsetInTextNode) const {
+ return aOffsetInTextNode >= mOffsetInTextNode &&
+ aOffsetInTextNode <= EndOffsetInTextNode();
+ }
+
+ /**
+ * EndOffsetInTextInBlock() returns end offset in the all text in ancestor
+ * block of the text node, which is managed by the instance.
+ */
+ uint32_t EndOffsetInTextInBlock() const {
+ return mOffsetInTextInBlock + mLength;
+ }
+
+ /**
+ * OffsetInTextNodeIsInRangeOrEndOffset() checks whether the offset in
+ * the all text in ancestor block of the text node is managed by the instance
+ * or not.
+ */
+ bool OffsetInTextInBlockIsInRangeOrEndOffset(
+ uint32_t aOffsetInTextInBlock) const {
+ return aOffsetInTextInBlock >= mOffsetInTextInBlock &&
+ aOffsetInTextInBlock <= EndOffsetInTextInBlock();
+ }
+
+ OwningNonNull<Text> mTextNode;
+ uint32_t mOffsetInTextNode;
+ // Offset in all text in the closest ancestor block of mTextNode.
+ uint32_t mOffsetInTextInBlock;
+ uint32_t mLength;
+ bool mIsInsertedText;
+ bool mIsValid;
+};
+
+template <typename ElementType>
+struct MOZ_STACK_CLASS ArrayLengthMutationGuard final {
+ ArrayLengthMutationGuard() = delete;
+ explicit ArrayLengthMutationGuard(const nsTArray<ElementType>& aArray)
+ : mArray(aArray), mOldLength(aArray.Length()) {}
+ ~ArrayLengthMutationGuard() {
+ if (mArray.Length() != mOldLength) {
+ MOZ_CRASH("The array length was changed unexpectedly");
+ }
+ }
+
+ private:
+ const nsTArray<ElementType>& mArray;
+ size_t mOldLength;
+};
+
+#define LockOffsetEntryArrayLengthInDebugBuild(aName, aArray) \
+ DebugOnly<ArrayLengthMutationGuard<UniquePtr<OffsetEntry>>> const aName = \
+ ArrayLengthMutationGuard<UniquePtr<OffsetEntry>>(aArray);
+
+TextServicesDocument::TextServicesDocument()
+ : mTxtSvcFilterType(0), mIteratorStatus(IteratorStatus::eDone) {}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TextServicesDocument)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TextServicesDocument)
+
+NS_INTERFACE_MAP_BEGIN(TextServicesDocument)
+ NS_INTERFACE_MAP_ENTRY(nsIEditActionListener)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditActionListener)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(TextServicesDocument)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(TextServicesDocument, mDocument, mSelCon, mEditorBase,
+ mFilteredIter, mPrevTextBlock, mNextTextBlock, mExtent)
+
+nsresult TextServicesDocument::InitWithEditor(nsIEditor* aEditor) {
+ nsCOMPtr<nsISelectionController> selCon;
+
+ NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
+
+ // Check to see if we already have an mSelCon. If we do, it
+ // better be the same one the editor uses!
+
+ nsresult rv = aEditor->GetSelectionController(getter_AddRefs(selCon));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!selCon || (mSelCon && selCon != mSelCon)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mSelCon) {
+ mSelCon = selCon;
+ }
+
+ // Check to see if we already have an mDocument. If we do, it
+ // better be the same one the editor uses!
+
+ RefPtr<Document> doc = aEditor->AsEditorBase()->GetDocument();
+ if (!doc || (mDocument && doc != mDocument)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mDocument) {
+ mDocument = doc;
+
+ rv = CreateDocumentContentIterator(getter_AddRefs(mFilteredIter));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eDone;
+
+ rv = FirstBlock();
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ mEditorBase = aEditor->AsEditorBase();
+
+ rv = aEditor->AddEditActionListener(this);
+
+ return rv;
+}
+
+nsresult TextServicesDocument::SetExtent(const AbstractRange* aAbstractRange) {
+ MOZ_ASSERT(aAbstractRange);
+
+ if (NS_WARN_IF(!mDocument)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // We need to store a copy of aAbstractRange since we don't know where it
+ // came from.
+ mExtent = nsRange::Create(aAbstractRange, IgnoreErrors());
+ if (NS_WARN_IF(!mExtent)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create a new iterator based on our new extent range.
+ nsresult rv =
+ CreateFilteredContentIterator(mExtent, getter_AddRefs(mFilteredIter));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Now position the iterator at the start of the first block
+ // in the range.
+ mIteratorStatus = IteratorStatus::eDone;
+
+ rv = FirstBlock();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "FirstBlock() failed");
+ return rv;
+}
+
+nsresult TextServicesDocument::ExpandRangeToWordBoundaries(
+ StaticRange* aStaticRange) {
+ MOZ_ASSERT(aStaticRange);
+
+ // Get the end points of the range.
+
+ nsCOMPtr<nsINode> rngStartNode, rngEndNode;
+ uint32_t rngStartOffset, rngEndOffset;
+
+ nsresult rv = GetRangeEndPoints(aStaticRange, getter_AddRefs(rngStartNode),
+ &rngStartOffset, getter_AddRefs(rngEndNode),
+ &rngEndOffset);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Create a content iterator based on the range.
+ RefPtr<FilteredContentIterator> filteredIter;
+ rv =
+ CreateFilteredContentIterator(aStaticRange, getter_AddRefs(filteredIter));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Find the first text node in the range.
+ IteratorStatus iterStatus = IteratorStatus::eDone;
+ rv = FirstTextNode(filteredIter, &iterStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (iterStatus == IteratorStatus::eDone) {
+ // No text was found so there's no adjustment necessary!
+ return NS_OK;
+ }
+
+ nsINode* firstText = filteredIter->GetCurrentNode();
+ if (NS_WARN_IF(!firstText)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Find the last text node in the range.
+
+ rv = LastTextNode(filteredIter, &iterStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (iterStatus == IteratorStatus::eDone) {
+ // We should never get here because a first text block
+ // was found above.
+ NS_ASSERTION(false, "Found a first without a last!");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsINode* lastText = filteredIter->GetCurrentNode();
+ if (NS_WARN_IF(!lastText)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now make sure our end points are in terms of text nodes in the range!
+
+ if (rngStartNode != firstText) {
+ // The range includes the start of the first text node!
+ rngStartNode = firstText;
+ rngStartOffset = 0;
+ }
+
+ if (rngEndNode != lastText) {
+ // The range includes the end of the last text node!
+ rngEndNode = lastText;
+ rngEndOffset = lastText->Length();
+ }
+
+ // Create a doc iterator so that we can scan beyond
+ // the bounds of the extent range.
+
+ RefPtr<FilteredContentIterator> docFilteredIter;
+ rv = CreateDocumentContentIterator(getter_AddRefs(docFilteredIter));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Grab all the text in the block containing our
+ // first text node.
+ rv = docFilteredIter->PositionAt(firstText);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ iterStatus = IteratorStatus::eValid;
+
+ OffsetEntryArray offsetTable;
+ nsAutoString blockStr;
+ Result<IteratorStatus, nsresult> result = offsetTable.Init(
+ *docFilteredIter, IteratorStatus::eValid, nullptr, &blockStr);
+ if (result.isErr()) {
+ return result.unwrapErr();
+ }
+
+ Result<EditorDOMRangeInTexts, nsresult> maybeWordRange =
+ offsetTable.FindWordRange(
+ blockStr, EditorRawDOMPoint(rngStartNode, rngStartOffset));
+ offsetTable.Clear();
+ if (maybeWordRange.isErr()) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryArray::FindWordRange() failed");
+ return maybeWordRange.unwrapErr();
+ }
+ rngStartNode = maybeWordRange.inspect().StartRef().GetContainerAs<Text>();
+ rngStartOffset = maybeWordRange.inspect().StartRef().Offset();
+
+ // Grab all the text in the block containing our
+ // last text node.
+
+ rv = docFilteredIter->PositionAt(lastText);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ result = offsetTable.Init(*docFilteredIter, IteratorStatus::eValid, nullptr,
+ &blockStr);
+ if (result.isErr()) {
+ return result.unwrapErr();
+ }
+
+ maybeWordRange = offsetTable.FindWordRange(
+ blockStr, EditorRawDOMPoint(rngEndNode, rngEndOffset));
+ offsetTable.Clear();
+ if (maybeWordRange.isErr()) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryArray::FindWordRange() failed");
+ return maybeWordRange.unwrapErr();
+ }
+
+ // To prevent expanding the range too much, we only change
+ // rngEndNode and rngEndOffset if it isn't already at the start of the
+ // word and isn't equivalent to rngStartNode and rngStartOffset.
+
+ if (rngEndNode !=
+ maybeWordRange.inspect().StartRef().GetContainerAs<Text>() ||
+ rngEndOffset != maybeWordRange.inspect().StartRef().Offset() ||
+ (rngEndNode == rngStartNode && rngEndOffset == rngStartOffset)) {
+ rngEndNode = maybeWordRange.inspect().EndRef().GetContainerAs<Text>();
+ rngEndOffset = maybeWordRange.inspect().EndRef().Offset();
+ }
+
+ // Now adjust the range so that it uses our new end points.
+ rv = aStaticRange->SetStartAndEnd(rngStartNode, rngStartOffset, rngEndNode,
+ rngEndOffset);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to update the given range");
+ return rv;
+}
+
+nsresult TextServicesDocument::SetFilterType(uint32_t aFilterType) {
+ mTxtSvcFilterType = aFilterType;
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::GetCurrentTextBlock(nsAString& aStr) {
+ aStr.Truncate();
+
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ Result<IteratorStatus, nsresult> result =
+ mOffsetTable.Init(*mFilteredIter, mIteratorStatus, mExtent, &aStr);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.unwrap();
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::FirstBlock() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ nsresult rv = FirstTextNode(mFilteredIter, &mIteratorStatus);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+
+ if (mIteratorStatus == IteratorStatus::eValid) {
+ mPrevTextBlock = nullptr;
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // There's no text block in the document!
+
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ // XXX Result of FirstTextNode() or GetFirstTextNodeInNextBlock().
+ return rv;
+}
+
+nsresult TextServicesDocument::LastSelectedBlock(
+ BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset,
+ uint32_t* aSelLength) {
+ NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER);
+
+ mIteratorStatus = IteratorStatus::eDone;
+
+ *aSelStatus = BlockSelectionStatus::eBlockNotFound;
+ *aSelOffset = *aSelLength = UINT32_MAX;
+
+ if (!mSelCon || !mFilteredIter) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<const nsRange> range;
+ nsCOMPtr<nsINode> parent;
+
+ if (selection->IsCollapsed()) {
+ // We have a caret. Check if the caret is in a text node.
+ // If it is, make the text node's block the current block.
+ // If the caret isn't in a text node, search forwards in
+ // the document, till we find a text node.
+
+ range = selection->GetRangeAt(0);
+ if (!range) {
+ return NS_ERROR_FAILURE;
+ }
+
+ parent = range->GetStartContainer();
+ if (!parent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+ if (parent->IsText()) {
+ // The caret is in a text node. Find the beginning
+ // of the text block containing this text node and
+ // return.
+
+ rv = mFilteredIter->PositionAt(parent->AsText());
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ Result<IteratorStatus, nsresult> result =
+ mOffsetTable.Init(*mFilteredIter, IteratorStatus::eValid, mExtent);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ mIteratorStatus = IteratorStatus::eValid; // XXX
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.unwrap();
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (*aSelStatus == BlockSelectionStatus::eBlockContains) {
+ rv = SetSelectionInternal(*aSelOffset, *aSelLength, false);
+ }
+ } else {
+ // The caret isn't in a text node. Create an iterator
+ // based on a range that extends from the current caret
+ // position to the end of the document, then walk forwards
+ // till you find a text node, then find the beginning of it's block.
+
+ range = CreateDocumentContentRootToNodeOffsetRange(
+ parent, range->StartOffset(), false);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (range->Collapsed()) {
+ // If we get here, the range is collapsed because there is nothing after
+ // the caret! Just return NS_OK;
+ return NS_OK;
+ }
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ rv = CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filteredIter->First();
+
+ Text* textNode = nullptr;
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ nsINode* currentNode = filteredIter->GetCurrentNode();
+ if (currentNode->IsText()) {
+ textNode = currentNode->AsText();
+ break;
+ }
+ }
+
+ if (!textNode) {
+ return NS_OK;
+ }
+
+ rv = mFilteredIter->PositionAt(textNode);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ Result<IteratorStatus, nsresult> result = mOffsetTable.Init(
+ *mFilteredIter, IteratorStatus::eValid, mExtent, nullptr);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ mIteratorStatus = IteratorStatus::eValid; // XXX
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.inspect();
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Result of SetSelectionInternal() in the |if| block or NS_OK.
+ return rv;
+ }
+
+ // If we get here, we have an uncollapsed selection!
+ // Look backwards through each range in the selection till you
+ // find the first text node. If you find one, find the
+ // beginning of its text block, and make it the current
+ // block.
+
+ const uint32_t rangeCount = selection->RangeCount();
+ MOZ_ASSERT(
+ rangeCount,
+ "Selection is not collapsed, so, the range count should be 1 or larger");
+
+ // XXX: We may need to add some code here to make sure
+ // the ranges are sorted in document appearance order!
+
+ for (const uint32_t i : Reversed(IntegerRange(rangeCount))) {
+ MOZ_ASSERT(selection->RangeCount() == rangeCount);
+ range = selection->GetRangeAt(i);
+ if (MOZ_UNLIKELY(!range)) {
+ return NS_OK; // XXX Really?
+ }
+
+ // Create an iterator for the range.
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filteredIter->Last();
+
+ // Now walk through the range till we find a text node.
+
+ for (; !filteredIter->IsDone(); filteredIter->Prev()) {
+ if (filteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ // We found a text node, so position the document's
+ // iterator at the beginning of the block, then get
+ // the selection in terms of the string offset.
+
+ nsresult rv = mFilteredIter->PositionAt(filteredIter->GetCurrentNode());
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+
+ Result<IteratorStatus, nsresult> result =
+ mOffsetTable.Init(*mFilteredIter, IteratorStatus::eValid, mExtent);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ mIteratorStatus = IteratorStatus::eValid; // XXX
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.unwrap();
+
+ return GetSelection(aSelStatus, aSelOffset, aSelLength);
+ }
+ }
+ }
+
+ // If we get here, we didn't find any text node in the selection!
+ // Create a range that extends from the end of the selection,
+ // to the end of the document, then iterate forwards through
+ // it till you find a text node!
+ range = rangeCount > 0 ? selection->GetRangeAt(rangeCount - 1) : nullptr;
+ if (!range) {
+ return NS_ERROR_FAILURE;
+ }
+
+ parent = range->GetEndContainer();
+ if (!parent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ range = CreateDocumentContentRootToNodeOffsetRange(parent, range->EndOffset(),
+ false);
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (range->Collapsed()) {
+ // If we get here, the range is collapsed because there is nothing after
+ // the current selection! Just return NS_OK;
+ return NS_OK;
+ }
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filteredIter->First();
+
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ if (filteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ // We found a text node! Adjust the document's iterator to point
+ // to the beginning of its text block, then get the current selection.
+ nsresult rv = mFilteredIter->PositionAt(filteredIter->GetCurrentNode());
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ Result<IteratorStatus, nsresult> result =
+ mOffsetTable.Init(*mFilteredIter, IteratorStatus::eValid, mExtent);
+ if (result.isErr()) {
+ NS_WARNING("OffsetEntryArray::Init() failed");
+ mIteratorStatus = IteratorStatus::eValid; // XXX
+ return result.unwrapErr();
+ }
+ mIteratorStatus = result.unwrap();
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextServicesDocument::GetSelection() failed");
+ return rv;
+ }
+ }
+
+ // If we get here, we didn't find any block before or inside
+ // the selection! Just return OK.
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::PrevBlock() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ switch (mIteratorStatus) {
+ case IteratorStatus::eValid:
+ case IteratorStatus::eNext: {
+ nsresult rv = FirstTextNodeInPrevBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ if (mFilteredIter->IsDone()) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return NS_OK;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+ }
+ case IteratorStatus::ePrev:
+
+ // The iterator already points to the previous
+ // block, so don't do anything.
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+
+ default:
+
+ mIteratorStatus = IteratorStatus::eDone;
+ break;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+ nsresult rv = NS_OK;
+ if (mIteratorStatus == IteratorStatus::eValid) {
+ GetFirstTextNodeInPrevBlock(getter_AddRefs(mPrevTextBlock));
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // We must be done!
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ // XXX The result of GetFirstTextNodeInNextBlock() or NS_OK.
+ return rv;
+}
+
+nsresult TextServicesDocument::NextBlock() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ switch (mIteratorStatus) {
+ case IteratorStatus::eValid: {
+ // Advance the iterator to the next text block.
+
+ nsresult rv = FirstTextNodeInNextBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ if (mFilteredIter->IsDone()) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return NS_OK;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+ }
+ case IteratorStatus::eNext:
+
+ // The iterator already points to the next block,
+ // so don't do anything to it!
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+
+ case IteratorStatus::ePrev:
+
+ // If the iterator is pointing to the previous block,
+ // we know that there is no next text block! Just
+ // fall through to the default case!
+
+ default:
+
+ mIteratorStatus = IteratorStatus::eDone;
+ break;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+ nsresult rv = NS_OK;
+ if (mIteratorStatus == IteratorStatus::eValid) {
+ GetFirstTextNodeInPrevBlock(getter_AddRefs(mPrevTextBlock));
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // We must be done.
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ // The result of GetFirstTextNodeInNextBlock() or NS_OK.
+ return rv;
+}
+
+nsresult TextServicesDocument::IsDone(bool* aIsDone) {
+ NS_ENSURE_TRUE(aIsDone, NS_ERROR_NULL_POINTER);
+
+ *aIsDone = false;
+
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ *aIsDone = mIteratorStatus == IteratorStatus::eDone;
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::SetSelection(uint32_t aOffset,
+ uint32_t aLength) {
+ NS_ENSURE_TRUE(mSelCon, NS_ERROR_FAILURE);
+
+ return SetSelectionInternal(aOffset, aLength, true);
+}
+
+nsresult TextServicesDocument::ScrollSelectionIntoView() {
+ NS_ENSURE_TRUE(mSelCon, NS_ERROR_FAILURE);
+
+ // After ScrollSelectionIntoView(), the pending notifications might be flushed
+ // and PresShell/PresContext/Frames may be dead. See bug 418470.
+ nsresult rv = mSelCon->ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_FOCUS_REGION,
+ nsISelectionController::SCROLL_SYNCHRONOUS);
+
+ return rv;
+}
+
+nsresult TextServicesDocument::OffsetEntryArray::WillDeleteSelection() {
+ MOZ_ASSERT(mSelection.IsSet());
+ MOZ_ASSERT(!mSelection.IsCollapsed());
+
+ for (size_t i = mSelection.StartIndex(); i <= mSelection.EndIndex(); i++) {
+ OffsetEntry* entry = ElementAt(i).get();
+ if (i == mSelection.StartIndex()) {
+ // Calculate the length of the selection. Note that the
+ // selection length can be zero if the start of the selection
+ // is at the very end of a text node entry.
+ uint32_t selLength;
+ if (entry->mIsInsertedText) {
+ // Inserted text offset entries have no width when
+ // talking in terms of string offsets! If the beginning
+ // of the selection is in an inserted text offset entry,
+ // the caret is always at the end of the entry!
+ selLength = 0;
+ } else {
+ selLength = entry->EndOffsetInTextInBlock() -
+ mSelection.StartOffsetInTextInBlock();
+ }
+
+ if (selLength > 0) {
+ if (mSelection.StartOffsetInTextInBlock() >
+ entry->mOffsetInTextInBlock) {
+ // Selection doesn't start at the beginning of the
+ // text node entry. We need to split this entry into
+ // two pieces, the piece before the selection, and
+ // the piece inside the selection.
+ nsresult rv = SplitElementAt(i, selLength);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("selLength was invalid for the OffsetEntry");
+ return rv;
+ }
+
+ // Adjust selection indexes to account for new entry:
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() + 1 < Length());
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.EndIndex() + 1 < Length());
+ mSelection.SetIndexes(mSelection.StartIndex() + 1,
+ mSelection.EndIndex() + 1);
+ entry = ElementAt(++i).get();
+ }
+
+ if (mSelection.StartIndex() < mSelection.EndIndex()) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+ }
+
+ if (i == mSelection.EndIndex()) {
+ if (entry->mIsInsertedText) {
+ // Inserted text offset entries have no width when
+ // talking in terms of string offsets! If the end
+ // of the selection is in an inserted text offset entry,
+ // the selection includes the entire entry!
+ entry->mIsValid = false;
+ } else {
+ // Calculate the length of the selection. Note that the
+ // selection length can be zero if the end of the selection
+ // is at the very beginning of a text node entry.
+
+ const uint32_t selLength =
+ mSelection.EndOffsetInTextInBlock() - entry->mOffsetInTextInBlock;
+ if (selLength) {
+ if (mSelection.EndOffsetInTextInBlock() <
+ entry->EndOffsetInTextInBlock()) {
+ // mOffsetInTextInBlock is guaranteed to be inside the selection,
+ // even when mSelection.IsInSameElement() is true.
+ nsresult rv = SplitElementAt(i, entry->mLength - selLength);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "entry->mLength - selLength was invalid for the OffsetEntry");
+ return rv;
+ }
+
+ // Update the entry fields:
+ ElementAt(i + 1)->mOffsetInTextNode = entry->mOffsetInTextNode;
+ }
+
+ if (mSelection.EndOffsetInTextInBlock() ==
+ entry->EndOffsetInTextInBlock()) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+ }
+ }
+
+ if (i != mSelection.StartIndex() && i != mSelection.EndIndex()) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::DeleteSelection() {
+ if (NS_WARN_IF(!mEditorBase) ||
+ NS_WARN_IF(!mOffsetTable.mSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mOffsetTable.mSelection.IsCollapsed()) {
+ return NS_OK;
+ }
+
+ // If we have an mExtent, save off its current set of
+ // end points so we can compare them against mExtent's
+ // set after the deletion of the content.
+
+ nsCOMPtr<nsINode> origStartNode, origEndNode;
+ uint32_t origStartOffset = 0, origEndOffset = 0;
+
+ if (mExtent) {
+ nsresult rv = GetRangeEndPoints(
+ mExtent, getter_AddRefs(origStartNode), &origStartOffset,
+ getter_AddRefs(origEndNode), &origEndOffset);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ if (NS_FAILED(mOffsetTable.WillDeleteSelection())) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryTable::WillDeleteSelection() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure mFilteredIter always points to something valid!
+ AdjustContentIterator();
+
+ // Now delete the actual content!
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ nsresult rv = editorBase->DeleteSelectionAsAction(nsIEditor::ePrevious,
+ nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Now that we've actually deleted the selected content,
+ // check to see if our mExtent has changed, if so, then
+ // we have to create a new content iterator!
+
+ if (origStartNode && origEndNode) {
+ nsCOMPtr<nsINode> curStartNode, curEndNode;
+ uint32_t curStartOffset = 0, curEndOffset = 0;
+
+ rv = GetRangeEndPoints(mExtent, getter_AddRefs(curStartNode),
+ &curStartOffset, getter_AddRefs(curEndNode),
+ &curEndOffset);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (origStartNode != curStartNode || origEndNode != curEndNode) {
+ // The range has changed, so we need to create a new content
+ // iterator based on the new range.
+ nsCOMPtr<nsIContent> curContent;
+ if (mIteratorStatus != IteratorStatus::eDone) {
+ // The old iterator is still pointing to something valid,
+ // so get its current node so we can restore it after we
+ // create the new iterator!
+ curContent = mFilteredIter->GetCurrentNode()
+ ? mFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ }
+
+ // Create the new iterator.
+ rv =
+ CreateFilteredContentIterator(mExtent, getter_AddRefs(mFilteredIter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Now make the new iterator point to the content node
+ // the old one was pointing at.
+ if (curContent) {
+ rv = mFilteredIter->PositionAt(curContent);
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ } else {
+ mIteratorStatus = IteratorStatus::eValid;
+ }
+ }
+ }
+ }
+
+ OffsetEntry* entry = mOffsetTable.DidDeleteSelection();
+ if (entry) {
+ SetSelection(mOffsetTable.mSelection.StartOffsetInTextInBlock(), 0);
+ }
+
+ // Now remove any invalid entries from the offset table.
+ mOffsetTable.RemoveInvalidElements();
+ return NS_OK;
+}
+
+OffsetEntry* TextServicesDocument::OffsetEntryArray::DidDeleteSelection() {
+ MOZ_ASSERT(mSelection.IsSet());
+
+ // Move the caret to the end of the first valid entry.
+ // Start with SelectionStartIndex() since it may still be valid.
+ OffsetEntry* entry = nullptr;
+ for (size_t i = mSelection.StartIndex() + 1; !entry && i > 0; i--) {
+ entry = ElementAt(i - 1).get();
+ if (!entry->mIsValid) {
+ entry = nullptr;
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(i - 1 < Length());
+ mSelection.Set(i - 1, entry->EndOffsetInTextInBlock());
+ }
+ }
+
+ // If we still don't have a valid entry, move the caret
+ // to the next valid entry after the selection:
+ for (size_t i = mSelection.EndIndex(); !entry && i < Length(); i++) {
+ entry = ElementAt(i).get();
+ if (!entry->mIsValid) {
+ entry = nullptr;
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(i < Length());
+ mSelection.Set(i, entry->mOffsetInTextInBlock);
+ }
+ }
+
+ if (!entry) {
+ // Uuughh we have no valid offset entry to place our
+ // caret ... just mark the selection invalid.
+ mSelection.Reset();
+ }
+
+ return entry;
+}
+
+nsresult TextServicesDocument::InsertText(const nsAString& aText) {
+ if (NS_WARN_IF(!mEditorBase) ||
+ NS_WARN_IF(!mOffsetTable.mSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If the selection is not collapsed, we need to save
+ // off the selection offsets so we can restore the
+ // selection and delete the selected content after we've
+ // inserted the new text. This is necessary to try and
+ // retain as much of the original style of the content
+ // being deleted.
+
+ const bool wasSelectionCollapsed = mOffsetTable.mSelection.IsCollapsed();
+ const uint32_t savedSelOffset =
+ mOffsetTable.mSelection.StartOffsetInTextInBlock();
+ const uint32_t savedSelLength = mOffsetTable.mSelection.LengthInTextInBlock();
+
+ if (!wasSelectionCollapsed) {
+ // Collapse to the start of the current selection
+ // for the insert!
+ nsresult rv =
+ SetSelection(mOffsetTable.mSelection.StartOffsetInTextInBlock(), 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // AutoTransactionBatchExternal grabs mEditorBase, so, we don't need to grab
+ // the instance with local variable here.
+ OwningNonNull<EditorBase> editorBase = *mEditorBase;
+ AutoTransactionBatchExternal treatAsOneTransaction(editorBase);
+
+ nsresult rv = editorBase->InsertTextAsAction(aText);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("InsertTextAsAction() failed");
+ return rv;
+ }
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ rv = mOffsetTable.DidInsertText(selection, aText);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextServicesDocument::OffsetEntry::DidInsertText() failed");
+ return rv;
+ }
+
+ if (!wasSelectionCollapsed) {
+ nsresult rv = SetSelection(savedSelOffset, savedSelLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = DeleteSelection();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::OffsetEntryArray::DidInsertText(
+ dom::Selection* aSelection, const nsAString& aInsertedString) {
+ MOZ_ASSERT(mSelection.IsSet());
+
+ // When you touch this method, please make sure that the entry instance
+ // won't be deleted. If you know it'll be deleted, you should set it to
+ // `nullptr`.
+ OffsetEntry* entry = ElementAt(mSelection.StartIndex()).get();
+ OwningNonNull<Text> const textNodeAtStartEntry = entry->mTextNode;
+
+ NS_ASSERTION((entry->mIsValid), "Invalid insertion point!");
+
+ if (entry->mOffsetInTextInBlock == mSelection.StartOffsetInTextInBlock()) {
+ if (entry->mIsInsertedText) {
+ // If the caret is in an inserted text offset entry,
+ // we simply insert the text at the end of the entry.
+ entry->mLength += aInsertedString.Length();
+ } else {
+ // Insert an inserted text offset entry before the current
+ // entry!
+ UniquePtr<OffsetEntry> newInsertedTextEntry =
+ MakeUnique<OffsetEntry>(entry->mTextNode, entry->mOffsetInTextInBlock,
+ aInsertedString.Length());
+ newInsertedTextEntry->mIsInsertedText = true;
+ newInsertedTextEntry->mOffsetInTextNode = entry->mOffsetInTextNode;
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ InsertElementAt(mSelection.StartIndex(), std::move(newInsertedTextEntry));
+ }
+ } else if (entry->EndOffsetInTextInBlock() ==
+ mSelection.EndOffsetInTextInBlock()) {
+ // We are inserting text at the end of the current offset entry.
+ // Look at the next valid entry in the table. If it's an inserted
+ // text entry, add to its length and adjust its node offset. If
+ // it isn't, add a new inserted text entry.
+ uint32_t nextIndex = mSelection.StartIndex() + 1;
+ OffsetEntry* insertedTextEntry = nullptr;
+ if (Length() > nextIndex) {
+ insertedTextEntry = ElementAt(nextIndex).get();
+ if (!insertedTextEntry) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check if the entry is a match. If it isn't, set
+ // iEntry to zero.
+ if (!insertedTextEntry->mIsInsertedText ||
+ insertedTextEntry->mOffsetInTextInBlock !=
+ mSelection.StartOffsetInTextInBlock()) {
+ insertedTextEntry = nullptr;
+ }
+ }
+
+ if (!insertedTextEntry) {
+ // We didn't find an inserted text offset entry, so
+ // create one.
+ UniquePtr<OffsetEntry> newInsertedTextEntry = MakeUnique<OffsetEntry>(
+ entry->mTextNode, mSelection.StartOffsetInTextInBlock(), 0);
+ newInsertedTextEntry->mOffsetInTextNode = entry->EndOffsetInTextNode();
+ newInsertedTextEntry->mIsInsertedText = true;
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ insertedTextEntry =
+ InsertElementAt(nextIndex, std::move(newInsertedTextEntry))->get();
+ }
+
+ // We have a valid inserted text offset entry. Update its
+ // length, adjust the selection indexes, and make sure the
+ // caret is properly placed!
+
+ insertedTextEntry->mLength += aInsertedString.Length();
+
+ MOZ_DIAGNOSTIC_ASSERT(nextIndex < Length());
+ mSelection.SetIndex(nextIndex);
+
+ if (!aSelection) {
+ return NS_OK;
+ }
+
+ OwningNonNull<Text> textNode = insertedTextEntry->mTextNode;
+ nsresult rv = aSelection->CollapseInLimiter(
+ textNode, insertedTextEntry->EndOffsetInTextNode());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Selection::CollapseInLimiter() failed");
+ return rv;
+ }
+ } else if (entry->EndOffsetInTextInBlock() >
+ mSelection.StartOffsetInTextInBlock()) {
+ // We are inserting text into the middle of the current offset entry.
+ // split the current entry into two parts, then insert an inserted text
+ // entry between them!
+ nsresult rv = SplitElementAt(mSelection.StartIndex(),
+ entry->EndOffsetInTextInBlock() -
+ mSelection.StartOffsetInTextInBlock());
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "entry->EndOffsetInTextInBlock() - "
+ "mSelection.StartOffsetInTextInBlock() was invalid for the "
+ "OffsetEntry");
+ return rv;
+ }
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ UniquePtr<OffsetEntry>& insertedTextEntry = *InsertElementAt(
+ mSelection.StartIndex() + 1,
+ MakeUnique<OffsetEntry>(entry->mTextNode,
+ mSelection.StartOffsetInTextInBlock(),
+ aInsertedString.Length()));
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ insertedTextEntry->mIsInsertedText = true;
+ insertedTextEntry->mOffsetInTextNode = entry->EndOffsetInTextNode();
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() + 1 < Length());
+ mSelection.SetIndex(mSelection.StartIndex() + 1);
+ }
+
+ // We've just finished inserting an inserted text offset entry.
+ // update all entries with the same mTextNode pointer that follow
+ // it in the table!
+
+ for (size_t i = mSelection.StartIndex() + 1; i < Length(); i++) {
+ const UniquePtr<OffsetEntry>& entry = ElementAt(i);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (entry->mTextNode != textNodeAtStartEntry) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mOffsetInTextNode += aInsertedString.Length();
+ }
+ }
+
+ return NS_OK;
+}
+
+void TextServicesDocument::DidDeleteContent(const nsIContent& aChildContent) {
+ if (NS_WARN_IF(!mFilteredIter) || !aChildContent.IsText()) {
+ return;
+ }
+
+ Maybe<size_t> maybeNodeIndex =
+ mOffsetTable.FirstIndexOf(*aChildContent.AsText());
+ if (maybeNodeIndex.isNothing()) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return;
+ }
+
+ nsINode* node = mFilteredIter->GetCurrentNode();
+ if (node && node == &aChildContent &&
+ mIteratorStatus != IteratorStatus::eDone) {
+ // XXX: This should never really happen because
+ // AdjustContentIterator() should have been called prior
+ // to the delete to try and position the iterator on the
+ // next valid text node in the offset table, and if there
+ // wasn't a next, it would've set mIteratorStatus to eIsDone.
+
+ NS_ERROR("DeleteNode called for current iterator node.");
+ }
+
+ for (size_t nodeIndex = *maybeNodeIndex; nodeIndex < mOffsetTable.Length();
+ nodeIndex++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[nodeIndex];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (!entry) {
+ return;
+ }
+
+ if (entry->mTextNode == &aChildContent) {
+ entry->mIsValid = false;
+ }
+ }
+}
+
+void TextServicesDocument::DidJoinContents(
+ const EditorRawDOMPoint& aJoinedPoint, const nsIContent& aRemovedContent,
+ JoinNodesDirection aJoinNodesDirection) {
+ // Make sure that both nodes are text nodes -- otherwise we don't care.
+ if (!aJoinedPoint.IsInTextNode() || !aRemovedContent.IsText()) {
+ return;
+ }
+
+ // Note: The editor merges the contents of the left node into the
+ // contents of the right.
+
+ Maybe<size_t> maybeRemovedIndex =
+ mOffsetTable.FirstIndexOf(*aRemovedContent.AsText());
+ if (maybeRemovedIndex.isNothing()) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return;
+ }
+
+ Maybe<size_t> maybeJoinedIndex =
+ mOffsetTable.FirstIndexOf(*aJoinedPoint.ContainerAs<Text>());
+ if (maybeJoinedIndex.isNothing()) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return;
+ }
+
+ const size_t removedIndex = *maybeRemovedIndex;
+ const size_t joinedIndex = *maybeJoinedIndex;
+
+ if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
+ if (MOZ_UNLIKELY(removedIndex > joinedIndex)) {
+ NS_ASSERTION(removedIndex < joinedIndex, "Indexes out of order.");
+ return;
+ }
+ NS_ASSERTION(mOffsetTable[joinedIndex]->mOffsetInTextNode == 0,
+ "Unexpected offset value for joinedIndex.");
+ } else {
+ if (MOZ_UNLIKELY(joinedIndex > removedIndex)) {
+ NS_ASSERTION(joinedIndex < removedIndex, "Indexes out of order.");
+ return;
+ }
+ NS_ASSERTION(mOffsetTable[removedIndex]->mOffsetInTextNode == 0,
+ "Unexpected offset value for rightIndex.");
+ }
+
+ // Run through the table and change all entries referring to
+ // the removed node so that they now refer to the joined node,
+ // and adjust offsets if necessary.
+ const uint32_t movedTextDataLength =
+ aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode
+ ? aJoinedPoint.Offset()
+ : aJoinedPoint.ContainerAs<Text>()->TextDataLength() -
+ aJoinedPoint.Offset();
+ for (uint32_t i = removedIndex; i < mOffsetTable.Length(); i++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (entry->mTextNode != aRemovedContent.AsText()) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mTextNode = aJoinedPoint.ContainerAs<Text>();
+ if (aJoinNodesDirection == JoinNodesDirection::RightNodeIntoLeftNode) {
+ // The text was moved from aRemovedContent to end of the container of
+ // aJoinedPoint.
+ entry->mOffsetInTextNode += movedTextDataLength;
+ }
+ }
+ }
+
+ if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
+ // The text was moved from aRemovedContent to start of the container of
+ // aJoinedPoint.
+ for (uint32_t i = joinedIndex; i < mOffsetTable.Length(); i++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (entry->mTextNode != aJoinedPoint.ContainerAs<Text>()) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mOffsetInTextNode += movedTextDataLength;
+ }
+ }
+ }
+
+ // Now check to see if the iterator is pointing to the
+ // left node. If it is, make it point to the joined node!
+ if (mFilteredIter->GetCurrentNode() == aRemovedContent.AsText()) {
+ mFilteredIter->PositionAt(aJoinedPoint.ContainerAs<Text>());
+ }
+}
+
+nsresult TextServicesDocument::CreateFilteredContentIterator(
+ const AbstractRange* aAbstractRange,
+ FilteredContentIterator** aFilteredIter) {
+ if (NS_WARN_IF(!aAbstractRange) || NS_WARN_IF(!aFilteredIter)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aFilteredIter = nullptr;
+
+ UniquePtr<nsComposeTxtSrvFilter> composeFilter;
+ switch (mTxtSvcFilterType) {
+ case nsIEditorSpellCheck::FILTERTYPE_NORMAL:
+ composeFilter = nsComposeTxtSrvFilter::CreateNormalFilter();
+ break;
+ case nsIEditorSpellCheck::FILTERTYPE_MAIL:
+ composeFilter = nsComposeTxtSrvFilter::CreateMailFilter();
+ break;
+ }
+
+ // Create a FilteredContentIterator
+ // This class wraps the ContentIterator in order to give itself a chance
+ // to filter out certain content nodes
+ RefPtr<FilteredContentIterator> filter =
+ new FilteredContentIterator(std::move(composeFilter));
+ nsresult rv = filter->Init(aAbstractRange);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filter.forget(aFilteredIter);
+ return NS_OK;
+}
+
+Element* TextServicesDocument::GetDocumentContentRootNode() const {
+ if (NS_WARN_IF(!mDocument)) {
+ return nullptr;
+ }
+
+ if (mDocument->IsHTMLOrXHTML()) {
+ Element* rootElement = mDocument->GetRootElement();
+ if (rootElement && rootElement->IsXULElement()) {
+ // HTML documents with root XUL elements should eventually be transitioned
+ // to a regular document structure, but for now the content root node will
+ // be the document element.
+ return mDocument->GetDocumentElement();
+ }
+ // For HTML documents, the content root node is the body.
+ return mDocument->GetBody();
+ }
+
+ // For non-HTML documents, the content root node will be the document element.
+ return mDocument->GetDocumentElement();
+}
+
+already_AddRefed<nsRange> TextServicesDocument::CreateDocumentContentRange() {
+ nsCOMPtr<nsINode> node = GetDocumentContentRootNode();
+ if (NS_WARN_IF(!node)) {
+ return nullptr;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(node);
+ IgnoredErrorResult ignoredError;
+ range->SelectNodeContents(*node, ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(), "SelectNodeContents() failed");
+ return range.forget();
+}
+
+already_AddRefed<nsRange>
+TextServicesDocument::CreateDocumentContentRootToNodeOffsetRange(
+ nsINode* aParent, uint32_t aOffset, bool aToStart) {
+ if (NS_WARN_IF(!aParent)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsINode> bodyNode = GetDocumentContentRootNode();
+ if (NS_WARN_IF(!bodyNode)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsINode> startNode;
+ nsCOMPtr<nsINode> endNode;
+ uint32_t startOffset, endOffset;
+
+ if (aToStart) {
+ // The range should begin at the start of the document
+ // and extend up until (aParent, aOffset).
+ startNode = bodyNode;
+ startOffset = 0;
+ endNode = aParent;
+ endOffset = aOffset;
+ } else {
+ // The range should begin at (aParent, aOffset) and
+ // extend to the end of the document.
+ startNode = aParent;
+ startOffset = aOffset;
+ endNode = bodyNode;
+ endOffset = endNode ? endNode->GetChildCount() : 0;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(startNode, startOffset, endNode,
+ endOffset, IgnoreErrors());
+ NS_WARNING_ASSERTION(range,
+ "nsRange::Create() failed to create new valid range");
+ return range.forget();
+}
+
+nsresult TextServicesDocument::CreateDocumentContentIterator(
+ FilteredContentIterator** aFilteredIter) {
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ RefPtr<nsRange> range = CreateDocumentContentRange();
+ if (NS_WARN_IF(!range)) {
+ *aFilteredIter = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ return CreateFilteredContentIterator(range, aFilteredIter);
+}
+
+nsresult TextServicesDocument::AdjustContentIterator() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsINode> node = mFilteredIter->GetCurrentNode();
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ Text* prevValidTextNode = nullptr;
+ Text* nextValidTextNode = nullptr;
+ bool foundEntry = false;
+
+ const size_t tableLength = mOffsetTable.Length();
+ for (size_t i = 0; i < tableLength && !nextValidTextNode; i++) {
+ UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (entry->mTextNode == node) {
+ if (entry->mIsValid) {
+ // The iterator is still pointing to something valid!
+ // Do nothing!
+ return NS_OK;
+ }
+ // We found an invalid entry that points to
+ // the current iterator node. Stop looking for
+ // a previous valid node!
+ foundEntry = true;
+ }
+
+ if (entry->mIsValid) {
+ if (!foundEntry) {
+ prevValidTextNode = entry->mTextNode;
+ } else {
+ nextValidTextNode = entry->mTextNode;
+ }
+ }
+ }
+
+ Text* validTextNode = nullptr;
+ if (prevValidTextNode) {
+ validTextNode = prevValidTextNode;
+ } else if (nextValidTextNode) {
+ validTextNode = nextValidTextNode;
+ }
+
+ if (validTextNode) {
+ nsresult rv = mFilteredIter->PositionAt(validTextNode);
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ } else {
+ mIteratorStatus = IteratorStatus::eValid;
+ }
+ return rv;
+ }
+
+ // If we get here, there aren't any valid entries
+ // in the offset table! Try to position the iterator
+ // on the next text block first, then previous if
+ // one doesn't exist!
+
+ if (mNextTextBlock) {
+ nsresult rv = mFilteredIter->PositionAt(mNextTextBlock);
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eNext;
+ } else if (mPrevTextBlock) {
+ nsresult rv = mFilteredIter->PositionAt(mPrevTextBlock);
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::ePrev;
+ } else {
+ mIteratorStatus = IteratorStatus::eDone;
+ }
+ return NS_OK;
+}
+
+// static
+bool TextServicesDocument::DidSkip(FilteredContentIterator* aFilteredIter) {
+ return aFilteredIter && aFilteredIter->DidSkip();
+}
+
+// static
+void TextServicesDocument::ClearDidSkip(
+ FilteredContentIterator* aFilteredIter) {
+ // Clear filter's skip flag
+ if (aFilteredIter) {
+ aFilteredIter->ClearDidSkip();
+ }
+}
+
+// static
+bool TextServicesDocument::HasSameBlockNodeParent(Text& aTextNode1,
+ Text& aTextNode2) {
+ // XXX How about the case that both text nodes are orphan nodes?
+ if (aTextNode1.GetParent() == aTextNode2.GetParent()) {
+ return true;
+ }
+
+ // I think that spellcheck should be available only in editable nodes.
+ // So, we also need to check whether they are in same editing host.
+ const Element* editableBlockElementOrInlineEditingHost1 =
+ HTMLEditUtils::GetAncestorElement(
+ aTextNode1,
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost);
+ const Element* editableBlockElementOrInlineEditingHost2 =
+ HTMLEditUtils::GetAncestorElement(
+ aTextNode2,
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost);
+ return editableBlockElementOrInlineEditingHost1 &&
+ editableBlockElementOrInlineEditingHost1 ==
+ editableBlockElementOrInlineEditingHost2;
+}
+
+Result<EditorRawDOMRangeInTexts, nsresult>
+TextServicesDocument::OffsetEntryArray::WillSetSelection(
+ uint32_t aOffsetInTextInBlock, uint32_t aLength) {
+ // Find start of selection in node offset terms:
+ EditorRawDOMPointInText newStart;
+ for (size_t i = 0; !newStart.IsSet() && i < Length(); i++) {
+ const UniquePtr<OffsetEntry>& entry = ElementAt(i);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (entry->mIsValid) {
+ if (entry->mIsInsertedText) {
+ // Caret can only be placed at the end of an
+ // inserted text offset entry, if the offsets
+ // match exactly!
+ if (entry->mOffsetInTextInBlock == aOffsetInTextInBlock) {
+ newStart.Set(entry->mTextNode, entry->EndOffsetInTextNode());
+ }
+ } else if (aOffsetInTextInBlock >= entry->mOffsetInTextInBlock) {
+ bool foundEntry = false;
+ if (aOffsetInTextInBlock < entry->EndOffsetInTextInBlock()) {
+ foundEntry = true;
+ } else if (aOffsetInTextInBlock == entry->EndOffsetInTextInBlock()) {
+ // Peek after this entry to see if we have any
+ // inserted text entries belonging to the same
+ // entry->mTextNode. If so, we have to place the selection
+ // after it!
+ if (i + 1 < Length()) {
+ const UniquePtr<OffsetEntry>& nextEntry = ElementAt(i + 1);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (!nextEntry->mIsValid ||
+ nextEntry->mOffsetInTextInBlock != aOffsetInTextInBlock) {
+ // Next offset entry isn't an exact match, so we'll
+ // just use the current entry.
+ foundEntry = true;
+ }
+ }
+ }
+
+ if (foundEntry) {
+ newStart.Set(entry->mTextNode, entry->mOffsetInTextNode +
+ aOffsetInTextInBlock -
+ entry->mOffsetInTextInBlock);
+ }
+ }
+
+ if (newStart.IsSet()) {
+ MOZ_DIAGNOSTIC_ASSERT(i < Length());
+ mSelection.Set(i, aOffsetInTextInBlock);
+ }
+ }
+ }
+
+ if (NS_WARN_IF(!newStart.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (!aLength) {
+ mSelection.CollapseToStart();
+ return EditorRawDOMRangeInTexts(newStart);
+ }
+
+ // Find the end of the selection in node offset terms:
+ EditorRawDOMPointInText newEnd;
+ const uint32_t endOffset = aOffsetInTextInBlock + aLength;
+ for (uint32_t i = Length(); !newEnd.IsSet() && i > 0; i--) {
+ const UniquePtr<OffsetEntry>& entry = ElementAt(i - 1);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (entry->mIsValid) {
+ if (entry->mIsInsertedText) {
+ if (entry->mOffsetInTextInBlock ==
+ (newEnd.IsSet() ? newEnd.Offset() : 0)) {
+ // If the selection ends on an inserted text offset entry,
+ // the selection includes the entire entry!
+ newEnd.Set(entry->mTextNode, entry->EndOffsetInTextNode());
+ }
+ } else if (entry->OffsetInTextInBlockIsInRangeOrEndOffset(endOffset)) {
+ newEnd.Set(entry->mTextNode, entry->mOffsetInTextNode + endOffset -
+ entry->mOffsetInTextInBlock);
+ }
+
+ if (newEnd.IsSet()) {
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() < Length());
+ MOZ_DIAGNOSTIC_ASSERT(i - 1 < Length());
+ mSelection.Set(mSelection.StartIndex(), i - 1,
+ mSelection.StartOffsetInTextInBlock(), endOffset);
+ }
+ }
+ }
+
+ return newEnd.IsSet() ? EditorRawDOMRangeInTexts(newStart, newEnd)
+ : EditorRawDOMRangeInTexts(newStart);
+}
+
+nsresult TextServicesDocument::SetSelectionInternal(
+ uint32_t aOffsetInTextInBlock, uint32_t aLength, bool aDoUpdate) {
+ if (NS_WARN_IF(!mSelCon)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ Result<EditorRawDOMRangeInTexts, nsresult> newSelectionRange =
+ mOffsetTable.WillSetSelection(aOffsetInTextInBlock, aLength);
+ if (newSelectionRange.isErr()) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryArray::WillSetSelection() failed");
+ return newSelectionRange.unwrapErr();
+ }
+
+ if (!aDoUpdate) {
+ return NS_OK;
+ }
+
+ // XXX: If we ever get a SetSelection() method in nsIEditor, we should
+ // use it.
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (newSelectionRange.inspect().Collapsed()) {
+ nsresult rv =
+ selection->CollapseInLimiter(newSelectionRange.inspect().StartRef());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Selection::CollapseInLimiter() failed");
+ return rv;
+ }
+
+ ErrorResult error;
+ selection->SetStartAndEndInLimiter(newSelectionRange.inspect().StartRef(),
+ newSelectionRange.inspect().EndRef(),
+ error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "Selection::SetStartAndEndInLimiter() failed");
+ return error.StealNSResult();
+}
+
+nsresult TextServicesDocument::GetSelection(BlockSelectionStatus* aSelStatus,
+ uint32_t* aSelOffset,
+ uint32_t* aSelLength) {
+ NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER);
+
+ *aSelStatus = BlockSelectionStatus::eBlockNotFound;
+ *aSelOffset = UINT32_MAX;
+ *aSelLength = UINT32_MAX;
+
+ NS_ENSURE_TRUE(mDocument && mSelCon, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ if (selection->IsCollapsed()) {
+ return GetCollapsedSelection(aSelStatus, aSelOffset, aSelLength);
+ }
+
+ return GetUncollapsedSelection(aSelStatus, aSelOffset, aSelLength);
+}
+
+nsresult TextServicesDocument::GetCollapsedSelection(
+ BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset,
+ uint32_t* aSelLength) {
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ // The calling function should have done the GetIsCollapsed()
+ // check already. Just assume it's collapsed!
+ *aSelStatus = BlockSelectionStatus::eBlockOutside;
+ *aSelOffset = *aSelLength = UINT32_MAX;
+
+ const uint32_t tableCount = mOffsetTable.Length();
+ if (!tableCount) {
+ return NS_OK;
+ }
+
+ // Get pointers to the first and last offset entries
+ // in the table.
+
+ UniquePtr<OffsetEntry>& eStart = mOffsetTable[0];
+ UniquePtr<OffsetEntry>& eEnd =
+ tableCount > 1 ? mOffsetTable[tableCount - 1] : eStart;
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+
+ const uint32_t eStartOffset = eStart->mOffsetInTextNode;
+ const uint32_t eEndOffset = eEnd->EndOffsetInTextNode();
+
+ RefPtr<const nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_STATE(range);
+
+ nsCOMPtr<nsINode> parent = range->GetStartContainer();
+ MOZ_ASSERT(parent);
+
+ uint32_t offset = range->StartOffset();
+
+ const Maybe<int32_t> e1s1 = nsContentUtils::ComparePoints(
+ eStart->mTextNode, eStartOffset, parent, offset);
+ const Maybe<int32_t> e2s1 = nsContentUtils::ComparePoints(
+ eEnd->mTextNode, eEndOffset, parent, offset);
+
+ if (MOZ_UNLIKELY(NS_WARN_IF(!e1s1) || NS_WARN_IF(!e2s1))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (*e1s1 > 0 || *e2s1 < 0) {
+ // We're done if the caret is outside the current text block.
+ return NS_OK;
+ }
+
+ if (parent->IsText()) {
+ // Good news, the caret is in a text node. Look
+ // through the offset table for the entry that
+ // matches its parent and offset.
+
+ for (uint32_t i = 0; i < tableCount; i++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (entry->mTextNode == parent->AsText() &&
+ entry->OffsetInTextNodeIsInRangeOrEndOffset(offset)) {
+ *aSelStatus = BlockSelectionStatus::eBlockContains;
+ *aSelOffset =
+ entry->mOffsetInTextInBlock + (offset - entry->mOffsetInTextNode);
+ *aSelLength = 0;
+ return NS_OK;
+ }
+ }
+
+ // If we get here, we didn't find a text node entry
+ // in our offset table that matched.
+ return NS_ERROR_FAILURE;
+ }
+
+ // The caret is in our text block, but it's positioned in some
+ // non-text node (ex. <b>). Create a range based on the start
+ // and end of the text block, then create an iterator based on
+ // this range, with its initial position set to the closest
+ // child of this non-text node. Then look for the closest text
+ // node.
+
+ range = nsRange::Create(eStart->mTextNode, eStartOffset, eEnd->mTextNode,
+ eEndOffset, IgnoreErrors());
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIContent* saveNode;
+ if (parent->HasChildren()) {
+ // XXX: We need to make sure that all of parent's
+ // children are in the text block.
+
+ // If the parent has children, position the iterator
+ // on the child that is to the left of the offset.
+
+ nsIContent* content = range->GetChildAtStartOffset();
+ if (content && parent->GetFirstChild() != content) {
+ content = content->GetPreviousSibling();
+ }
+ NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
+
+ nsresult rv = filteredIter->PositionAt(content);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ saveNode = content;
+ } else {
+ // The parent has no children, so position the iterator
+ // on the parent.
+ NS_ENSURE_TRUE(parent->IsContent(), NS_ERROR_FAILURE);
+ nsCOMPtr<nsIContent> content = parent->AsContent();
+
+ nsresult rv = filteredIter->PositionAt(content);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ saveNode = content;
+ }
+
+ // Now iterate to the left, towards the beginning of
+ // the text block, to find the first text node you
+ // come across.
+
+ Text* textNode = nullptr;
+ for (; !filteredIter->IsDone(); filteredIter->Prev()) {
+ nsINode* current = filteredIter->GetCurrentNode();
+ if (current->IsText()) {
+ textNode = current->AsText();
+ break;
+ }
+ }
+
+ if (textNode) {
+ // We found a node, now set the offset to the end
+ // of the text node.
+ offset = textNode->TextLength();
+ } else {
+ // We should never really get here, but I'm paranoid.
+
+ // We didn't find a text node above, so iterate to
+ // the right, towards the end of the text block, looking
+ // for a text node.
+
+ nsresult rv = filteredIter->PositionAt(saveNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ textNode = nullptr;
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ nsINode* current = filteredIter->GetCurrentNode();
+ if (current->IsText()) {
+ textNode = current->AsText();
+ break;
+ }
+ }
+ NS_ENSURE_TRUE(textNode, NS_ERROR_FAILURE);
+
+ // We found a text node, so set the offset to
+ // the beginning of the node.
+ offset = 0;
+ }
+
+ for (size_t i = 0; i < tableCount; i++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (entry->mTextNode == textNode &&
+ entry->OffsetInTextNodeIsInRangeOrEndOffset(offset)) {
+ *aSelStatus = BlockSelectionStatus::eBlockContains;
+ *aSelOffset =
+ entry->mOffsetInTextInBlock + (offset - entry->mOffsetInTextNode);
+ *aSelLength = 0;
+
+ // Now move the caret so that it is actually in the text node.
+ // We do this to keep things in sync.
+ //
+ // In most cases, the user shouldn't see any movement in the caret
+ // on screen.
+ return SetSelectionInternal(*aSelOffset, *aSelLength, true);
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult TextServicesDocument::GetUncollapsedSelection(
+ BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset,
+ uint32_t* aSelLength) {
+ RefPtr<const nsRange> range;
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ // It is assumed that the calling function has made sure that the
+ // selection is not collapsed, and that the input params to this
+ // method are initialized to some defaults.
+
+ nsCOMPtr<nsINode> startContainer, endContainer;
+
+ const size_t tableCount = mOffsetTable.Length();
+
+ // Get pointers to the first and last offset entries
+ // in the table.
+
+ UniquePtr<OffsetEntry>& eStart = mOffsetTable[0];
+ UniquePtr<OffsetEntry>& eEnd =
+ tableCount > 1 ? mOffsetTable[tableCount - 1] : eStart;
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+
+ const uint32_t eStartOffset = eStart->mOffsetInTextNode;
+ const uint32_t eEndOffset = eEnd->EndOffsetInTextNode();
+
+ const uint32_t rangeCount = selection->RangeCount();
+ MOZ_ASSERT(rangeCount);
+
+ // Find the first range in the selection that intersects
+ // the current text block.
+ Maybe<int32_t> e1s2;
+ Maybe<int32_t> e2s1;
+ uint32_t startOffset, endOffset;
+ for (const uint32_t i : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(selection->RangeCount() == rangeCount);
+ range = selection->GetRangeAt(i);
+ if (MOZ_UNLIKELY(NS_WARN_IF(!range))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv =
+ GetRangeEndPoints(range, getter_AddRefs(startContainer), &startOffset,
+ getter_AddRefs(endContainer), &endOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ e1s2 = nsContentUtils::ComparePoints(eStart->mTextNode, eStartOffset,
+ endContainer, endOffset);
+ if (NS_WARN_IF(!e1s2)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ e2s1 = nsContentUtils::ComparePoints(eEnd->mTextNode, eEndOffset,
+ startContainer, startOffset);
+ if (NS_WARN_IF(!e2s1)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Break out of the loop if the text block intersects the current range.
+
+ if (*e1s2 <= 0 && *e2s1 >= 0) {
+ break;
+ }
+ }
+
+ // We're done if we didn't find an intersecting range.
+
+ if (rangeCount < 1 || *e1s2 > 0 || *e2s1 < 0) {
+ *aSelStatus = BlockSelectionStatus::eBlockOutside;
+ *aSelOffset = *aSelLength = UINT32_MAX;
+ return NS_OK;
+ }
+
+ // Now that we have an intersecting range, find out more info:
+ const Maybe<int32_t> e1s1 = nsContentUtils::ComparePoints(
+ eStart->mTextNode, eStartOffset, startContainer, startOffset);
+ if (NS_WARN_IF(!e1s1)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const Maybe<int32_t> e2s2 = nsContentUtils::ComparePoints(
+ eEnd->mTextNode, eEndOffset, endContainer, endOffset);
+ if (NS_WARN_IF(!e2s2)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (rangeCount > 1) {
+ // There are multiple selection ranges, we only deal
+ // with the first one that intersects the current,
+ // text block, so mark this a as a partial.
+ *aSelStatus = BlockSelectionStatus::eBlockPartial;
+ } else if (*e1s1 > 0 && *e2s2 < 0) {
+ // The range extends beyond the start and
+ // end of the current text block.
+ *aSelStatus = BlockSelectionStatus::eBlockInside;
+ } else if (*e1s1 <= 0 && *e2s2 >= 0) {
+ // The current text block contains the entire
+ // range.
+ *aSelStatus = BlockSelectionStatus::eBlockContains;
+ } else {
+ // The range partially intersects the block.
+ *aSelStatus = BlockSelectionStatus::eBlockPartial;
+ }
+
+ // Now create a range based on the intersection of the
+ // text block and range:
+
+ nsCOMPtr<nsINode> p1, p2;
+ uint32_t o1, o2;
+
+ // The start of the range will be the rightmost
+ // start node.
+
+ if (*e1s1 >= 0) {
+ p1 = eStart->mTextNode;
+ o1 = eStartOffset;
+ } else {
+ p1 = startContainer;
+ o1 = startOffset;
+ }
+
+ // The end of the range will be the leftmost
+ // end node.
+
+ if (*e2s2 <= 0) {
+ p2 = eEnd->mTextNode;
+ o2 = eEndOffset;
+ } else {
+ p2 = endContainer;
+ o2 = endOffset;
+ }
+
+ range = nsRange::Create(p1, o1, p2, o2, IgnoreErrors());
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now iterate over this range to figure out the selection's
+ // block offset and length.
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Find the first text node in the range.
+ nsCOMPtr<nsIContent> content;
+ filteredIter->First();
+ if (!p1->IsText()) {
+ bool found = false;
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ nsINode* node = filteredIter->GetCurrentNode();
+ if (node->IsText()) {
+ p1 = node->AsText();
+ o1 = 0;
+ found = true;
+ break;
+ }
+ }
+ NS_ENSURE_TRUE(found, NS_ERROR_FAILURE);
+ }
+
+ // Find the last text node in the range.
+ filteredIter->Last();
+ if (!p2->IsText()) {
+ bool found = false;
+ for (; !filteredIter->IsDone(); filteredIter->Prev()) {
+ nsINode* node = filteredIter->GetCurrentNode();
+ if (node->IsText()) {
+ p2 = node->AsText();
+ o2 = p2->AsText()->Length();
+ found = true;
+
+ break;
+ }
+ }
+ NS_ENSURE_TRUE(found, NS_ERROR_FAILURE);
+ }
+
+ bool found = false;
+ *aSelLength = 0;
+
+ for (size_t i = 0; i < tableCount; i++) {
+ const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
+ LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
+ if (!found) {
+ if (entry->mTextNode == p1.get() &&
+ entry->OffsetInTextNodeIsInRangeOrEndOffset(o1)) {
+ *aSelOffset =
+ entry->mOffsetInTextInBlock + (o1 - entry->mOffsetInTextNode);
+ if (p1 == p2 && entry->OffsetInTextNodeIsInRangeOrEndOffset(o2)) {
+ // The start and end of the range are in the same offset
+ // entry. Calculate the length of the range then we're done.
+ *aSelLength = o2 - o1;
+ break;
+ }
+ // Add the length of the sub string in this offset entry
+ // that follows the start of the range.
+ *aSelLength = entry->EndOffsetInTextNode() - o1;
+ found = true;
+ }
+ } else { // Found.
+ if (entry->mTextNode == p2.get() &&
+ entry->OffsetInTextNodeIsInRangeOrEndOffset(o2)) {
+ // We found the end of the range. Calculate the length of the
+ // sub string that is before the end of the range, then we're done.
+ *aSelLength += o2 - entry->mOffsetInTextNode;
+ break;
+ }
+ // The entire entry must be in the range.
+ *aSelLength += entry->mLength;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::GetRangeEndPoints(
+ const AbstractRange* aAbstractRange, nsINode** aStartContainer,
+ uint32_t* aStartOffset, nsINode** aEndContainer, uint32_t* aEndOffset) {
+ if (NS_WARN_IF(!aAbstractRange) || NS_WARN_IF(!aStartContainer) ||
+ NS_WARN_IF(!aEndContainer) || NS_WARN_IF(!aEndOffset)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsINode> startContainer = aAbstractRange->GetStartContainer();
+ if (NS_WARN_IF(!startContainer)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsINode> endContainer = aAbstractRange->GetEndContainer();
+ if (NS_WARN_IF(!endContainer)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ startContainer.forget(aStartContainer);
+ endContainer.forget(aEndContainer);
+ *aStartOffset = aAbstractRange->StartOffset();
+ *aEndOffset = aAbstractRange->EndOffset();
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNode(
+ FilteredContentIterator* aFilteredIter, IteratorStatus* aIteratorStatus) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eDone;
+ }
+
+ for (aFilteredIter->First(); !aFilteredIter->IsDone();
+ aFilteredIter->Next()) {
+ if (aFilteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eValid;
+ }
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::LastTextNode(
+ FilteredContentIterator* aFilteredIter, IteratorStatus* aIteratorStatus) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eDone;
+ }
+
+ for (aFilteredIter->Last(); !aFilteredIter->IsDone(); aFilteredIter->Prev()) {
+ if (aFilteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eValid;
+ }
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNodeInCurrentBlock(
+ FilteredContentIterator* aFilteredIter) {
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ ClearDidSkip(aFilteredIter);
+
+ // Walk backwards over adjacent text nodes until
+ // we hit a block boundary:
+ RefPtr<Text> lastTextNode;
+ while (!aFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> content =
+ aFilteredIter->GetCurrentNode()->IsContent()
+ ? aFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ if (lastTextNode && content &&
+ (HTMLEditUtils::IsBlockElement(*content) ||
+ content->IsHTMLElement(nsGkAtoms::br))) {
+ break;
+ }
+ if (content && content->IsText()) {
+ if (lastTextNode && !TextServicesDocument::HasSameBlockNodeParent(
+ *content->AsText(), *lastTextNode)) {
+ // We're done, the current text node is in a
+ // different block.
+ break;
+ }
+ lastTextNode = content->AsText();
+ }
+
+ aFilteredIter->Prev();
+
+ if (DidSkip(aFilteredIter)) {
+ break;
+ }
+ }
+
+ if (lastTextNode) {
+ aFilteredIter->PositionAt(lastTextNode);
+ }
+
+ // XXX: What should we return if last is null?
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNodeInPrevBlock(
+ FilteredContentIterator* aFilteredIter) {
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ // XXX: What if mFilteredIter is not currently on a text node?
+
+ // Make sure mFilteredIter is pointing to the first text node in the
+ // current block:
+
+ nsresult rv = FirstTextNodeInCurrentBlock(aFilteredIter);
+
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+
+ // Point mFilteredIter to the first node before the first text node:
+
+ aFilteredIter->Prev();
+
+ if (aFilteredIter->IsDone()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now find the first text node of the next block:
+
+ return FirstTextNodeInCurrentBlock(aFilteredIter);
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNodeInNextBlock(
+ FilteredContentIterator* aFilteredIter) {
+ bool crossedBlockBoundary = false;
+
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ ClearDidSkip(aFilteredIter);
+
+ RefPtr<Text> previousTextNode;
+ while (!aFilteredIter->IsDone()) {
+ if (nsCOMPtr<nsIContent> content =
+ aFilteredIter->GetCurrentNode()->IsContent()
+ ? aFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr) {
+ if (content->IsText()) {
+ if (crossedBlockBoundary ||
+ (previousTextNode && !TextServicesDocument::HasSameBlockNodeParent(
+ *previousTextNode, *content->AsText()))) {
+ break;
+ }
+ previousTextNode = content->AsText();
+ } else if (!crossedBlockBoundary &&
+ (HTMLEditUtils::IsBlockElement(*content) ||
+ content->IsHTMLElement(nsGkAtoms::br))) {
+ crossedBlockBoundary = true;
+ }
+ }
+
+ aFilteredIter->Next();
+
+ if (!crossedBlockBoundary && DidSkip(aFilteredIter)) {
+ crossedBlockBoundary = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::GetFirstTextNodeInPrevBlock(
+ nsIContent** aContent) {
+ NS_ENSURE_TRUE(aContent, NS_ERROR_NULL_POINTER);
+
+ *aContent = 0;
+
+ // Save the iterator's current content node so we can restore
+ // it when we are done:
+
+ nsINode* node = mFilteredIter->GetCurrentNode();
+
+ nsresult rv = FirstTextNodeInPrevBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ // Try to restore the iterator before returning.
+ mFilteredIter->PositionAt(node);
+ return rv;
+ }
+
+ if (!mFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> current =
+ mFilteredIter->GetCurrentNode()->IsContent()
+ ? mFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ current.forget(aContent);
+ }
+
+ // Restore the iterator:
+
+ return mFilteredIter->PositionAt(node);
+}
+
+nsresult TextServicesDocument::GetFirstTextNodeInNextBlock(
+ nsIContent** aContent) {
+ NS_ENSURE_TRUE(aContent, NS_ERROR_NULL_POINTER);
+
+ *aContent = 0;
+
+ // Save the iterator's current content node so we can restore
+ // it when we are done:
+
+ nsINode* node = mFilteredIter->GetCurrentNode();
+
+ nsresult rv = FirstTextNodeInNextBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ // Try to restore the iterator before returning.
+ mFilteredIter->PositionAt(node);
+ return rv;
+ }
+
+ if (!mFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> current =
+ mFilteredIter->GetCurrentNode()->IsContent()
+ ? mFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ current.forget(aContent);
+ }
+
+ // Restore the iterator:
+ return mFilteredIter->PositionAt(node);
+}
+
+Result<TextServicesDocument::IteratorStatus, nsresult>
+TextServicesDocument::OffsetEntryArray::Init(
+ FilteredContentIterator& aFilteredIter, IteratorStatus aIteratorStatus,
+ nsRange* aIterRange, nsAString* aAllTextInBlock /* = nullptr */) {
+ Clear();
+
+ if (aAllTextInBlock) {
+ aAllTextInBlock->Truncate();
+ }
+
+ if (aIteratorStatus == IteratorStatus::eDone) {
+ return IteratorStatus::eDone;
+ }
+
+ // If we have an aIterRange, retrieve the endpoints so
+ // they can be used in the while loop below to trim entries
+ // for text nodes that are partially selected by aIterRange.
+
+ nsCOMPtr<nsINode> rngStartNode, rngEndNode;
+ uint32_t rngStartOffset = 0, rngEndOffset = 0;
+ if (aIterRange) {
+ nsresult rv = TextServicesDocument::GetRangeEndPoints(
+ aIterRange, getter_AddRefs(rngStartNode), &rngStartOffset,
+ getter_AddRefs(rngEndNode), &rngEndOffset);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextServicesDocument::GetRangeEndPoints() failed");
+ return Err(rv);
+ }
+ }
+
+ // The text service could have added text nodes to the beginning
+ // of the current block and called this method again. Make sure
+ // we really are at the beginning of the current block:
+
+ nsresult rv =
+ TextServicesDocument::FirstTextNodeInCurrentBlock(&aFilteredIter);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextServicesDocument::FirstTextNodeInCurrentBlock() failed");
+ return Err(rv);
+ }
+
+ TextServicesDocument::ClearDidSkip(&aFilteredIter);
+
+ uint32_t offset = 0;
+ RefPtr<Text> firstTextNode, previousTextNode;
+ while (!aFilteredIter.IsDone()) {
+ if (nsCOMPtr<nsIContent> content =
+ aFilteredIter.GetCurrentNode()->IsContent()
+ ? aFilteredIter.GetCurrentNode()->AsContent()
+ : nullptr) {
+ if (HTMLEditUtils::IsBlockElement(*content) ||
+ content->IsHTMLElement(nsGkAtoms::br)) {
+ break;
+ }
+ if (content->IsText()) {
+ if (previousTextNode && !TextServicesDocument::HasSameBlockNodeParent(
+ *previousTextNode, *content->AsText())) {
+ break;
+ }
+
+ nsString str;
+ content->AsText()->GetNodeValue(str);
+
+ // Add an entry for this text node into the offset table:
+
+ UniquePtr<OffsetEntry>& entry = *AppendElement(
+ MakeUnique<OffsetEntry>(*content->AsText(), offset, str.Length()));
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+
+ // If one or both of the endpoints of the iteration range
+ // are in the text node for this entry, make sure the entry
+ // only accounts for the portion of the text node that is
+ // in the range.
+
+ uint32_t startOffset = 0;
+ uint32_t endOffset = str.Length();
+ bool adjustStr = false;
+
+ if (entry->mTextNode == rngStartNode) {
+ entry->mOffsetInTextNode = startOffset = rngStartOffset;
+ adjustStr = true;
+ }
+
+ if (entry->mTextNode == rngEndNode) {
+ endOffset = rngEndOffset;
+ adjustStr = true;
+ }
+
+ if (adjustStr) {
+ entry->mLength = endOffset - startOffset;
+ str = Substring(str, startOffset, entry->mLength);
+ }
+
+ offset += str.Length();
+
+ if (aAllTextInBlock) {
+ // Append the text node's string to the output string:
+ if (!firstTextNode) {
+ *aAllTextInBlock = str;
+ } else {
+ *aAllTextInBlock += str;
+ }
+ }
+
+ previousTextNode = content->AsText();
+
+ if (!firstTextNode) {
+ firstTextNode = content->AsText();
+ }
+ }
+ }
+
+ aFilteredIter.Next();
+
+ if (TextServicesDocument::DidSkip(&aFilteredIter)) {
+ break;
+ }
+ }
+
+ if (firstTextNode) {
+ // Always leave the iterator pointing at the first
+ // text node of the current block!
+ aFilteredIter.PositionAt(firstTextNode);
+ return aIteratorStatus;
+ }
+
+ // If we never ran across a text node, the iterator
+ // might have been pointing to something invalid to
+ // begin with.
+ return IteratorStatus::eDone;
+}
+
+void TextServicesDocument::OffsetEntryArray::RemoveInvalidElements() {
+ for (size_t i = 0; i < Length();) {
+ if (ElementAt(i)->mIsValid) {
+ i++;
+ continue;
+ }
+
+ RemoveElementAt(i);
+ if (!mSelection.IsSet()) {
+ continue;
+ }
+ if (mSelection.StartIndex() == i) {
+ NS_ASSERTION(false, "What should we do in this case?");
+ mSelection.Reset();
+ } else if (mSelection.StartIndex() > i) {
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() - 1 < Length());
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.EndIndex() - 1 < Length());
+ mSelection.SetIndexes(mSelection.StartIndex() - 1,
+ mSelection.EndIndex() - 1);
+ } else if (mSelection.EndIndex() >= i) {
+ MOZ_DIAGNOSTIC_ASSERT(mSelection.EndIndex() - 1 < Length());
+ mSelection.SetIndexes(mSelection.StartIndex(), mSelection.EndIndex() - 1);
+ }
+ }
+}
+
+nsresult TextServicesDocument::OffsetEntryArray::SplitElementAt(
+ size_t aIndex, uint32_t aOffsetInTextNode) {
+ OffsetEntry* leftEntry = ElementAt(aIndex).get();
+ MOZ_ASSERT(leftEntry);
+ NS_ASSERTION((aOffsetInTextNode > 0), "aOffsetInTextNode == 0");
+ NS_ASSERTION((aOffsetInTextNode < leftEntry->mLength),
+ "aOffsetInTextNode >= mLength");
+
+ if (aOffsetInTextNode < 1 || aOffsetInTextNode >= leftEntry->mLength) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const uint32_t oldLength = leftEntry->mLength - aOffsetInTextNode;
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ UniquePtr<OffsetEntry>& rightEntry = *InsertElementAt(
+ aIndex + 1,
+ MakeUnique<OffsetEntry>(leftEntry->mTextNode,
+ leftEntry->mOffsetInTextInBlock + oldLength,
+ aOffsetInTextNode));
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ leftEntry->mLength = oldLength;
+ rightEntry->mOffsetInTextNode = leftEntry->mOffsetInTextNode + oldLength;
+
+ return NS_OK;
+}
+
+Maybe<size_t> TextServicesDocument::OffsetEntryArray::FirstIndexOf(
+ const Text& aTextNode) const {
+ for (size_t i = 0; i < Length(); i++) {
+ if (ElementAt(i)->mTextNode == &aTextNode) {
+ return Some(i);
+ }
+ }
+ return Nothing();
+}
+
+// Spellchecker code has this. See bug 211343
+#define IS_NBSP_CHAR(c) (((unsigned char)0xa0) == (c))
+
+Result<EditorDOMRangeInTexts, nsresult>
+TextServicesDocument::OffsetEntryArray::FindWordRange(
+ nsAString& aAllTextInBlock, const EditorRawDOMPoint& aStartPointToScan) {
+ MOZ_ASSERT(aStartPointToScan.IsInTextNode());
+ // It's assumed that aNode is a text node. The first thing
+ // we do is get its index in the offset table so we can
+ // calculate the dom point's string offset.
+ Maybe<size_t> maybeEntryIndex =
+ FirstIndexOf(*aStartPointToScan.ContainerAs<Text>());
+ if (NS_WARN_IF(maybeEntryIndex.isNothing())) {
+ NS_WARNING(
+ "TextServicesDocument::OffsetEntryArray::FirstIndexOf() didn't find "
+ "entries");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Next we map offset into a string offset.
+
+ const UniquePtr<OffsetEntry>& entry = ElementAt(*maybeEntryIndex);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ uint32_t strOffset = entry->mOffsetInTextInBlock +
+ aStartPointToScan.Offset() - entry->mOffsetInTextNode;
+
+ // Now we use the word breaker to find the beginning and end
+ // of the word from our calculated string offset.
+
+ const char16_t* str = aAllTextInBlock.BeginReading();
+ uint32_t strLen = aAllTextInBlock.Length();
+ MOZ_ASSERT(strOffset <= strLen,
+ "The string offset shouldn't be greater than the string length!");
+
+ intl::WordRange res = intl::WordBreaker::FindWord(str, strLen, strOffset);
+
+ // Strip out the NBSPs at the ends
+ while (res.mBegin <= res.mEnd && IS_NBSP_CHAR(str[res.mBegin])) {
+ res.mBegin++;
+ }
+ if (str[res.mEnd] == static_cast<char16_t>(0x20)) {
+ uint32_t realEndWord = res.mEnd - 1;
+ while (realEndWord > res.mBegin && IS_NBSP_CHAR(str[realEndWord])) {
+ realEndWord--;
+ }
+ if (realEndWord < res.mEnd - 1) {
+ res.mEnd = realEndWord + 1;
+ }
+ }
+
+ // Now that we have the string offsets for the beginning
+ // and end of the word, run through the offset table and
+ // convert them back into dom points.
+
+ EditorDOMPointInText wordStart, wordEnd;
+ size_t lastIndex = Length() - 1;
+ for (size_t i = 0; i <= lastIndex; i++) {
+ // Check to see if res.mBegin is within the range covered
+ // by this entry. Note that if res.mBegin is after the last
+ // character covered by this entry, we will use the next
+ // entry if there is one.
+ const UniquePtr<OffsetEntry>& entry = ElementAt(i);
+ LockOffsetEntryArrayLengthInDebugBuild(observer, *this);
+ if (entry->mOffsetInTextInBlock <= res.mBegin &&
+ (res.mBegin < entry->EndOffsetInTextInBlock() ||
+ (res.mBegin == entry->EndOffsetInTextInBlock() && i == lastIndex))) {
+ wordStart.Set(entry->mTextNode, entry->mOffsetInTextNode + res.mBegin -
+ entry->mOffsetInTextInBlock);
+ }
+
+ // Check to see if res.mEnd is within the range covered
+ // by this entry.
+ if (entry->mOffsetInTextInBlock <= res.mEnd &&
+ res.mEnd <= entry->EndOffsetInTextInBlock()) {
+ if (res.mBegin == res.mEnd &&
+ res.mEnd == entry->EndOffsetInTextInBlock() && i != lastIndex) {
+ // Wait for the next round so that we use the same entry
+ // we did for aWordStartNode.
+ continue;
+ }
+
+ wordEnd.Set(entry->mTextNode, entry->mOffsetInTextNode + res.mEnd -
+ entry->mOffsetInTextInBlock);
+ break;
+ }
+ }
+
+ return EditorDOMRangeInTexts(wordStart, wordEnd);
+}
+
+/**
+ * nsIEditActionListener implementation:
+ * Don't implement the behavior directly here. The methods won't be called
+ * if the instance is created for inline spell checker created for editor.
+ * If you need to listen a new edit action, you need to add similar
+ * non-virtual method and you need to call it from EditorBase directly.
+ */
+
+NS_IMETHODIMP
+TextServicesDocument::DidDeleteNode(nsINode* aChild, nsresult aResult) {
+ if (NS_WARN_IF(NS_FAILED(aResult)) || NS_WARN_IF(!aChild) ||
+ !aChild->IsContent()) {
+ return NS_OK;
+ }
+ DidDeleteContent(*aChild->AsContent());
+ return NS_OK;
+}
+
+NS_IMETHODIMP TextServicesDocument::DidJoinContents(
+ const EditorRawDOMPoint& aJoinedPoint, const nsINode* aRemovedNode,
+ bool aLeftNodeWasRemoved) {
+ if (MOZ_UNLIKELY(NS_WARN_IF(!aJoinedPoint.IsSetAndValid()) ||
+ NS_WARN_IF(!aRemovedNode->IsContent()))) {
+ return NS_OK;
+ }
+ DidJoinContents(aJoinedPoint, *aRemovedNode->AsContent(),
+ aLeftNodeWasRemoved
+ ? JoinNodesDirection::LeftNodeIntoRightNode
+ : JoinNodesDirection::RightNodeIntoLeftNode);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::DidInsertText(CharacterData* aTextNode, int32_t aOffset,
+ const nsAString& aString,
+ nsresult aResult) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::WillDeleteText(CharacterData* aTextNode, int32_t aOffset,
+ int32_t aLength) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::WillDeleteRanges(
+ const nsTArray<RefPtr<nsRange>>& aRangesToDelete) {
+ return NS_OK;
+}
+
+#undef LockOffsetEntryArrayLengthInDebugBuild
+
+} // namespace mozilla
diff --git a/editor/spellchecker/TextServicesDocument.h b/editor/spellchecker/TextServicesDocument.h
new file mode 100644
index 0000000000..7bc6aad7d5
--- /dev/null
+++ b/editor/spellchecker/TextServicesDocument.h
@@ -0,0 +1,438 @@
+/* -*- 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_TextServicesDocument_h
+#define mozilla_TextServicesDocument_h
+
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIEditActionListener.h"
+#include "nsISupportsImpl.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+class nsIContent;
+class nsIEditor;
+class nsINode;
+class nsISelectionController;
+class nsRange;
+
+namespace mozilla {
+
+class EditorBase;
+class FilteredContentIterator;
+class OffsetEntry;
+enum class JoinNodesDirection; // Declared in HTMLEditHelpers.h
+
+namespace dom {
+class AbstractRange;
+class Document;
+class Element;
+class StaticRange;
+}; // namespace dom
+
+/**
+ * The TextServicesDocument presents the document in as a bunch of flattened
+ * text blocks. Each text block can be retrieved as an nsString.
+ */
+class TextServicesDocument final : public nsIEditActionListener {
+ private:
+ enum class IteratorStatus : uint8_t {
+ // No iterator (I), or iterator doesn't point to anything valid.
+ eDone = 0,
+ // I points to first text node (TN) in current block (CB).
+ eValid,
+ // No TN in CB, I points to first TN in prev block.
+ ePrev,
+ // No TN in CB, I points to first TN in next block.
+ eNext,
+ };
+
+ class OffsetEntryArray final : public nsTArray<UniquePtr<OffsetEntry>> {
+ public:
+ /**
+ * Init() initializes this array with aFilteredIter.
+ *
+ * @param[in] aIterRange Can be nullptr.
+ * @param[out] aAllTextInBlock
+ * Returns all text in the block.
+ */
+ Result<IteratorStatus, nsresult> Init(
+ FilteredContentIterator& aFilteredIter, IteratorStatus aIteratorStatus,
+ nsRange* aIterRange, nsAString* aAllTextInBlock = nullptr);
+
+ /**
+ * Returns index of first `OffsetEntry` which manages aTextNode.
+ */
+ Maybe<size_t> FirstIndexOf(const dom::Text& aTextNode) const;
+
+ /**
+ * FindWordRange() returns a word range starting from aStartPointToScan
+ * in aAllTextInBlock.
+ */
+ Result<EditorDOMRangeInTexts, nsresult> FindWordRange(
+ nsAString& aAllTextInBlock, const EditorRawDOMPoint& aStartPointToScan);
+
+ /**
+ * SplitElementAt() splits an `OffsetEntry` at aIndex if aOffsetInTextNode
+ * is middle of the range in the text node.
+ *
+ * @param aIndex Index of the entry which you want to split.
+ * @param aOffsetInTextNode
+ * Offset in the text node. I.e., the offset should be
+ * greater than 0 and less than `mLength`.
+ */
+ nsresult SplitElementAt(size_t aIndex, uint32_t aOffsetInTextNode);
+
+ /**
+ * Remove all `OffsetEntry` elements whose `mIsValid` is set to false.
+ */
+ void RemoveInvalidElements();
+
+ /**
+ * Called when non-collapsed selection will be deleted.
+ */
+ nsresult WillDeleteSelection();
+
+ /**
+ * Called when non-collapsed selection is deleteded.
+ */
+ OffsetEntry* DidDeleteSelection();
+
+ /**
+ * Called when aInsertedText is inserted.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult DidInsertText(dom::Selection* aSelection,
+ const nsAString& aInsertedString);
+
+ /**
+ * Called when selection range will be applied to the DOM Selection.
+ */
+ Result<EditorRawDOMRangeInTexts, nsresult> WillSetSelection(
+ uint32_t aOffsetInTextInBlock, uint32_t aLength);
+
+ class Selection final {
+ public:
+ size_t StartIndex() const {
+ MOZ_ASSERT(IsIndexesSet());
+ return *mStartIndex;
+ }
+ size_t EndIndex() const {
+ MOZ_ASSERT(IsIndexesSet());
+ return *mEndIndex;
+ }
+
+ uint32_t StartOffsetInTextInBlock() const {
+ MOZ_ASSERT(IsSet());
+ return *mStartOffsetInTextInBlock;
+ }
+ uint32_t EndOffsetInTextInBlock() const {
+ MOZ_ASSERT(IsSet());
+ return *mEndOffsetInTextInBlock;
+ }
+ uint32_t LengthInTextInBlock() const {
+ MOZ_ASSERT(IsSet());
+ return *mEndOffsetInTextInBlock - *mStartOffsetInTextInBlock;
+ }
+
+ bool IsCollapsed() {
+ return !IsSet() || (IsInSameElement() && StartOffsetInTextInBlock() ==
+ EndOffsetInTextInBlock());
+ }
+
+ bool IsIndexesSet() const {
+ return mStartIndex.isSome() && mEndIndex.isSome();
+ }
+ bool IsSet() const {
+ return IsIndexesSet() && mStartOffsetInTextInBlock.isSome() &&
+ mEndOffsetInTextInBlock.isSome();
+ }
+ bool IsInSameElement() const {
+ return IsIndexesSet() && StartIndex() == EndIndex();
+ }
+
+ void Reset() {
+ mStartIndex.reset();
+ mEndIndex.reset();
+ mStartOffsetInTextInBlock.reset();
+ mEndOffsetInTextInBlock.reset();
+ }
+ void SetIndex(size_t aIndex) { mEndIndex = mStartIndex = Some(aIndex); }
+ void Set(size_t aIndex, uint32_t aOffsetInTextInBlock) {
+ mEndIndex = mStartIndex = Some(aIndex);
+ mStartOffsetInTextInBlock = mEndOffsetInTextInBlock =
+ Some(aOffsetInTextInBlock);
+ }
+ void SetIndexes(size_t aStartIndex, size_t aEndIndex) {
+ MOZ_DIAGNOSTIC_ASSERT(aStartIndex <= aEndIndex);
+ mStartIndex = Some(aStartIndex);
+ mEndIndex = Some(aEndIndex);
+ }
+ void Set(size_t aStartIndex, size_t aEndIndex,
+ uint32_t aStartOffsetInTextInBlock,
+ uint32_t aEndOffsetInTextInBlock) {
+ MOZ_DIAGNOSTIC_ASSERT(aStartIndex <= aEndIndex);
+ mStartIndex = Some(aStartIndex);
+ mEndIndex = Some(aEndIndex);
+ mStartOffsetInTextInBlock = Some(aStartOffsetInTextInBlock);
+ mEndOffsetInTextInBlock = Some(aEndOffsetInTextInBlock);
+ }
+
+ void CollapseToStart() {
+ MOZ_ASSERT(mStartIndex.isSome());
+ MOZ_ASSERT(mStartOffsetInTextInBlock.isSome());
+ mEndIndex = mStartIndex;
+ mEndOffsetInTextInBlock = mStartOffsetInTextInBlock;
+ }
+
+ private:
+ Maybe<size_t> mStartIndex;
+ Maybe<size_t> mEndIndex;
+ // Selected start and end offset in all text in a block element.
+ Maybe<uint32_t> mStartOffsetInTextInBlock;
+ Maybe<uint32_t> mEndOffsetInTextInBlock;
+ };
+ Selection mSelection;
+ };
+
+ RefPtr<dom::Document> mDocument;
+ nsCOMPtr<nsISelectionController> mSelCon;
+ RefPtr<EditorBase> mEditorBase;
+ RefPtr<FilteredContentIterator> mFilteredIter;
+ nsCOMPtr<nsIContent> mPrevTextBlock;
+ nsCOMPtr<nsIContent> mNextTextBlock;
+ OffsetEntryArray mOffsetTable;
+ RefPtr<nsRange> mExtent;
+
+ uint32_t mTxtSvcFilterType;
+ IteratorStatus mIteratorStatus;
+
+ protected:
+ virtual ~TextServicesDocument() = default;
+
+ public:
+ TextServicesDocument();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(TextServicesDocument)
+
+ /**
+ * Initializes the text services document to use a particular editor. The
+ * text services document will use the DOM document and presentation shell
+ * used by the editor.
+ *
+ * @param aEditor The editor to use.
+ */
+ nsresult InitWithEditor(nsIEditor* aEditor);
+
+ /**
+ * Sets the range/extent over which the text services document will iterate.
+ * Note that InitWithEditor() should have been called prior to calling this
+ * method. If this method is never called, the text services defaults to
+ * iterating over the entire document.
+ *
+ * @param aAbstractRange The range to use. aAbstractRange must point to a
+ * valid range object.
+ */
+ nsresult SetExtent(const dom::AbstractRange* aAbstractRange);
+
+ /**
+ * Expands the end points of the range so that it spans complete words. This
+ * call does not change any internal state of the text services document.
+ *
+ * @param aStaticRange [in/out] The range to be expanded/adjusted.
+ */
+ nsresult ExpandRangeToWordBoundaries(dom::StaticRange* aStaticRange);
+
+ /**
+ * Sets the filter type to be used while iterating over content.
+ * This will clear the current filter type if it's not either
+ * FILTERTYPE_NORMAL or FILTERTYPE_MAIL.
+ *
+ * @param aFilterType The filter type to be used while iterating over
+ * content.
+ */
+ nsresult SetFilterType(uint32_t aFilterType);
+
+ /**
+ * Returns the text in the current text block.
+ *
+ * @param aStr [OUT] This will contain the text.
+ */
+ nsresult GetCurrentTextBlock(nsAString& aStr);
+
+ /**
+ * Tells the document to point to the first text block in the document. This
+ * method does not adjust the current cursor position or selection.
+ */
+ nsresult FirstBlock();
+
+ enum class BlockSelectionStatus {
+ // There is no text block (TB) in or before the selection (S).
+ eBlockNotFound = 0,
+ // No TB in S, but found one before/after S.
+ eBlockOutside,
+ // S extends beyond the start and end of TB.
+ eBlockInside,
+ // TB contains entire S.
+ eBlockContains,
+ // S begins or ends in TB but extends outside of TB.
+ eBlockPartial,
+ };
+
+ /**
+ * Tells the document to point to the last text block that contains the
+ * current selection or caret.
+ *
+ * @param aSelectionStatus [OUT] This will contain the text block
+ * selection status.
+ * @param aSelectionOffset [OUT] This will contain the offset into the
+ * string returned by GetCurrentTextBlock() where
+ * the selection begins.
+ * @param aLength [OUT] This will contain the number of
+ * characters that are selected in the string.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult LastSelectedBlock(BlockSelectionStatus* aSelStatus,
+ uint32_t* aSelOffset, uint32_t* aSelLength);
+
+ /**
+ * Tells the document to point to the text block before the current one.
+ * This method will return NS_OK, even if there is no previous block.
+ * Callers should call IsDone() to check if we have gone beyond the first
+ * text block in the document.
+ */
+ nsresult PrevBlock();
+
+ /**
+ * Tells the document to point to the text block after the current one.
+ * This method will return NS_OK, even if there is no next block. Callers
+ * should call IsDone() to check if we have gone beyond the last text block
+ * in the document.
+ */
+ nsresult NextBlock();
+
+ /**
+ * IsDone() will always set aIsDone == false unless the document contains
+ * no text, PrevBlock() was called while the document was already pointing
+ * to the first text block in the document, or NextBlock() was called while
+ * the document was already pointing to the last text block in the document.
+ *
+ * @param aIsDone [OUT] This will contain the result.
+ */
+ nsresult IsDone(bool* aIsDone);
+
+ /**
+ * SetSelection() allows the caller to set the selection based on an offset
+ * into the string returned by GetCurrentTextBlock(). A length of zero
+ * places the cursor at that offset. A positive non-zero length "n" selects
+ * n characters in the string.
+ *
+ * @param aOffset Offset into string returned by
+ * GetCurrentTextBlock().
+ * @param aLength Number of characters selected.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult SetSelection(uint32_t aOffset, uint32_t aLength);
+
+ /**
+ * Scrolls the document so that the current selection is visible.
+ */
+ nsresult ScrollSelectionIntoView();
+
+ /**
+ * Deletes the text selected by SetSelection(). Calling DeleteSelection()
+ * with nothing selected, or with a collapsed selection (cursor) does
+ * nothing and returns NS_OK.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult DeleteSelection();
+
+ /**
+ * Inserts the given text at the current cursor position. If there is a
+ * selection, it will be deleted before the text is inserted.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult InsertText(const nsAString& aText);
+
+ /**
+ * nsIEditActionListener method implementations.
+ */
+ NS_DECL_NSIEDITACTIONLISTENER
+
+ /**
+ * Actual edit action listeners. When you add new method here for listening
+ * to new edit action, you need to make it called by EditorBase.
+ * Additionally, you need to call it from proper method of
+ * nsIEditActionListener too because if this is created not for inline
+ * spell checker of the editor, edit actions will be notified via
+ * nsIEditActionListener (slow path, though).
+ */
+ void DidDeleteContent(const nsIContent& aChildContent);
+ void DidJoinContents(const EditorRawDOMPoint& aJoinedPoint,
+ const nsIContent& aRemovedContent,
+ JoinNodesDirection aJoinNodesDirection);
+
+ private:
+ // TODO: We should get rid of this method since `aAbstractRange` has
+ // enough simple API to get them.
+ static nsresult GetRangeEndPoints(const dom::AbstractRange* aAbstractRange,
+ nsINode** aStartContainer,
+ uint32_t* aStartOffset,
+ nsINode** aEndContainer,
+ uint32_t* aEndOffset);
+
+ nsresult CreateFilteredContentIterator(
+ const dom::AbstractRange* aAbstractRange,
+ FilteredContentIterator** aFilteredIter);
+
+ dom::Element* GetDocumentContentRootNode() const;
+ already_AddRefed<nsRange> CreateDocumentContentRange();
+ already_AddRefed<nsRange> CreateDocumentContentRootToNodeOffsetRange(
+ nsINode* aParent, uint32_t aOffset, bool aToStart);
+ nsresult CreateDocumentContentIterator(
+ FilteredContentIterator** aFilteredIter);
+
+ nsresult AdjustContentIterator();
+
+ static nsresult FirstTextNode(FilteredContentIterator* aFilteredIter,
+ IteratorStatus* aIteratorStatus);
+ static nsresult LastTextNode(FilteredContentIterator* aFilteredIter,
+ IteratorStatus* aIteratorStatus);
+
+ static nsresult FirstTextNodeInCurrentBlock(
+ FilteredContentIterator* aFilteredIter);
+ static nsresult FirstTextNodeInPrevBlock(
+ FilteredContentIterator* aFilteredIter);
+ static nsresult FirstTextNodeInNextBlock(
+ FilteredContentIterator* aFilteredIter);
+
+ nsresult GetFirstTextNodeInPrevBlock(nsIContent** aContent);
+ nsresult GetFirstTextNodeInNextBlock(nsIContent** aContent);
+
+ static bool DidSkip(FilteredContentIterator* aFilteredIter);
+ static void ClearDidSkip(FilteredContentIterator* aFilteredIter);
+
+ static bool HasSameBlockNodeParent(dom::Text& aTextNode1,
+ dom::Text& aTextNode2);
+
+ MOZ_CAN_RUN_SCRIPT nsresult SetSelectionInternal(uint32_t aOffset,
+ uint32_t aLength,
+ bool aDoUpdate);
+ MOZ_CAN_RUN_SCRIPT nsresult GetSelection(BlockSelectionStatus* aSelStatus,
+ uint32_t* aSelOffset,
+ uint32_t* aSelLength);
+ MOZ_CAN_RUN_SCRIPT nsresult
+ GetCollapsedSelection(BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset,
+ uint32_t* aSelLength);
+ nsresult GetUncollapsedSelection(BlockSelectionStatus* aSelStatus,
+ uint32_t* aSelOffset, uint32_t* aSelLength);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_TextServicesDocument_h
diff --git a/editor/spellchecker/moz.build b/editor/spellchecker/moz.build
new file mode 100644
index 0000000000..8e4932c209
--- /dev/null
+++ b/editor/spellchecker/moz.build
@@ -0,0 +1,34 @@
+# -*- 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/mochitest.ini"]
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.ini"]
+
+XPIDL_SOURCES += [
+ "nsIInlineSpellChecker.idl",
+]
+
+XPIDL_MODULE = "txtsvc"
+
+EXPORTS.mozilla += [
+ "EditorSpellCheck.h",
+ "TextServicesDocument.h",
+]
+
+UNIFIED_SOURCES += [
+ "EditorSpellCheck.cpp",
+ "FilteredContentIterator.cpp",
+ "nsComposeTxtSrvFilter.cpp",
+ "TextServicesDocument.cpp",
+]
+
+LOCAL_INCLUDES += [
+ # For stop exposing libeditor's headers, allow to refer them directly
+ "/editor/libeditor",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/editor/spellchecker/nsComposeTxtSrvFilter.cpp b/editor/spellchecker/nsComposeTxtSrvFilter.cpp
new file mode 100644
index 0000000000..7ab6eae8cb
--- /dev/null
+++ b/editor/spellchecker/nsComposeTxtSrvFilter.cpp
@@ -0,0 +1,64 @@
+/* -*- 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 "nsComposeTxtSrvFilter.h"
+#include "nsError.h" // for NS_OK
+#include "nsIContent.h" // for nsIContent
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "mozilla/dom/Element.h" // for nsIContent
+
+using namespace mozilla;
+
+bool nsComposeTxtSrvFilter::Skip(nsINode* aNode) const {
+ if (NS_WARN_IF(!aNode)) {
+ return false;
+ }
+
+ // Check to see if we can skip this node
+
+ if (aNode->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::textarea,
+ nsGkAtoms::select, nsGkAtoms::style,
+ nsGkAtoms::map)) {
+ return true;
+ }
+
+ if (!mIsForMail) {
+ return false;
+ }
+
+ // For nodes that are blockquotes, we must make sure
+ // their type is "cite"
+ if (aNode->IsHTMLElement(nsGkAtoms::blockquote)) {
+ return aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::cite, eIgnoreCase);
+ }
+
+ if (aNode->IsHTMLElement(nsGkAtoms::span)) {
+ if (aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozquote,
+ nsGkAtoms::_true, eIgnoreCase)) {
+ return true;
+ }
+
+ return aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
+ nsGkAtoms::mozsignature,
+ eCaseMatters);
+ }
+
+ if (aNode->IsHTMLElement(nsGkAtoms::table)) {
+ return aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
+ u"moz-email-headers-table"_ns,
+ eCaseMatters);
+ }
+
+ return false;
+}
+
+// static
+UniquePtr<nsComposeTxtSrvFilter> nsComposeTxtSrvFilter::CreateHelper(
+ bool aIsForMail) {
+ auto filter = MakeUnique<nsComposeTxtSrvFilter>();
+ filter->Init(aIsForMail);
+ return filter;
+}
diff --git a/editor/spellchecker/nsComposeTxtSrvFilter.h b/editor/spellchecker/nsComposeTxtSrvFilter.h
new file mode 100644
index 0000000000..b2de83451b
--- /dev/null
+++ b/editor/spellchecker/nsComposeTxtSrvFilter.h
@@ -0,0 +1,45 @@
+/* -*- 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 nsComposeTxtSrvFilter_h__
+#define nsComposeTxtSrvFilter_h__
+
+#include "mozilla/UniquePtr.h"
+
+class nsINode;
+
+/**
+ * This class enables those using it to skip over certain nodes when
+ * traversing content.
+ *
+ * This filter is used to skip over various form control nodes and
+ * mail's cite nodes
+ */
+class nsComposeTxtSrvFilter final {
+ public:
+ static mozilla::UniquePtr<nsComposeTxtSrvFilter> CreateNormalFilter() {
+ return CreateHelper(false);
+ }
+ static mozilla::UniquePtr<nsComposeTxtSrvFilter> CreateMailFilter() {
+ return CreateHelper(true);
+ }
+
+ /**
+ * Indicates whether the content node should be skipped by the iterator
+ * @param aNode - node to skip
+ */
+ bool Skip(nsINode* aNode) const;
+
+ private:
+ // Helper - Intializer
+ void Init(bool aIsForMail) { mIsForMail = aIsForMail; }
+
+ static mozilla::UniquePtr<nsComposeTxtSrvFilter> CreateHelper(
+ bool aIsForMail);
+
+ bool mIsForMail = false;
+};
+
+#endif
diff --git a/editor/spellchecker/nsIInlineSpellChecker.idl b/editor/spellchecker/nsIInlineSpellChecker.idl
new file mode 100644
index 0000000000..d6f66a793d
--- /dev/null
+++ b/editor/spellchecker/nsIInlineSpellChecker.idl
@@ -0,0 +1,40 @@
+/* -*- 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 "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsIEditor;
+interface nsIEditorSpellCheck;
+
+webidl Node;
+webidl Range;
+
+[scriptable, uuid(b7b7a77c-40c4-4196-b0b7-b0338243b3fe)]
+interface nsIInlineSpellChecker : nsISupports
+{
+ readonly attribute nsIEditorSpellCheck spellChecker;
+
+ void init(in nsIEditor aEditor);
+ void cleanup(in boolean aDestroyingFrames);
+
+ attribute boolean enableRealTimeSpell;
+
+ void spellCheckRange(in Range aSelection);
+
+ Range getMisspelledWord(in Node aNode, in unsigned long aOffset);
+ [can_run_script]
+ void replaceWord(in Node aNode,
+ in unsigned long aOffset,
+ in AString aNewword);
+ void addWordToDictionary(in AString aWord);
+ void removeWordFromDictionary(in AString aWord);
+
+ void ignoreWord(in AString aWord);
+ void ignoreWords(in Array<AString> aWordsToIgnore);
+ void updateCurrentDictionary();
+
+ readonly attribute boolean spellCheckPending;
+};
diff --git a/editor/spellchecker/tests/bug1200533_subframe.html b/editor/spellchecker/tests/bug1200533_subframe.html
new file mode 100644
index 0000000000..f095c0601f
--- /dev/null
+++ b/editor/spellchecker/tests/bug1200533_subframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Language" content="en-US">
+</head>
+<body>
+<textarea id="none">root en-US</textarea>
+<textarea id="en-GB" lang="en-GB">root en-US, but element en-GB</textarea>
+<textarea id="en-gb" lang="en-gb">root en-US, but element en-gb (lower case)</textarea>
+<textarea id="en-ZA-not-avail" lang="en-ZA">root en-US, but element en-ZA (which is not installed)</textarea>
+<textarea id="en-generic" lang="en">root en-US, but element en</textarea>
+<textarea id="en" lang="en">root en-US, but element en</textarea>
+<textarea id="ko-not-avail" lang="ko">root en-US, but element ko (which is not installed)</textarea>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/bug1204147_subframe.html b/editor/spellchecker/tests/bug1204147_subframe.html
new file mode 100644
index 0000000000..a9b1225cd9
--- /dev/null
+++ b/editor/spellchecker/tests/bug1204147_subframe.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="en-GB" lang="en-GB">element en-GB</textarea>
+<textarea id="en-US" lang="testing-XX">element should default to en-US</textarea>
+
+<div id="trouble-maker" contenteditable>the presence of this div triggers the faulty code path</div>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/bug1204147_subframe2.html b/editor/spellchecker/tests/bug1204147_subframe2.html
new file mode 100644
index 0000000000..935777bd99
--- /dev/null
+++ b/editor/spellchecker/tests/bug1204147_subframe2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="en-GB" lang="en-GB">element en-GB</textarea>
+<textarea id="en-US" lang="testing-XX">element should default to en-US</textarea>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/bug678842_subframe.html b/editor/spellchecker/tests/bug678842_subframe.html
new file mode 100644
index 0000000000..39d578ee41
--- /dev/null
+++ b/editor/spellchecker/tests/bug678842_subframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="textarea" lang="testing-XXX"></textarea>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/bug717433_subframe.html b/editor/spellchecker/tests/bug717433_subframe.html
new file mode 100644
index 0000000000..3c2927e88f
--- /dev/null
+++ b/editor/spellchecker/tests/bug717433_subframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="textarea" lang="en"></textarea>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/chrome.ini b/editor/spellchecker/tests/chrome.ini
new file mode 100644
index 0000000000..76faf04121
--- /dev/null
+++ b/editor/spellchecker/tests/chrome.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+prefs =
+ gfx.font_loader.delay=0
+
+skip-if = os == 'android'
+
+[test_nsIEditorSpellCheck_ReplaceWord.html]
+support-files =
+ spellcheck.js
diff --git a/editor/spellchecker/tests/de-DE/de_DE.aff b/editor/spellchecker/tests/de-DE/de_DE.aff
new file mode 100644
index 0000000000..5dc6896b6d
--- /dev/null
+++ b/editor/spellchecker/tests/de-DE/de_DE.aff
@@ -0,0 +1,2 @@
+# Affix file for German English dictionary
+# Fake file, nothing here.
diff --git a/editor/spellchecker/tests/de-DE/de_DE.dic b/editor/spellchecker/tests/de-DE/de_DE.dic
new file mode 100644
index 0000000000..415c216861
--- /dev/null
+++ b/editor/spellchecker/tests/de-DE/de_DE.dic
@@ -0,0 +1,6 @@
+5
+ein
+guter
+heute
+ist
+Tag
diff --git a/editor/spellchecker/tests/en-AU/en_AU.aff b/editor/spellchecker/tests/en-AU/en_AU.aff
new file mode 100644
index 0000000000..e0c467248d
--- /dev/null
+++ b/editor/spellchecker/tests/en-AU/en_AU.aff
@@ -0,0 +1,2 @@
+# Affix file for British English dictionary
+# Fake file, nothing here.
diff --git a/editor/spellchecker/tests/en-AU/en_AU.dic b/editor/spellchecker/tests/en-AU/en_AU.dic
new file mode 100644
index 0000000000..0a1be725d4
--- /dev/null
+++ b/editor/spellchecker/tests/en-AU/en_AU.dic
@@ -0,0 +1,4 @@
+3
+Mary
+Paul
+Peter
diff --git a/editor/spellchecker/tests/en-GB/en_GB.aff b/editor/spellchecker/tests/en-GB/en_GB.aff
new file mode 100644
index 0000000000..e0c467248d
--- /dev/null
+++ b/editor/spellchecker/tests/en-GB/en_GB.aff
@@ -0,0 +1,2 @@
+# Affix file for British English dictionary
+# Fake file, nothing here.
diff --git a/editor/spellchecker/tests/en-GB/en_GB.dic b/editor/spellchecker/tests/en-GB/en_GB.dic
new file mode 100644
index 0000000000..0a1be725d4
--- /dev/null
+++ b/editor/spellchecker/tests/en-GB/en_GB.dic
@@ -0,0 +1,4 @@
+3
+Mary
+Paul
+Peter
diff --git a/editor/spellchecker/tests/mochitest.ini b/editor/spellchecker/tests/mochitest.ini
new file mode 100644
index 0000000000..6d3c53d71b
--- /dev/null
+++ b/editor/spellchecker/tests/mochitest.ini
@@ -0,0 +1,68 @@
+[DEFAULT]
+prefs =
+ gfx.font_loader.delay=0
+
+skip-if = os == 'android'
+support-files =
+ en-GB/en_GB.dic
+ en-GB/en_GB.aff
+ en-AU/en_AU.dic
+ en-AU/en_AU.aff
+ de-DE/de_DE.dic
+ de-DE/de_DE.aff
+ ru-RU/ru_RU.dic
+ ru-RU/ru_RU.aff
+ spellcheck.js
+
+[test_async_UpdateCurrentDictionary.html]
+[test_bug1100966.html]
+[test_bug1154791.html]
+[test_bug1200533.html]
+skip-if =
+ http3
+support-files =
+ bug1200533_subframe.html
+[test_bug1204147.html]
+skip-if =
+ http3
+support-files =
+ bug1204147_subframe.html
+ bug1204147_subframe2.html
+[test_bug1205983.html]
+[test_bug1209414.html]
+[test_bug1219928.html]
+skip-if = true
+[test_bug1365383.html]
+[test_bug1368544.html]
+[test_bug1402822.html]
+[test_bug1418629.html]
+[test_bug1497480.html]
+[test_bug1602526.html]
+[test_bug1761273.html]
+[test_bug1773802.html]
+[test_bug1837268.html]
+[test_bug366682.html]
+[test_bug432225.html]
+[test_bug484181.html]
+[test_bug596333.html]
+[test_bug636465.html]
+[test_bug678842.html]
+skip-if =
+ http3
+support-files =
+ bug678842_subframe.html
+[test_bug697981.html]
+[test_bug717433.html]
+skip-if =
+ http3
+support-files =
+ bug717433_subframe.html
+[test_multiple_content_languages.html]
+skip-if =
+ http3
+support-files =
+ multiple_content_languages_subframe.html
+[test_spellcheck_after_edit.html]
+[test_spellcheck_after_pressing_navigation_key.html]
+[test_spellcheck_selection.html]
+[test_suggest.html]
diff --git a/editor/spellchecker/tests/multiple_content_languages_subframe.html b/editor/spellchecker/tests/multiple_content_languages_subframe.html
new file mode 100644
index 0000000000..da6a4ed664
--- /dev/null
+++ b/editor/spellchecker/tests/multiple_content_languages_subframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Language" content="en-US, en-GB, ko, en-CA">
+</head>
+<body>
+<textarea id="none">root en-US and en-GB</textarea>
+<textarea id="en-GB" lang="en-GB">root multiple, but element only en-GB</textarea>
+<textarea id="en-ZA-not-avail" lang="en-ZA">root multiple en, but element en-ZA (which is not installed)</textarea>
+<textarea id="en" lang="en">root multiple en, but element en</textarea>
+<textarea id="ko-not-avail" lang="ko">root multiple en, but element ko (which is not installed)</textarea>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/ru-RU/ru_RU.aff b/editor/spellchecker/tests/ru-RU/ru_RU.aff
new file mode 100644
index 0000000000..c6cf721484
--- /dev/null
+++ b/editor/spellchecker/tests/ru-RU/ru_RU.aff
@@ -0,0 +1 @@
+SET KOI8-R
diff --git a/editor/spellchecker/tests/ru-RU/ru_RU.dic b/editor/spellchecker/tests/ru-RU/ru_RU.dic
new file mode 100644
index 0000000000..dbfe3a9f23
--- /dev/null
+++ b/editor/spellchecker/tests/ru-RU/ru_RU.dic
@@ -0,0 +1,2 @@
+1
+ÐÒÁ×ÉÌØÎÙÊ
diff --git a/editor/spellchecker/tests/spellcheck.js b/editor/spellchecker/tests/spellcheck.js
new file mode 100644
index 0000000000..b6a1586628
--- /dev/null
+++ b/editor/spellchecker/tests/spellcheck.js
@@ -0,0 +1,36 @@
+function isSpellingCheckOk(aEditor, aMisspelledWords, aTodo = false) {
+ var selcon = aEditor.selectionController;
+ var sel = selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+ var numWords = sel.rangeCount;
+
+ if (aTodo) {
+ todo_is(
+ numWords,
+ aMisspelledWords.length,
+ "Correct number of misspellings and words."
+ );
+ } else {
+ is(
+ numWords,
+ aMisspelledWords.length,
+ "Correct number of misspellings and words."
+ );
+ }
+
+ if (numWords !== aMisspelledWords.length) {
+ return false;
+ }
+
+ for (var i = 0; i < numWords; ++i) {
+ var word = String(sel.getRangeAt(i));
+ if (aTodo) {
+ todo_is(word, aMisspelledWords[i], "Misspelling is what we think it is.");
+ } else {
+ is(word, aMisspelledWords[i], "Misspelling is what we think it is.");
+ }
+ if (word !== aMisspelledWords[i]) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html b/editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html
new file mode 100644
index 0000000000..6f8371d2cd
--- /dev/null
+++ b/editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=856270
+-->
+<head>
+ <title>Test for Bug 856270 - Async UpdateCurrentDictionary</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=856270">Mozilla Bug 856270</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="editor" spellcheck="true"></textarea>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(start);
+
+function start() {
+ var textarea = document.getElementById("editor");
+ textarea.focus();
+
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ )
+ onSpellCheck(textarea, function() {
+ var isc = SpecialPowers.wrap(textarea).editor.getInlineSpellChecker(false);
+ ok(isc, "Inline spell checker should exist after focus and spell check");
+ var sc = isc.spellChecker;
+
+ sc.setCurrentDictionaries(["testing-XX"]).then(() => {
+ is(true, false, "Setting a non-existent dictionary should fail");
+ }, () => {
+ let currentDictionaries = sc.getCurrentDictionaries();
+
+ is(currentDictionaries.length, 0, "expected no dictionaries");
+ // First, set the lang attribute on the textarea, call Update, and make
+ // sure the spell checker's language was updated appropriately.
+ var lang = "en-US";
+ textarea.setAttribute("lang", lang);
+ sc.UpdateCurrentDictionary(function() {
+ currentDictionaries = sc.getCurrentDictionaries();
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ is(sc.getCurrentDictionaries()[0], lang,
+ "UpdateCurrentDictionary should set the current dictionary.");
+ sc.setCurrentDictionaries([]).then(() => {
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1100966.html b/editor/spellchecker/tests/test_bug1100966.html
new file mode 100644
index 0000000000..4ed52968a0
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1100966.html
@@ -0,0 +1,74 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1100966
+-->
+<head>
+ <title>Test for Bug 1100966</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>
+=====<br>
+correct<br>
+fivee sixx<br>
+====
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+/** Test for Bug 1100966 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("content");
+ div.focus();
+ synthesizeMouseAtCenter(div, {});
+
+ getSpellChecker().UpdateCurrentDictionary(() => {
+ sendString(" ");
+ SimpleTest.executeSoon(function() {
+ sendString("a");
+ SimpleTest.executeSoon(function() {
+ synthesizeKey("KEY_Backspace");
+
+ maybeOnSpellCheck(div, function() {
+ var sel = getSpellCheckSelection();
+ is(sel.rangeCount, 2, "We should have two misspelled words");
+ is(String(sel.getRangeAt(0)), "fivee", "Correct misspelled word");
+ is(String(sel.getRangeAt(1)), "sixx", "Correct misspelled word");
+
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+});
+
+function getEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window);
+}
+
+function getSpellChecker() {
+ return getEditor().getInlineSpellChecker(false).spellChecker;
+}
+
+function getSpellCheckSelection() {
+ var selcon = getEditor().selectionController;
+ return selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/spellchecker/tests/test_bug1154791.html b/editor/spellchecker/tests/test_bug1154791.html
new file mode 100644
index 0000000000..7ed6c53e35
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1154791.html
@@ -0,0 +1,74 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1154791
+-->
+<head>
+ <title>Test for Bug 1154791</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>
+<tt>thiss onee is stilll a</tt>
+</div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+/** Test for Bug 1154791 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var div = document.getElementById("content");
+ div.focus();
+ getSpellChecker().UpdateCurrentDictionary(() => {
+ synthesizeMouseAtCenter(div, {});
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_ArrowLeft");
+
+ SimpleTest.executeSoon(function() {
+ synthesizeKey("KEY_Backspace");
+ SimpleTest.executeSoon(function() {
+ sendString(" ");
+
+ maybeOnSpellCheck(div, function() {
+ var sel = getSpellCheckSelection();
+ is(sel.rangeCount, 2, "We should have two misspelled words");
+ is(String(sel.getRangeAt(0)), "thiss", "Correct misspelled word");
+ is(String(sel.getRangeAt(1)), "onee", "Correct misspelled word");
+
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+});
+
+function getEditor() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ return editingSession.getEditorForWindow(window);
+}
+
+function getSpellChecker() {
+ return getEditor().getInlineSpellChecker(false).spellChecker;
+}
+
+function getSpellCheckSelection() {
+ var selcon = getEditor().selectionController;
+ return selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/editor/spellchecker/tests/test_bug1200533.html b/editor/spellchecker/tests/test_bug1200533.html
new file mode 100644
index 0000000000..d97dea8ccd
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1200533.html
@@ -0,0 +1,163 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1200533
+-->
+<head>
+ <title>Test for Bug 1200533</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=1200533">Mozilla Bug 1200533</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" ttype="application/javascript">
+
+/** Test for Bug 1200533 **/
+/** Visit the elements defined above and check the dictionary we got **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+
+var tests = [
+ // text area, value of spellchecker.dictionary, result.
+ // Result: Document language.
+ [ "none", "", "en-US" ],
+ // Result: Element language.
+ [ "en-GB", "", "en-GB" ],
+ [ "en-gb", "", "en-GB" ],
+ // Result: Random en-* or en-US (if application locale is en-US).
+ [ "en-ZA-not-avail", "", "*" ],
+ [ "en-generic", "", "*" ],
+ [ "en", "", "*" ],
+ // Result: Locale.
+ [ "ko-not-avail", "", "en-US" ],
+
+ // Result: Preference value in all cases.
+ [ "en-ZA-not-avail", "en-AU", "en-AU" ],
+ [ "en-generic", "en-AU", "en-AU" ],
+ [ "ko-not-avail", "en-AU", "en-AU" ],
+
+ // Result: Random en-*.
+ [ "en-ZA-not-avail", "de-DE", "*" ],
+ [ "en-generic", "de-DE", "*" ],
+ // Result: Preference value.
+ [ "ko-not-avail", "de-DE", "de-DE" ],
+ ];
+
+var loadCount = 0;
+var retrying = false;
+var script;
+
+var loadListener = async function(evt) {
+ if (loadCount == 0) {
+ script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install en-GB, en-AU and de-DE dictionaries.
+ var en_GB = dir.clone();
+ var en_AU = dir.clone();
+ var de_DE = dir.clone();
+ en_GB.append("en-GB");
+ en_AU.append("en-AU");
+ de_DE.append("de-DE");
+ hunspell.addDirectory(en_GB);
+ hunspell.addDirectory(en_AU);
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("check-existence",
+ () => [en_GB.exists(), en_AU.exists(),
+ de_DE.exists()]);
+ addMessageListener("destroy", () => {
+ hunspell.removeDirectory(en_GB);
+ hunspell.removeDirectory(en_AU);
+ hunspell.removeDirectory(de_DE);
+ });
+ });
+ var existenceChecks = await script.sendQuery("check-existence");
+ is(existenceChecks[0], true, "true expected (en-GB directory should exist)");
+ is(existenceChecks[1], true, "true expected (en-AU directory should exist)");
+ is(existenceChecks[2], true, "true expected (de-DE directory should exist)");
+ }
+
+ SpecialPowers.pushPrefEnv({set: [["spellchecker.dictionary", tests[loadCount][1]]]},
+ function() { continueTest(evt); });
+};
+
+function continueTest(evt) {
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById(tests[loadCount][0]);
+ var editor = SpecialPowers.wrap(elem).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+ const is_en_US = SpecialPowers.Services.locale.appLocaleAsBCP47 == "en-US";
+
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ onSpellCheck(elem, async function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictionaries;
+ try {
+ currentDictionaries = spellchecker.getCurrentDictionaries();
+ } catch (e) {}
+
+ if (!currentDictionaries && !retrying) {
+ // It's possible for an asynchronous font-list update to cause a reflow
+ // that disrupts the async spell-check and results in not getting a
+ // current dictionary here; if that happens, we retry the same testcase
+ // by reloading the iframe without bumping loadCount.
+ info(`No current dictionary: retrying testcase ${loadCount}`);
+ retrying = true;
+ } else {
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ let dict = currentDictionaries[0];
+ if (tests[loadCount][2] != "*") {
+ is(dict, tests[loadCount][2], "expected " + tests[loadCount][2]);
+ } else if (is_en_US && tests[loadCount][0].startsWith("en")) {
+ // Current application locale is en-US and content lang is en or
+ // en-unknown, so we should use en-US dictionary as default.
+ is(dict, "en-US", "expected en-US that is application locale");
+ } else {
+ var gotEn = (dict == "en-GB" || dict == "en-AU" || dict == "en-US");
+ is(gotEn, true, "expected en-AU or en-GB or en-US");
+ }
+
+ loadCount++;
+ retrying = false;
+ }
+
+ if (loadCount < tests.length) {
+ // Load the iframe again.
+ content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug1200533_subframe.html?firstload=false";
+ } else {
+ // Remove the fake dictionaries again, since it's otherwise picked up by later tests.
+ await script.sendQuery("destroy");
+
+ SimpleTest.finish();
+ }
+ });
+}
+
+content.addEventListener("load", loadListener);
+
+content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug1200533_subframe.html?firstload=true";
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1204147.html b/editor/spellchecker/tests/test_bug1204147.html
new file mode 100644
index 0000000000..3d5f0e120f
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1204147.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1204147
+-->
+<head>
+ <title>Test for Bug 1204147</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=1204147">Mozilla Bug 1204147</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1204147 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+// Load a subframe containing an editor with using "en-GB". At first
+// load, it will set the dictionary to "en-GB". The bug was that a content preference
+// was also created. At second load, we check the dictionary for another element,
+// one that should use "en-US". With the bug corrected, we get "en-US", before
+// we got "en-GB" from the content preference.
+
+var firstLoad = true;
+var script;
+
+var loadListener = async function(evt) {
+ if (firstLoad) {
+ script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install en-GB dictionary.
+ let en_GB = dir.clone();
+ en_GB.append("en-GB");
+ hunspell.addDirectory(en_GB);
+
+ addMessageListener("en_GB-exists", () => en_GB.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(en_GB));
+ });
+ is(await script.sendQuery("en_GB-exists"), true,
+ "true expected (en-GB directory should exist)");
+ }
+
+ var doc = evt.target.contentDocument;
+ var elem;
+ if (firstLoad) {
+ elem = doc.getElementById("en-GB");
+ } else {
+ elem = doc.getElementById("en-US");
+ }
+
+ var editor = SpecialPowers.wrap(elem).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ onSpellCheck(elem, async function() {
+ let spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictionaries = spellchecker.getCurrentDictionaries();
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ let currentDictionary = currentDictionaries[0];
+ if (firstLoad) {
+ firstLoad = false;
+
+ // First time around, the element's language should be used.
+ is(currentDictionary, "en-GB", "unexpected lang " + currentDictionary + " instead of en-GB");
+
+ // Note that on second load, we load a different page, which does NOT have the trouble-causing
+ // contenteditable in it. Sadly, loading the same page with the trouble-maker in it
+ // doesn't allow the retrieval of the spell check dictionary used for the element,
+ // because the trouble-maker causes the 'global' spell check dictionary to be set to "en-GB"
+ // (since it picks the first one from the list) before we have the chance to retrieve
+ // the dictionary for the element (which happens asynchonously after the spell check has completed).
+ content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug1204147_subframe2.html?firstload=false";
+ } else {
+ // Second time around, the element should default to en-US.
+ // Without the fix, the first run sets the content preference to en-GB for the whole site.
+ is(currentDictionary, "en-US", "unexpected lang " + currentDictionary + " instead of en-US");
+ content.removeEventListener("load", loadListener);
+
+ // Remove the fake en-GB dictionary again, since it's otherwise picked up by later tests.
+ await script.sendQuery("destroy");
+
+ // Reset the preference, so the last value we set doesn't collide with the next test.
+ SimpleTest.finish();
+ }
+ });
+};
+
+content.addEventListener("load", loadListener);
+
+content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug1204147_subframe.html?firstload=true";
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1205983.html b/editor/spellchecker/tests/test_bug1205983.html
new file mode 100644
index 0000000000..3de5e196bf
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1205983.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1205983
+-->
+<head>
+ <title>Test for Bug 1205983</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=1205983">Mozilla Bug 1205983</a>
+<p id="display"></p>
+</div>
+
+<div contenteditable id="de-DE" lang="de-DE" onfocus="deFocus()">German heute ist ein guter Tag</div>
+<textarea id="en-US" lang="en-US" onfocus="enFocus()">Nogoodword today is a nice day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(SpecialPowers.Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var selcon_de;
+var script;
+
+var { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+/** Test for Bug 1205983 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ var de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ });
+ is(await script.sendQuery("de_DE-exists"), true,
+ "true expected (de_DE directory should exist)");
+
+ document.getElementById("de-DE").focus();
+});
+
+function deFocus() {
+ elem_de = document.getElementById("de-DE");
+
+ maybeOnSpellCheck(elem_de, function() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ editor_de = editingSession.getEditorForWindow(window);
+ selcon_de = editor_de.selectionController;
+ var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+
+ // Check that we spelled in German, so there is only one misspelled word.
+ is(sel.toString(), "German", "one misspelled word expected: German");
+
+ // Now focus the textarea, which requires English spelling.
+ document.getElementById("en-US").focus();
+ });
+}
+
+function enFocus() {
+ var elem_en = document.getElementById("en-US");
+ var editor_en =
+ SpecialPowers.wrap(elem_en)
+ .editor;
+ editor_en.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor_en.getInlineSpellChecker(true);
+
+ maybeOnSpellCheck(elem_en, async function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictionaries = spellchecker.getCurrentDictionaries();
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ let currentDictionary = currentDictionaries[0];
+
+ // Check that the English dictionary is loaded and that the spell check has worked.
+ is(currentDictionary, "en-US", "expected en-US");
+ is(getMisspelledWords(editor_en), "Nogoodword", "one misspelled word expected: Nogoodword");
+
+ // So far all was boring. The important thing is whether the spell check result
+ // in the de-DE editor is still the same. After losing focus, no spell check
+ // updates should take place there.
+ var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+ is(sel.toString(), "German", "one misspelled word expected: German");
+
+ // Remove the fake de_DE dictionary again.
+ await script.sendQuery("destroy");
+
+ // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
+ elem_de.onfocus = null;
+ elem_de.blur();
+ elem_de.focus();
+
+ // After removal, the de_DE editor should refresh the spelling with en-US.
+ maybeOnSpellCheck(elem_de, function() {
+ var endSel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+ // eslint-disable-next-line no-useless-concat
+ is(endSel.toString(), "heute" + "ist" + "ein" + "guter",
+ "some misspelled words expected: heute ist ein guter");
+
+ // If we don't reset this, we cause massive leaks.
+ selcon_de = null;
+ editor_de = null;
+
+ SimpleTest.finish();
+ });
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1209414.html b/editor/spellchecker/tests/test_bug1209414.html
new file mode 100644
index 0000000000..b4b0cae947
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1209414.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1209414
+-->
+<head>
+ <title>Test for Bug 1209414</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=1209414">Mozilla Bug 1209414</a>
+<p id="display"></p>
+</div>
+
+<textarea id="de-DE" lang="de-DE">heute ist ein guter Tag - today is a good day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Ci = SpecialPowers.Ci;
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var script;
+
+/** Test for Bug 1209414 **/
+/*
+ * All we want to do in this test is change the spelling using a right-click and selection from the menu.
+ * This is necessary since all the other tests use setCurrentDictionaries() which doesn't reflect
+ * user behaviour.
+ */
+
+let { maybeOnSpellCheck, onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ var chromeWin = actorParent.rootFrameLoader
+ .ownerElement.ownerGlobal.browsingContext.topChromeWindow;
+ var contextMenu = chromeWin.document.getElementById("contentAreaContextMenu");
+ contextMenu.addEventListener("popupshown",
+ () => sendAsyncMessage("popupshown"));
+
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ let de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("hidepopup", function() {
+ var state = contextMenu.state;
+
+ // Select Language from the menu. Take a look at
+ // toolkit/modules/InlineSpellChecker.sys.mjs to see how the menu works.
+ contextMenu.ownerDocument.getElementById("spell-check-dictionary-en-US")
+ .doCommand();
+ contextMenu.hidePopup();
+ return state;
+ });
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ addMessageListener("contextMenu-not-null", () => contextMenu != null);
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ });
+ is(await script.sendQuery("contextMenu-not-null"), true,
+ "Got context menu XUL");
+ is(await script.sendQuery("de_DE-exists"), true,
+ "true expected (de_DE directory should exist)");
+ script.addMessageListener("popupshown", handlePopup);
+
+ elem_de = document.getElementById("de-DE");
+ editor_de = SpecialPowers.wrap(elem_de).editor;
+ editor_de.setSpellcheckUserOverride(true);
+
+ maybeOnSpellCheck(elem_de, function() {
+ var inlineSpellChecker = editor_de.getInlineSpellChecker(true);
+ var spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictionaries = spellchecker.getCurrentDictionaries();
+
+ // Check that the German dictionary is loaded and that the spell check has worked.
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ is(currentDictionaries[0], "de-DE", "expected de-DE");
+ // eslint-disable-next-line no-useless-concat
+ is(getMisspelledWords(editor_de), "today" + "is" + "a" + "good" + "day", "some misspelled words expected: today is a good day");
+
+ // Focus again, just to be sure that the context-click won't trigger another spell check.
+ elem_de.focus();
+
+ // Make sure all spell checking action is done before right-click to select the en-US dictionary.
+ maybeOnSpellCheck(elem_de, function() {
+ synthesizeMouse(elem_de, 2, 2, { type: "contextmenu", button: 2 }, window);
+ });
+ });
+});
+
+async function handlePopup() {
+ var state = await script.sendQuery("hidepopup");
+ is(state, "open", "checking if popup is open");
+
+ onSpellCheck(elem_de, async function() {
+ var inlineSpellChecker = editor_de.getInlineSpellChecker(true);
+ var spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictionaries = spellchecker.getCurrentDictionaries();
+
+ // Check that the English dictionary is loaded and that the spell check has worked.
+ is(currentDictionaries.length, 2, "expected two dictionaries");
+ let dictionaryArray = Array.from(currentDictionaries);
+ ok(dictionaryArray.includes("de-DE"), "expected de-DE");
+ ok(dictionaryArray.includes("en-US"), "expected en-US");
+ is(getMisspelledWords(editor_de), "", "No misspelled words expected");
+
+ // Remove the fake de_DE dictionary again.
+ await script.sendQuery("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.setCurrentDictionaries([]).then(() => {
+ SimpleTest.finish();
+ });
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1219928.html b/editor/spellchecker/tests/test_bug1219928.html
new file mode 100644
index 0000000000..83ae8de908
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1219928.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1219928
+-->
+<head>
+ <title>Test for Bug 1219928</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=1219928">Mozilla Bug 1219928</a>
+<p id="display"></p>
+
+<div contenteditable id="en-US" lang="en-US">
+<p>And here a missspelled word</p>
+<style>
+<!-- and here another onnee in a style comment -->
+</style>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1219928 **/
+/* Very simple test to check that <style> blocks are skipped in the spell check */
+
+var spellchecker;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+
+ var elem = document.getElementById("en-US");
+ elem.focus();
+
+ maybeOnSpellCheck(elem, function() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ var editor = editingSession.getEditorForWindow(window);
+ var selcon = editor.selectionController;
+ var sel = selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+
+ is(sel.toString(), "missspelled", "one misspelled word expected: missspelled");
+
+ spellchecker = SpecialPowers.Cu.createSpellChecker();
+ spellchecker.setFilterType(spellchecker.FILTERTYPE_NORMAL);
+ spellchecker.InitSpellChecker(editor, false, spellCheckStarted);
+ });
+});
+
+function spellCheckStarted() {
+ var misspelledWord = spellchecker.GetNextMisspelledWord();
+ is(misspelledWord, "missspelled", "first misspelled word expected: missspelled");
+
+ // Without the fix, the next misspelled word was 'onnee', so we check that we don't get it.
+ misspelledWord = spellchecker.GetNextMisspelledWord();
+ isnot(misspelledWord, "onnee", "second misspelled word should not be: onnee");
+
+ spellchecker = "";
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1365383.html b/editor/spellchecker/tests/test_bug1365383.html
new file mode 100644
index 0000000000..d4333b4eb0
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1365383.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1365383
+-->
+<head>
+ <title>Test for Bug 1365383</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=1365383">Mozilla Bug 1365383</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="editor" spellcheck="true"></textarea>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let textarea = document.getElementById("editor");
+ let editor = SpecialPowers.wrap(textarea).editor;
+
+ let spellChecker = SpecialPowers.Cu.createSpellChecker();
+
+ // Callback parameter isn't set
+ spellChecker.InitSpellChecker(editor, false);
+
+ textarea.focus();
+
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ onSpellCheck(textarea, () => {
+ // Callback parameter isn't set
+ spellChecker.UpdateCurrentDictionary();
+
+ var canSpellCheck = spellChecker.canSpellCheck();
+ ok(canSpellCheck, "spellCheck is enabled");
+ SimpleTest.finish();
+ });
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1368544.html b/editor/spellchecker/tests/test_bug1368544.html
new file mode 100644
index 0000000000..4a182615a8
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1368544.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi=id=1368544
+-->
+<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=1368544">Mozilla Bug 1368544</a>
+<div id="display"></div>
+<textarea id=textarea></textarea>
+<pre id="test">
+</pre>
+
+<script class="testbody">
+function hasEmptyTextNode(div) {
+ return div.firstChild.nodeType === Node.TEXT_NODE && div.firstChild.length === 0;
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let textarea = document.getElementById("textarea");
+ let editor = SpecialPowers.wrap(textarea).editor;
+
+ let spellChecker = SpecialPowers.Cu.createSpellChecker();
+ spellChecker.InitSpellChecker(editor, false);
+
+ textarea.focus();
+
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ onSpellCheck(textarea, () => {
+ spellChecker.UpdateCurrentDictionary(() => {
+ textarea.value = "ABC";
+ ok(editor.rootElement.hasChildNodes(),
+ "editor of textarea has child nodes");
+ sendString("D");
+ is(textarea.value, "ABCD", "D is last character");
+ ok(editor.rootElement.hasChildNodes(),
+ "editor of textarea has child nodes");
+ textarea.value = "";
+ ok(editor.rootElement.hasChildNodes(),
+ "editor of textarea has child node even if value is empty");
+
+ sendString("AAA");
+ synthesizeKey("KEY_Backspace", {repeat: 3});
+ is(textarea.value, "", "value is empty");
+ ok(editor.rootElement.hasChildNodes(),
+ "editor of textarea has child node even if value is empty");
+
+ textarea.value = "ABC";
+ SpecialPowers.wrap(textarea).setUserInput("");
+ is(textarea.value, "",
+ "textarea should become empty when setUserInput() is called with empty string");
+ ok(hasEmptyTextNode(editor.rootElement),
+ "editor of textarea should only have an empty text node when user input emulation set the value to empty");
+ todo(editor.rootElement.childNodes.length === 1, "editor of textarea should only have a single child");
+ if (editor.rootElement.childNodes.length > 1) {
+ is(editor.rootElement.childNodes.length, 2, "There should be only one additional <br> node");
+ is(editor.rootElement.lastChild.tagName.toLowerCase(), "br", "The node should be a <br> element node");
+ ok(!SpecialPowers.wrap(editor.rootElement.lastChild).isPaddingForEmptyEditor,
+ "The <br> should not be a padding <br> element");
+ }
+ textarea.value = "ABC";
+ synthesizeKey("KEY_Enter", {repeat: 2});
+ textarea.value = "";
+ ok(editor.rootElement.hasChildNodes(),
+ "editor of textarea has child node even if value is empty");
+
+ sendString("AAA");
+ is(textarea.value, "AAA", "value is AAA");
+ textarea.addEventListener("keyup", (e) => {
+ if (e.key == "Enter") {
+ textarea.value = "";
+ ok(editor.rootElement.hasChildNodes(),
+ "editor of textarea has child node even if value is empty");
+ SimpleTest.finish();
+ }
+ });
+
+ synthesizeKey("KEY_Enter");
+ });
+ });
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1402822.html b/editor/spellchecker/tests/test_bug1402822.html
new file mode 100644
index 0000000000..6380060001
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1402822.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1402822
+-->
+<head>
+ <title>Test for Bug 1402822</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=1402822">Mozilla Bug 1402822</a>
+<p id="display"></p>
+</div>
+
+<textarea id="editor">heute ist ein guter Tag - today is a good day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Ci = SpecialPowers.Ci;
+
+let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+/** Test for Bug 1402822 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(start);
+async function start() {
+ let script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ let dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ let hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ let de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ });
+ is(await script.sendQuery("de_DE-exists"), true,
+ "true expected (de_DE directory should exist)");
+
+ let textarea = document.getElementById("editor");
+ let editor = SpecialPowers.wrap(textarea).editor;
+ textarea.focus();
+
+ maybeOnSpellCheck(textarea, () => {
+ let isc = SpecialPowers.wrap(textarea).editor.getInlineSpellChecker(false);
+ ok(isc, "Inline spell checker should exist after focus and spell check");
+ let spellchecker = isc.spellChecker;
+
+ spellchecker.setCurrentDictionaries(["en-US"]).then(() => {
+ let currentDictionaries = spellchecker.getCurrentDictionaries();
+
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ is(currentDictionaries[0], "en-US", "expected en-US");
+ is(getMisspelledWords(editor), "heuteisteinguter", "some misspelled words expected: heuteisteinguter");
+ spellchecker.setCurrentDictionaries(["en-US", "de-DE"]).then(() => {
+ textarea.blur();
+ textarea.focus();
+ maybeOnSpellCheck(textarea, () => {
+ currentDictionaries = spellchecker.getCurrentDictionaries();
+
+ is(currentDictionaries.length, 2, "expected two dictionaries");
+ is(currentDictionaries[0], "en-US", "expected en-US");
+ is(currentDictionaries[1], "de-DE", "expected de-DE");
+ is(getMisspelledWords(editor), "", "No misspelled words expected");
+
+ spellchecker.setCurrentDictionaries(["de-DE"]).then(() => {
+ textarea.blur();
+ textarea.focus();
+ maybeOnSpellCheck(textarea, async function() {
+ currentDictionaries = spellchecker.getCurrentDictionaries();
+
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ is(currentDictionaries[0], "de-DE", "expected de-DE");
+ is(getMisspelledWords(editor), "todayisagoodday", "some misspelled words expected: todayisagoodday");
+
+ // Remove the fake de_DE dictionary again.
+ await script.sendQuery("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.setCurrentDictionaries([]).then(() => {
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1418629.html b/editor/spellchecker/tests/test_bug1418629.html
new file mode 100644
index 0000000000..442663f0c2
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1418629.html
@@ -0,0 +1,210 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Mozilla bug 1418629</title>
+ <link rel=stylesheet href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/editor/spellchecker/tests/spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1418629">Mozilla Bug 1418629</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<input id="input1" spellcheck="true">
+<textarea id="textarea1"></textarea>
+<div id="edit1" contenteditable=true></div>
+
+<script>
+const { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+SimpleTest.waitForExplicitFinish();
+
+function getEditor(input) {
+ if (input instanceof HTMLInputElement ||
+ input instanceof HTMLTextAreaElement) {
+ return SpecialPowers.wrap(input).editor;
+ }
+
+ return SpecialPowers.wrap(window).docShell.editor;
+}
+
+function resetEditableContent(input) {
+ if (input instanceof HTMLInputElement ||
+ input instanceof HTMLTextAreaElement) {
+ input.value = "";
+ return;
+ }
+ input.innerHTML = "";
+}
+
+async function test_with_single_quote(input) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("d");
+ synthesizeKey("o");
+ synthesizeKey("e");
+ synthesizeKey("s");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(input, resolve); });
+ let editor = getEditor(input);
+ // isSpellingCheckOk is defined in spellcheck.js
+ ok(isSpellingCheckOk(editor, misspeltWords), "no misspelt words");
+
+ synthesizeKey("n");
+ synthesizeKey("\'");
+ is(input.value || input.textContent, "doesn\'", "");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(input, resolve); });
+ // XXX This won't work since mozInlineSpellWordUtil::SplitDOM removes
+ // last single quote unfortunately that is during inputting.
+ // isSpellingCheckOk is defined in spellcheck.js
+ todo_is(isSpellingCheckOk(editor, misspeltWords, true), true,
+ "don't run spellchecker during inputting word");
+
+ synthesizeKey(" ");
+ is(input.value || input.textContent, "doesn\' ", "");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(input, resolve); });
+ misspeltWords.push("doesn");
+ // isSpellingCheckOk is defined in spellcheck.js
+ ok(isSpellingCheckOk(editor, misspeltWords), "should run spellchecker");
+}
+
+async function test_with_twice_characters(input, ch) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("d");
+ synthesizeKey("o");
+ synthesizeKey("e");
+ synthesizeKey("s");
+ synthesizeKey("n");
+ synthesizeKey(ch);
+ synthesizeKey(ch);
+ is(input.value || input.textContent, "doesn" + ch + ch, "");
+
+ // trigger spellchecker
+ synthesizeKey(" ");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(input, resolve); });
+ misspeltWords.push("doesn");
+ let editor = getEditor(input);
+ // isSpellingCheckOk is defined in spellcheck.js
+ ok(isSpellingCheckOk(editor, misspeltWords), "should run spellchecker");
+}
+
+async function test_between_single_quote(input) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("\'");
+ synthesizeKey("t");
+ synthesizeKey("e");
+ synthesizeKey("s");
+ synthesizeKey("t");
+ synthesizeKey("\'");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(input, resolve); });
+ let editor = getEditor(input);
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "don't run spellchecker between single qoute");
+}
+
+async function test_with_email(input) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("@");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey(".");
+ synthesizeKey("c");
+ synthesizeKey("o");
+ synthesizeKey("m");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(input, resolve); });
+ let editor = getEditor(input);
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "don't run spellchecker for email address");
+
+ synthesizeKey(" ");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(input, resolve); });
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "no misspelt words due to email address");
+}
+
+async function test_with_url(input) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("h");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("p");
+ synthesizeKey(":");
+ synthesizeKey("/");
+ synthesizeKey("/");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey(".");
+ synthesizeKey("c");
+ synthesizeKey("o");
+ synthesizeKey("m");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(input, resolve); });
+ let editor = getEditor(input);
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "don't run spellchecker for URL");
+
+ synthesizeKey(" ");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(input, resolve); });
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "no misspelt words due to URL");
+}
+
+SimpleTest.waitForFocus(() => {
+ for (let n of ["input1", "textarea1", "edit1"]) {
+ add_task(test_with_single_quote.bind(null,
+ document.getElementById(n)));
+ add_task(test_with_twice_characters.bind(null,
+ document.getElementById(n),
+ "\'"));
+ add_task(test_with_twice_characters.bind(null,
+ document.getElementById(n),
+ String.fromCharCode(0x2019)));
+ add_task(test_between_single_quote.bind(null,
+ document.getElementById(n)));
+ add_task(test_with_email.bind(null, document.getElementById(n)));
+ add_task(test_with_url.bind(null, document.getElementById(n)));
+ }
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1497480.html b/editor/spellchecker/tests/test_bug1497480.html
new file mode 100644
index 0000000000..a7063271f4
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1497480.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1497480
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1497480</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="spellcheck.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=1497480">Mozilla Bug 1497480</a>
+<p id="display"></p>
+
+<div id="outOfTarget" contenteditable>Bug 1497480</div>
+<div id="light"></div>
+
+<script>
+
+/** Test for Bug 1497480 **/
+let gMisspeltWords = [];
+let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+const template = document.createElement("template");
+template.innerHTML = `<div id="target" contenteditable>Test</div>`;
+
+let shadow = document.getElementById("light").attachShadow({mode: "closed"});
+shadow.appendChild(template.content.cloneNode(true));
+
+let target = shadow.getElementById("target");
+let outOfTarget = document.getElementById("outOfTarget");
+
+function getEditor() {
+ var win = window;
+ var editingSession = SpecialPowers.wrap(win).docShell.editingSession;
+ return editingSession.getEditorForWindow(win);
+}
+
+// Wait for the page to be ready for testing
+add_task(async function() {
+ await new Promise((resolve) => {
+ SimpleTest.waitForFocus(() => {
+ SimpleTest.executeSoon(resolve);
+ }, window);
+ });
+
+ // Wait for first full spell-checking.
+ synthesizeMouseAtCenter(outOfTarget, {}, window);
+ await new Promise((resolve) => {
+ maybeOnSpellCheck(outOfTarget, function() {
+ resolve();
+ });
+ });
+});
+
+// Should perform spell-checking when anchor navigates away from ShadowDOM.
+add_task(async function() {
+ synthesizeMouseAtCenter(target, {}, window);
+ sendString(" spellechek");
+ gMisspeltWords.push("spellechek");
+ synthesizeMouseAtCenter(outOfTarget, {}, window);
+ await new Promise((resolve) => {
+ maybeOnSpellCheck(target, function() {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "Spell-checking should be performed when anchor navigates away from ShadowDOM");
+ SimpleTest.executeSoon(resolve);
+ });
+ });
+});
+
+// Should perform spell-checking when pressing enter in contenteditable in ShadowDOM.
+add_task(async function() {
+ synthesizeMouseAtCenter(target, {}, window);
+ sendString(" spellechck");
+ gMisspeltWords.push("spellechck");
+ synthesizeKey("KEY_Enter", {}, window);
+ await new Promise((resolve) => {
+ maybeOnSpellCheck(target, function() {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "Spell-checking should be performed when pressing enter in contenteditable in ShadowDOM");
+ SimpleTest.executeSoon(resolve);
+ });
+ });
+});
+
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1602526.html b/editor/spellchecker/tests/test_bug1602526.html
new file mode 100644
index 0000000000..e8c2886d9d
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1602526.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Mozilla bug 1602526</title>
+ <link rel=stylesheet href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/editor/spellchecker/tests/spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1602526">Mozilla Bug 1602526</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="contenteditable" contenteditable=true>kkkk&#xf6;kkkk</div>
+
+<script>
+const { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+SimpleTest.waitForExplicitFinish();
+
+function getEditor() {
+ return SpecialPowers.wrap(window).docShell.editor;
+}
+
+SimpleTest.waitForFocus(async () => {
+ let contenteditable = document.getElementById("contenteditable");
+ let misspeltWords = [];
+ misspeltWords.push("kkkk\u00f6kkkk");
+
+ contenteditable.focus();
+ window.getSelection().collapse(contenteditable.firstChild, contenteditable.firstChild.length);
+
+ synthesizeKey(" ");
+
+ // Run spell checker
+ await new Promise((resolve) => { maybeOnSpellCheck(contenteditable, resolve); });
+
+ synthesizeKey("a");
+ synthesizeKey("a");
+ synthesizeKey("a");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(contenteditable, resolve); });
+ let editor = getEditor();
+ // isSpellingCheckOk is defined in spellcheck.js
+ // eslint-disable-next-line no-undef
+ ok(isSpellingCheckOk(editor, misspeltWords), "correct word is seleced as misspell");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1761273.html b/editor/spellchecker/tests/test_bug1761273.html
new file mode 100644
index 0000000000..543e56e810
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1761273.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1761273
+-->
+<head>
+ <title>Test for Bug 1761273</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=1761273">Mozilla Bug 1761273</a>
+<p id="display"></p>
+</div>
+
+<textarea id="editor" lang="en-US">heute ist ein guter Tag - today is a good day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Ci = SpecialPowers.Ci;
+
+let {
+ getDictionaryContentPref,
+ onSpellCheck,
+} = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+/** Test for Bug 1402822 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(start);
+async function start() {
+ let script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ let dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ let hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ let de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ });
+ is(await script.sendQuery("de_DE-exists"), true,
+ "true expected (de_DE directory should exist)");
+
+ let textarea = document.getElementById("editor");
+ textarea.focus();
+
+ onSpellCheck(textarea, async () => {
+ let isc = SpecialPowers.wrap(textarea).editor.getInlineSpellChecker(true);
+ ok(isc, "Inline spell checker should exist after focus and spell check");
+ let spellchecker = isc.spellChecker;
+
+ // Setting the language to the language of the texteditor should not set the
+ // content preference.
+ await spellchecker.setCurrentDictionaries(["en-US"]);
+ let dictionaryContentPref = await getDictionaryContentPref();
+ is(dictionaryContentPref, "", "Content pref should be empty");
+
+ await spellchecker.setCurrentDictionaries(["en-US", "de-DE"]);
+ dictionaryContentPref = await getDictionaryContentPref();
+ is(dictionaryContentPref, "en-US,de-DE,", "Content pref should be en-US,de-DE,");
+
+ await spellchecker.setCurrentDictionaries(["de-DE"]);
+ dictionaryContentPref = await getDictionaryContentPref();
+ is(dictionaryContentPref, "de-DE,", "Content pref should be de-DE,");
+
+ // Remove the fake de_DE dictionary again.
+ await script.sendQuery("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ await spellchecker.setCurrentDictionaries([]);
+ dictionaryContentPref = await getDictionaryContentPref();
+ is(dictionaryContentPref, "", "Content pref should be empty");
+
+ SimpleTest.finish();
+ });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1773802.html b/editor/spellchecker/tests/test_bug1773802.html
new file mode 100644
index 0000000000..e31c819761
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1773802.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1773802
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1773802</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=1773802">Mozilla Bug 1773802</a>
+<p id="display"></p>
+</div>
+
+<textarea id="editor">correct правильный, incarrect непровильный</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Ci = SpecialPowers.Ci;
+
+let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+/** Test for Bug 1773802 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(start);
+async function start() {
+ let script = SpecialPowers.loadChromeScript(() => {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ let dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ let hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install ru-RU dictionary.
+ let ru_RU = dir.clone();
+ ru_RU.append("ru-RU");
+ hunspell.addDirectory(ru_RU);
+
+ addMessageListener("destroy", () => hunspell.removeDirectory(ru_RU));
+ addMessageListener("ru_RU-exists", () => ru_RU.exists());
+ });
+ is(await script.sendQuery("ru_RU-exists"), true,
+ "true expected (ru_RU directory should exist)");
+
+ let textarea = document.getElementById("editor");
+ let editor = SpecialPowers.wrap(textarea).editor;
+ textarea.focus();
+
+ maybeOnSpellCheck(textarea, () => {
+ let isc = SpecialPowers.wrap(textarea).editor.getInlineSpellChecker(false);
+ ok(isc, "Inline spell checker should exist after focus and spell check");
+ let spellchecker = isc.spellChecker;
+
+ spellchecker.setCurrentDictionaries(["en-US", "ru-RU"]).then(async () => {
+ textarea.blur();
+ textarea.focus();
+ maybeOnSpellCheck(textarea, async function() {
+ let currentDictionaries = spellchecker.getCurrentDictionaries();
+
+ is(currentDictionaries.length, 2, "expected two dictionaries");
+ is(currentDictionaries[0], "en-US", "expected en-US");
+ is(currentDictionaries[1], "ru-RU", "expected ru-RU");
+ is(getMisspelledWords(editor), "incarrectнепровильный", "some misspelled words expected: incarrect непровильный");
+
+ // Remove the fake ru_RU dictionary again.
+ await script.sendQuery("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.setCurrentDictionaries([]).then(() => {
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1837268.html b/editor/spellchecker/tests/test_bug1837268.html
new file mode 100644
index 0000000000..1467a328b5
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1837268.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Mozilla bug 1837268</title>
+ <link rel=stylesheet href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/editor/spellchecker/tests/spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1837268">Mozilla Bug 1837268</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="contenteditable" contenteditable=true>aabbcc</div>
+
+<script>
+const { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs"
+);
+
+SimpleTest.waitForExplicitFinish();
+
+function getEditor() {
+ return SpecialPowers.wrap(window).docShell.editor;
+}
+
+SimpleTest.waitForFocus(async () => {
+ let contenteditable = document.getElementById("contenteditable");
+ contenteditable.addEventListener("beforeinput", (ev) => {
+ ev.preventDefault();
+ let text = contenteditable.textContent;
+ const sel = window.getSelection();
+ let offset = sel.anchorOffset;
+ switch (ev.inputType) {
+ case "insertText":
+ text = text.substring(0, offset) + ev.data + text.substring(offset);
+ offset += 1;
+ break;
+ case "deleteContentBackward":
+ text = text.substring(0, offset - 1) + text.substring(offset);
+ offset -= 1;
+ break;
+ default:
+ return;
+ }
+ if (contenteditable.firstChild) {
+ contenteditable.firstChild.nodeValue = text;
+ } else {
+ contenteditable.textContent = text;
+ }
+ sel.collapse(contenteditable.firstChild ?? contenteditable, offset);
+ });
+
+ let misspelledWords = [];
+ misspelledWords.push("aabbc"); // One c removed.
+
+ contenteditable.focus();
+ window.getSelection().collapse(contenteditable.firstChild, contenteditable.firstChild.length);
+
+ // Run spell checker
+ await new Promise((resolve) => { maybeOnSpellCheck(contenteditable, resolve); });
+
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey(" ");
+
+ await new Promise((resolve) => { maybeOnSpellCheck(contenteditable, resolve); });
+ let editor = getEditor();
+ // isSpellingCheckOk is defined in spellcheck.js
+ // eslint-disable-next-line no-undef
+ ok(isSpellingCheckOk(editor, misspelledWords), "correct word is selected as misspelled");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug338427.html b/editor/spellchecker/tests/test_bug338427.html
new file mode 100644
index 0000000000..dd869ffc11
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug338427.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=338427
+-->
+<head>
+ <title>Test for Bug 338427</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=338427">Mozilla Bug 338427</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="editor" lang="testing-XX" spellcheck="true"></textarea>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 338427 **/
+function init() {
+ var { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ var textarea = document.getElementById("editor");
+ var editor = SpecialPowers.wrap(textarea).editor;
+ var spellchecker = editor.getInlineSpellChecker(true);
+ spellchecker.enableRealTimeSpell = true;
+ textarea.focus();
+
+ onSpellCheck(textarea, function() {
+ var list = spellchecker.spellChecker.GetDictionaryList();
+ ok(!!list.length, "At least one dictionary should be present");
+
+ var lang = list[0];
+ spellchecker.spellChecker.setCurrentDictionaries([lang]).then(() => {
+ onSpellCheck(textarea, function() {
+ try {
+ var dictionaries =
+ spellchecker.spellChecker.getCurrentDictionaries();
+ } catch (e) {}
+ is(dictionaries.length, 1, "Expected one dictionary");
+ is(dictionaries[0], lang, "Unexpected spell check dictionary");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.spellChecker.setCurrentDictionaries([]).then(() => {
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(init);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug366682.html b/editor/spellchecker/tests/test_bug366682.html
new file mode 100644
index 0000000000..ac38bcf9d2
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug366682.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=366682
+-->
+<head>
+ <title>Test for Bug 366682</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=366682">Mozilla Bug 366682</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 366682 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords;
+
+function getEdit() {
+ return document.getElementById("edit");
+}
+
+function editDoc() {
+ return getEdit().contentDocument;
+}
+
+function getEditor() {
+ var win = editDoc().defaultView;
+ var editingSession = SpecialPowers.wrap(win).docShell.editingSession;
+ return editingSession.getEditorForWindow(win);
+}
+
+function runTest() {
+ editDoc().body.innerHTML = "<div>errror and an other errror</div>";
+ gMisspeltWords = ["errror", "errror"];
+ editDoc().designMode = "on";
+ editDoc().defaultView.focus();
+
+ const { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ maybeOnSpellCheck(editDoc().documentElement, evalTest);
+}
+
+function evalTest() {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings accounted for.");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+
+<iframe id="edit" width="200" height="100" src="about:blank"></iframe>
+
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug432225.html b/editor/spellchecker/tests/test_bug432225.html
new file mode 100644
index 0000000000..2168429613
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug432225.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=432225
+-->
+<head>
+ <title>Test for Bug 432225</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 src="spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=432225">Mozilla Bug 432225</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 432225 **/
+
+let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords = [];
+
+function getEdit() {
+ return document.getElementById("edit");
+}
+
+function editDoc() {
+ return getEdit().contentDocument;
+}
+
+function getEditor() {
+ var win = editDoc().defaultView;
+ var editingSession = SpecialPowers.wrap(win).docShell.editingSession;
+ return editingSession.getEditorForWindow(win);
+}
+
+function runTest() {
+ editDoc().designMode = "on";
+ setTimeout(function() { addWords(100); }, 0);
+}
+
+function addWords(aLimit) {
+ if (aLimit == 0) {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings accounted for.");
+ SimpleTest.finish();
+ return;
+ }
+ getEdit().focus();
+ sendString("aa OK ");
+ gMisspeltWords.push("aa");
+ maybeOnSpellCheck(editDoc(), function() {
+ addWords(aLimit - 1);
+ });
+}
+</script>
+</pre>
+
+<iframe id="edit" width="200" height="100" src="about:blank"></iframe>
+
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug484181.html b/editor/spellchecker/tests/test_bug484181.html
new file mode 100644
index 0000000000..30f0b359c8
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug484181.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=484181
+-->
+<head>
+ <title>Test for Bug 484181</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 src="spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=484181">Mozilla Bug 484181</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 484181 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords;
+
+function getEditor() {
+ var win = window;
+ var editingSession = SpecialPowers.wrap(win).docShell.editingSession;
+ return editingSession.getEditorForWindow(win);
+}
+
+function append(str) {
+ var edit = document.getElementById("edit");
+ var editor = getEditor();
+ var sel = editor.selection;
+ sel.selectAllChildren(edit);
+ sel.collapseToEnd();
+ sendString(str);
+}
+
+function runTest() {
+ gMisspeltWords = ["haz", "cheezburger"];
+ var edit = document.getElementById("edit");
+ edit.focus();
+
+ const { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ maybeOnSpellCheck(edit, function() {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings before editing are accounted for.");
+
+ append(" becaz I'm a lulcat!");
+ maybeOnSpellCheck(edit, function() {
+ gMisspeltWords.push("becaz");
+ gMisspeltWords.push("lulcat");
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings after typing are accounted for.");
+
+ SimpleTest.finish();
+ });
+ });
+}
+</script>
+</pre>
+
+<div><div></div><div id="edit" contenteditable="true">I can haz cheezburger</div></div>
+
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug596333.html b/editor/spellchecker/tests/test_bug596333.html
new file mode 100644
index 0000000000..ce6714565b
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug596333.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596333
+-->
+<head>
+ <title>Test for Bug 596333</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 src="spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596333">Mozilla Bug 596333</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 596333 **/
+const Ci = SpecialPowers.Ci;
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+var gMisspeltWords;
+var onSpellCheck;
+
+function getEditor() {
+ return SpecialPowers.wrap(document.getElementById("edit")).editor;
+}
+
+function append(str) {
+ var edit = document.getElementById("edit");
+ edit.focus();
+ edit.selectionStart = edit.selectionEnd = edit.value.length;
+ sendString(str);
+}
+
+function getLoadContext() {
+ return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function paste(str) {
+ var Cc = SpecialPowers.Cc;
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ var s = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ s.data = str;
+ trans.setTransferData("text/plain", s);
+
+ let beforeInputEvent = null;
+ let inputEvent = null;
+ window.addEventListener("beforeinput", aEvent => { beforeInputEvent = aEvent; }, {once: true});
+ window.addEventListener("input", aEvent => { inputEvent = aEvent; }, {once: true});
+ getEditor().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, str, `data of "beforeinput" event should be "${str}"`);
+ is(beforeInputEvent.dataTransfer, null, 'dataTransfer of "beforeinput" event should be null on <textarea>');
+ is(beforeInputEvent.getTargetRanges().length, 0, 'getTargetRanges() of "beforeinput" event should return empty array on <textarea>');
+ }
+ is(inputEvent.type, "input", '"input" event should be fired');
+ is(inputEvent.inputType, "insertFromPaste", '"inputType of "input" event should be "insertFromPaste"');
+ is(inputEvent.data, str, `data of "input" event should be "${str}"`);
+ is(inputEvent.dataTransfer, null, 'dataTransfer of "input" event should be null on <textarea>');
+ is(inputEvent.getTargetRanges().length, 0, 'getTargetRanges() of "input" event should return empty array on <textarea>');
+}
+
+function runOnFocus() {
+ var edit = document.getElementById("edit");
+
+ gMisspeltWords = ["haz", "cheezburger"];
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings before editing are accounted for.");
+ append(" becaz I'm a lulcat!");
+ onSpellCheck(edit, function() {
+ gMisspeltWords.push("becaz");
+ gMisspeltWords.push("lulcat");
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings after typing are accounted for.");
+
+ // Now, type an invalid word, and instead of hitting "space" at the end, just blur
+ // the textarea and see if the spell check after the blur event catches it.
+ append(" workd");
+ edit.blur();
+ onSpellCheck(edit, function() {
+ gMisspeltWords.push("workd");
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "All misspellings after blur are accounted for.");
+
+ // Also, test the case when we're entering the first word in a textarea
+ gMisspeltWords = ["workd"];
+ edit.value = "";
+ append("workd ");
+ onSpellCheck(edit, function() {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "Misspelling in the first entered word is accounted for.");
+
+ // Make sure that pasting would also trigger spell checking for the previous word
+ gMisspeltWords = ["workd"];
+ edit.value = "";
+ append("workd");
+ paste(" x");
+ onSpellCheck(edit, function() {
+ ok(isSpellingCheckOk(getEditor(), gMisspeltWords),
+ "Misspelling is accounted for after pasting.");
+
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+}
+
+function runTest() {
+ var edit = document.getElementById("edit");
+ edit.focus();
+
+ onSpellCheck = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ ).onSpellCheck;
+ onSpellCheck(edit, runOnFocus);
+}
+</script>
+</pre>
+
+<textarea id="edit">I can haz cheezburger</textarea>
+
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug636465.html b/editor/spellchecker/tests/test_bug636465.html
new file mode 100644
index 0000000000..97252d92df
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug636465.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<title>Mozilla bug 636465</title>
+<link rel=stylesheet href="/tests/SimpleTest/test.css">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=636465"
+ target="_blank">Mozilla Bug 636465</a>
+<input id="x" value="foobarbaz" spellcheck="true" style="background-color: transparent; border: transparent;">
+<script>
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ var x = document.getElementById("x");
+ x.focus();
+ onSpellCheck(x, function() {
+ x.blur();
+ var spellCheckTrue = snapshotWindow(window);
+ x.setAttribute("spellcheck", "false");
+ var spellCheckFalse = snapshotWindow(window);
+ x.setAttribute("spellcheck", "true");
+ x.focus();
+ onSpellCheck(x, function() {
+ x.blur();
+ var spellCheckTrueAgain = snapshotWindow(window);
+ x.removeAttribute("spellcheck");
+ var spellCheckNone = snapshotWindow(window);
+ var ret = compareSnapshots(spellCheckTrue, spellCheckFalse, false)[0];
+ ok(ret,
+ "Setting the spellcheck attribute to false should work");
+ if (!ret) {
+ ok(false, "\nspellCheckTrue: " + spellCheckTrue.toDataURL() + "\nspellCheckFalse: " + spellCheckFalse.toDataURL());
+ }
+ ret = compareSnapshots(spellCheckTrue, spellCheckTrueAgain, true)[0];
+ ok(ret,
+ "Setting the spellcheck attribute back to true should work");
+ if (!ret) {
+ ok(false, "\nspellCheckTrue: " + spellCheckTrue.toDataURL() + "\nspellCheckTrueAgain: " + spellCheckTrueAgain.toDataURL());
+ }
+ ret = compareSnapshots(spellCheckNone, spellCheckFalse, true)[0];
+ ok(ret,
+ "Unsetting the spellcheck attribute should work");
+ if (!ret) {
+ ok(false, "\spellCheckNone: " + spellCheckNone.toDataURL() + "\nspellCheckFalse: " + spellCheckFalse.toDataURL());
+ }
+ SimpleTest.finish();
+ });
+ });
+}
+addLoadEvent(runTest);
+</script>
diff --git a/editor/spellchecker/tests/test_bug678842.html b/editor/spellchecker/tests/test_bug678842.html
new file mode 100644
index 0000000000..f5f190ab0f
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug678842.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=678842
+-->
+<head>
+ <title>Test for Bug 678842</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=678842">Mozilla Bug 678842</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 678842 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+// load a subframe containing an editor with a defined unknown lang. At first
+// load, it will set dictionary to en-US. At second load, it will return current
+// dictionary. So, we can check, dictionary is correctly remembered between
+// loads.
+
+var firstLoad = true;
+var script;
+
+var loadListener = async function(evt) {
+ if (firstLoad) {
+ script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install en-GB dictionary.
+ let en_GB = dir.clone();
+ en_GB.append("en-GB");
+ hunspell.addDirectory(en_GB);
+
+ addMessageListener("en_GB-exists", () => en_GB.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(en_GB));
+ });
+ is(await script.sendQuery("en_GB-exists"), true,
+ "true expected (en-GB directory should exist)");
+ }
+
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById("textarea");
+ var editor = SpecialPowers.wrap(elem).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ onSpellCheck(elem, async function() {
+ let spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictionaries = spellchecker.getCurrentDictionaries();
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ let currentDictionary = currentDictionaries[0];
+
+ if (firstLoad) {
+ firstLoad = false;
+
+ // First time around, the dictionary defaults to the locale.
+ is(currentDictionary, "en-US", "unexpected lang " + currentDictionary + " instead of en-US");
+
+ // Select en-GB.
+ spellchecker.setCurrentDictionaries(["en-GB"]).then(() => {
+ content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug678842_subframe.html?firstload=false";
+ });
+ } else {
+ is(currentDictionary, "en-GB", "unexpected lang " + currentDictionary + " instead of en-GB");
+ content.removeEventListener("load", loadListener);
+
+ // Remove the fake en-GB dictionary again, since it's otherwise picked up by later tests.
+ await script.sendQuery("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.setCurrentDictionaries([]).then( () => {
+ SimpleTest.finish();
+ });
+ }
+ });
+};
+
+content.addEventListener("load", loadListener);
+
+content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug678842_subframe.html?firstload=true";
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug697981.html b/editor/spellchecker/tests/test_bug697981.html
new file mode 100644
index 0000000000..044f82048b
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug697981.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=697981
+-->
+<head>
+ <title>Test for Bug 697981</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=697981">Mozilla Bug 697981</a>
+<p id="display"></p>
+</div>
+
+<textarea id="de-DE" lang="de-DE" onfocus="deFocus()">German heute ist ein guter Tag</textarea>
+<textarea id="en-US" lang="en-US" onfocus="enFocus()">Nogoodword today is a nice day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(SpecialPowers.Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var script;
+
+var { maybeOnSpellCheck, onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+/** Test for Bug 697981 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ var de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ });
+ is(await script.sendQuery("de_DE-exists"), true,
+ "true expected (de_DE directory should exist)");
+
+ document.getElementById("de-DE").focus();
+});
+
+function deFocus() {
+ elem_de = document.getElementById("de-DE");
+ editor_de = SpecialPowers.wrap(elem_de).editor;
+ editor_de.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor_de.getInlineSpellChecker(true);
+
+ maybeOnSpellCheck(elem_de, function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictionaries = spellchecker.getCurrentDictionaries();
+ } catch (e) {}
+
+ // Check that the German dictionary is loaded and that the spell check has worked.
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ is(currentDictionaries[0], "de-DE", "expected de-DE");
+ is(getMisspelledWords(editor_de), "German", "one misspelled word expected: German");
+
+ // Now focus the other textarea, which requires English spelling.
+ document.getElementById("en-US").focus();
+ });
+}
+
+function enFocus() {
+ var elem_en = document.getElementById("en-US");
+ var editor_en = SpecialPowers.wrap(elem_en).editor;
+ editor_en.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor_en.getInlineSpellChecker(true);
+
+ onSpellCheck(elem_en, async function() {
+ let spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictionaries = spellchecker.getCurrentDictionaries();
+
+ // Check that the English dictionary is loaded and that the spell check has worked.
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ is(currentDictionaries[0], "en-US", "expected en-US");
+ is(getMisspelledWords(editor_en), "Nogoodword", "one misspelled word expected: Nogoodword");
+
+ // So far all was boring. The important thing is whether the spell check result
+ // in the de-DE editor is still the same. After losing focus, no spell check
+ // updates should take place there.
+ is(getMisspelledWords(editor_de), "German", "one misspelled word expected: German");
+
+ // Remove the fake de_DE dictionary again.
+ await script.sendQuery("destroy");
+
+ // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
+ elem_de.onfocus = null;
+ elem_de.blur();
+ elem_de.focus();
+
+ // After removal, the de_DE editor should refresh the spelling with en-US.
+ maybeOnSpellCheck(elem_de, function() {
+ spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ currentDictionaries = spellchecker.getCurrentDictionaries();
+ } catch (e) {}
+
+ // Check that the default English dictionary is loaded and that the spell check has worked.
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ is(currentDictionaries[0], "en-US", "expected en-US");
+ // eslint-disable-next-line no-useless-concat
+ is(getMisspelledWords(editor_de), "heute" + "ist" + "ein" + "guter",
+ "some misspelled words expected: heute ist ein guter");
+
+ SimpleTest.finish();
+ });
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug717433.html b/editor/spellchecker/tests/test_bug717433.html
new file mode 100644
index 0000000000..2f09cb70fd
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug717433.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=717433
+-->
+<head>
+ <title>Test for Bug 717433</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=717433">Mozilla Bug 717433</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 717433 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+// Load a subframe containing an editor with language "en". At first
+// load, it will set the dictionary to en-GB or en-US. We set the other one.
+// At second load, it will return the current dictionary. We can check that the
+// dictionary is correctly remembered between loads.
+
+var firstLoad = true;
+var expected = "";
+var script;
+
+var loadListener = async function(evt) {
+ if (firstLoad) {
+ script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install en-GB dictionary.
+ var en_GB = dir.clone();
+ en_GB.append("en-GB");
+ hunspell.addDirectory(en_GB);
+
+ addMessageListener("en_GB-exists", () => en_GB.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(en_GB));
+ });
+ is(await script.sendQuery("en_GB-exists"), true,
+ "true expected (en-GB directory should exist)");
+ }
+
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById("textarea");
+ var editor = SpecialPowers.wrap(elem).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ onSpellCheck(elem, async function() {
+ let spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictionaries = spellchecker.getCurrentDictionaries();
+
+ is(currentDictionaries.length, 1, "expected one dictionary");
+ let currentDictionary = currentDictionaries[0];
+
+ if (firstLoad) {
+ firstLoad = false;
+
+ // First time around, we get a random dictionary based on the language "en".
+ if (currentDictionary == "en-GB") {
+ expected = "en-US";
+ } else if (currentDictionary == "en-US") {
+ expected = "en-GB";
+ } else {
+ is(true, false, "Neither en-US nor en-GB are current");
+ }
+ spellchecker.setCurrentDictionaries([expected]).then(() => {
+ content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug717433_subframe.html?firstload=false";});
+ } else {
+ is(currentDictionary, expected, expected + " expected");
+ content.removeEventListener("load", loadListener);
+
+ // Remove the fake en-GB dictionary again, since it's otherwise picked up by later tests.
+ await script.sendQuery("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.setCurrentDictionaries([]).then(() => {
+ SimpleTest.finish();
+ });
+ }
+ });
+};
+
+content.addEventListener("load", loadListener);
+
+content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug717433_subframe.html?firstload=true";
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_multiple_content_languages.html b/editor/spellchecker/tests/test_multiple_content_languages.html
new file mode 100644
index 0000000000..c3357dec91
--- /dev/null
+++ b/editor/spellchecker/tests/test_multiple_content_languages.html
@@ -0,0 +1,176 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for multiple Content-Language values</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+<pre id="test">
+<script class="testbody">
+
+/** Test for multiple Content-Language values **/
+/** Visit the elements defined above and check the dictionaries we got **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+
+var tests = [
+ // text area, value of spellchecker.dictionary, result.
+ // Result: Document language.
+ [ "none", "", ["en-US", "en-GB"] ],
+
+ // Result: Element language.
+ [ "en-GB", "", ["en-GB"] ],
+ // Result: Random en-* or en-US (if application locale is en-US).
+ [ "en-ZA-not-avail", "", ["*"] ],
+ [ "en", "", ["*"] ],
+ // Result: Locale.
+ [ "ko-not-avail", "", ["en-US"] ],
+
+ // Result: Document language, plus preference value in all cases.
+ [ "none", "en-AU", ["en-US", "en-GB", "en-AU"] ],
+ [ "en-ZA-not-avail", "en-AU", ["en-AU"] ],
+ [ "ko-not-avail", "en-AU", ["en-AU"] ],
+
+ // Result: Document language, plus first matching preference language.
+ [ "none", "en-AU,en-US", ["en-US", "en-GB", "en-AU"] ],
+ // Result: First matching preference language.
+ [ "en-ZA-not-avail", "en-AU,en-US", ["en-AU"] ],
+ // Result: Fall back to preference languages.
+ [ "ko-not-avail", "en-AU,en-US", ["en-AU", "en-US"] ],
+
+ // Result: Random en-*.
+ [ "en-ZA-not-avail", "de-DE", ["*"] ],
+ // Result: Preference value.
+ [ "ko-not-avail", "de-DE", ["de-DE"] ],
+ ];
+
+var loadCount = 0;
+var retrying = false;
+var script;
+
+var loadListener = async function(evt) {
+ if (loadCount == 0) {
+ script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install en-GB, en-AU and de-DE dictionaries.
+ var en_GB = dir.clone();
+ var en_AU = dir.clone();
+ var de_DE = dir.clone();
+ en_GB.append("en-GB");
+ en_AU.append("en-AU");
+ de_DE.append("de-DE");
+ hunspell.addDirectory(en_GB);
+ hunspell.addDirectory(en_AU);
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("check-existence",
+ () => [en_GB.exists(), en_AU.exists(),
+ de_DE.exists()]);
+ addMessageListener("destroy", () => {
+ hunspell.removeDirectory(en_GB);
+ hunspell.removeDirectory(en_AU);
+ hunspell.removeDirectory(de_DE);
+ });
+ });
+ var existenceChecks = await script.sendQuery("check-existence");
+ is(existenceChecks[0], true, "true expected (en-GB directory should exist)");
+ is(existenceChecks[1], true, "true expected (en-AU directory should exist)");
+ is(existenceChecks[2], true, "true expected (de-DE directory should exist)");
+ }
+
+ SpecialPowers.pushPrefEnv({set: [["spellchecker.dictionary", tests[loadCount][1]]]},
+ function() { continueTest(evt); });
+};
+
+function continueTest(evt) {
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById(tests[loadCount][0]);
+ var editor = SpecialPowers.wrap(elem).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+ const is_en_US = SpecialPowers.Services.locale.appLocaleAsBCP47 == "en-US";
+
+ const { onSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ onSpellCheck(elem, async function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictionaries;
+ try {
+ currentDictionaries = spellchecker.getCurrentDictionaries();
+ } catch (e) {}
+
+ if (!currentDictionaries && !retrying) {
+ // It's possible for an asynchronous font-list update to cause a reflow
+ // that disrupts the async spell-check and results in not getting a
+ // current dictionary here; if that happens, we retry the same testcase
+ // by reloading the iframe without bumping loadCount.
+ info(`No current dictionary: retrying testcase ${loadCount}`);
+ retrying = true;
+ } else {
+ let expectedDictionaries = tests[loadCount][2];
+ let dictionaryArray = Array.from(currentDictionaries);
+ is(
+ dictionaryArray.length,
+ expectedDictionaries.length,
+ "Expected matching dictionary count"
+ );
+ if (expectedDictionaries[0] != "*") {
+ ok(
+ dictionaryArray.every(dict => expectedDictionaries.includes(dict)),
+ "active dictionaries should match expectation"
+ );
+ } else if (is_en_US && tests[loadCount][0].startsWith("en")) {
+ // Current application locale is en-US and content lang is en or
+ // en-unknown, so we should use en-US dictionary as default.
+ is(
+ dictionaryArray[0],
+ "en-US",
+ "expected en-US that is application locale"
+ );
+ } else {
+ let dict = dictionaryArray[0];
+ var gotEn = (dict == "en-GB" || dict == "en-AU" || dict == "en-US");
+ is(gotEn, true, "expected en-AU or en-GB or en-US");
+ }
+
+ loadCount++;
+ retrying = false;
+ }
+
+ if (loadCount < tests.length) {
+ // Load the iframe again.
+ content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/multiple_content_languages_subframe.html?firstload=false";
+ } else {
+ // Remove the fake dictionaries again, since it's otherwise picked up by later tests.
+ await script.sendQuery("destroy");
+
+ SimpleTest.finish();
+ }
+ });
+}
+
+content.addEventListener("load", loadListener);
+
+content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/multiple_content_languages_subframe.html?firstload=true";
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_nsIEditorSpellCheck_ReplaceWord.html b/editor/spellchecker/tests/test_nsIEditorSpellCheck_ReplaceWord.html
new file mode 100644
index 0000000000..6088b97e3e
--- /dev/null
+++ b/editor/spellchecker/tests/test_nsIEditorSpellCheck_ReplaceWord.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for nsIEditorSpellCheck.ReplaceWord()</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<div contenteditable spellcheck="true" lang="en-US"></div>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+ const editor = document.querySelector("div[contenteditable]");
+ async function replaceWord(aMisspelledWord, aCorrectWord, aReplaceAll) {
+ const editorObj = SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
+ const inlineSpellChecker = editorObj.getInlineSpellChecker(true);
+ await new Promise(resolve => maybeOnSpellCheck(editor, resolve));
+ const editorSpellCheck = inlineSpellChecker.spellChecker;
+ editorObj.beginTransaction();
+ try {
+ editorSpellCheck.ReplaceWord(aMisspelledWord, aCorrectWord, aReplaceAll);
+ } catch (e) {
+ ok(false, `Unexpected exception: ${e.message}`);
+ }
+ editorObj.endTransaction();
+ editorSpellCheck.GetNextMisspelledWord();
+ }
+
+ async function testReplaceAllMisspelledWords(aCorrectWord) {
+ editor.innerHTML = "<p>def abc def<br>abc def abc</p><p>abc def abc<br>def abc def</p>";
+ editor.focus();
+ editor.getBoundingClientRect();
+ await replaceWord("abc", aCorrectWord, true);
+ is(
+ editor.innerHTML,
+ `<p>def ${aCorrectWord} def<br>${aCorrectWord} def ${aCorrectWord}</p><p>${aCorrectWord} def ${aCorrectWord}<br>def ${aCorrectWord} def</p>`,
+ `nsIEditorSpellCheck.ReplaceWord(..., true) should replace all misspelled words with ${
+ (() => {
+ if (aCorrectWord.length > "abc".length) {
+ return "longer";
+ }
+ return aCorrectWord.length < "abc".length ? "shorter" : "same length"
+ })()
+ } correct word`
+ );
+ editor.blur();
+ editor.getBoundingClientRect();
+ }
+ await testReplaceAllMisspelledWords("ABC");
+ await testReplaceAllMisspelledWords("ABC!");
+ await testReplaceAllMisspelledWords("AB");
+
+ // TODO: Add tests for not all replacing cases.
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_spellcheck_after_edit.html b/editor/spellchecker/tests/test_spellcheck_after_edit.html
new file mode 100644
index 0000000000..e4fa76d2e4
--- /dev/null
+++ b/editor/spellchecker/tests/test_spellcheck_after_edit.html
@@ -0,0 +1,198 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Spellcheck result after edit</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+function waitForTick() {
+ return new Promise(resolve =>
+ SimpleTest.executeSoon(
+ () => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)
+ )
+ )
+ );
+}
+
+async function waitForOnSpellCheck(
+ aSpellCheckSelection,
+ aEditingHost,
+ aWaitForNumberOfMisspelledWords,
+ aWhen
+) {
+ info(`Waiting for onSpellCheck (${aWhen})...`);
+ for (let retry = 0; retry < 100; retry++) {
+ await waitForTick();
+ await new Promise(resolve => maybeOnSpellCheck(aEditingHost, resolve));
+ if (aWaitForNumberOfMisspelledWords === 0) {
+ if (aSpellCheckSelection.rangeCount === 0) {
+ break;
+ }
+ } else if (aSpellCheckSelection.rangeCount >= aWaitForNumberOfMisspelledWords) {
+ break;
+ }
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ /**
+ * test object should have:
+ * init function
+ * @param normalSel The normal selection for the editing host
+ * @param editingHost The editing host of the editor
+ * @return Number of misspelled word in the editor
+ *
+ * run function
+ * @param editingHost The editing host of the editor
+ * @return Expected number of misspelled word in the editor
+ *
+ * check function
+ * @param spellCheckSel The spellcheck selection for the editing host
+ * @param editingHost The editing host of the editor
+ */
+ for (const test of [
+ {
+ init: (normalSel, editingHost) => {
+ info("Staring to test spellcheck of misspelled word after joining paragraphs");
+ // eslint-disable-next-line no-unsanitized/property
+ editingHost.innerHTML = "<p>It is</p><p>what I want</p>";
+ normalSel.collapse(editingHost.querySelector("p + p").firstChild, 0);
+ return 0;
+ },
+ run: (editingHost) => {
+ document.execCommand("delete");
+ return 0;
+ },
+ check: (spellCheckSel, editingHost) => {
+ is(
+ spellCheckSel.rangeCount,
+ 0,
+ "The joined misspelled word shouldn't be marked as misspelled word because caret is in the word"
+ );
+ },
+ },
+ {
+ init: (normalSel, editingHost) => {
+ info("Staring to test spellcheck of correct word after joining paragraphs");
+ // eslint-disable-next-line no-unsanitized/property
+ editingHost.innerHTML = "<p>It's beco</p><p>ming nicer</p>";
+ normalSel.collapse(editingHost.querySelector("p + p").firstChild, 0);
+ return 2;
+ },
+ run: (editingHost) => {
+ document.execCommand("delete");
+ return 0;
+ },
+ check: (spellCheckSel, editingHost) => {
+ is(
+ spellCheckSel.rangeCount,
+ 0,
+ "There shouldn't be misspelled word after joining separated word anyway"
+ );
+ },
+ },
+ {
+ init: (normalSel, editingHost) => {
+ info("Staring to test spellcheck of correct words after splitting a paragraph");
+ // eslint-disable-next-line no-unsanitized/property
+ editingHost.innerHTML = "<p>It iswhat I want</p>";
+ normalSel.collapse(editingHost.querySelector("p").firstChild, "It is".length);
+ return 1;
+ },
+ run: (editingHost) => {
+ document.execCommand("insertParagraph");
+ return 0;
+ },
+ check: (spellCheckSel, editingHost) => {
+ is(
+ spellCheckSel.rangeCount,
+ 0,
+ "No word should be marked as misspelled after split"
+ );
+ },
+ },
+ {
+ init: (normalSel, editingHost) => {
+ info("Staring to test spellcheck of misspelled words after splitting a paragraph");
+ // eslint-disable-next-line no-unsanitized/property
+ editingHost.innerHTML = "<p>It's becoming nicer</p>";
+ normalSel.collapse(editingHost.querySelector("p").firstChild, "It's beco".length);
+ return 0;
+ },
+ run: (editingHost) => {
+ document.execCommand("insertParagraph");
+ return 1;
+ },
+ check: (spellCheckSel, editingHost) => {
+ is(
+ spellCheckSel.rangeCount,
+ 1,
+ "The split word in the first paragraph should be marked as misspelled, but the second paragraph's should be so because of caret is in it"
+ );
+ if (!spellCheckSel.rangeCount) {
+ return;
+ }
+ is(
+ SpecialPowers.unwrap(spellCheckSel.getRangeAt(0).startContainer),
+ editingHost.querySelector("p").firstChild,
+ "First misspelled word should start in the first child of the first <p>"
+ );
+ is(
+ SpecialPowers.unwrap(spellCheckSel.getRangeAt(0).endContainer),
+ editingHost.querySelector("p").firstChild,
+ "First misspelled word should end in the first child of the first <p>"
+ );
+ is(
+ spellCheckSel.getRangeAt(0).startOffset,
+ "It's ".length,
+ "First misspelled word should start after 'It '"
+ );
+ is(
+ spellCheckSel.getRangeAt(0).endOffset,
+ "It's beco".length,
+ "First misspelled word should end by after 'bec'"
+ );
+ },
+ },
+ ]) {
+ const editingHost = document.createElement("div");
+ editingHost.setAttribute("contenteditable", "");
+ editingHost.setAttribute("spellcheck", "true");
+ document.body.appendChild(editingHost);
+ editingHost.focus();
+ const editor =
+ SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
+ const nsISelectionController = SpecialPowers.Ci.nsISelectionController;
+ const normalSel = editor.selectionController.getSelection(
+ nsISelectionController.SELECTION_NORMAL
+ );
+ const spellCheckSel = editor.selectionController.getSelection(
+ nsISelectionController.SELECTION_SPELLCHECK
+ );
+ const initialMisspelledWords = test.init(normalSel, editingHost);
+ await waitForOnSpellCheck(
+ spellCheckSel, editingHost, initialMisspelledWords, "before edit"
+ );
+ await waitForTick();
+ const expectedMisspelledWords = test.run(editingHost);
+ await waitForOnSpellCheck(
+ spellCheckSel, editingHost, expectedMisspelledWords, "after edit"
+ );
+ test.check(spellCheckSel, editingHost);
+ editingHost.remove();
+ await waitForTick();
+ }
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_spellcheck_after_pressing_navigation_key.html b/editor/spellchecker/tests/test_spellcheck_after_pressing_navigation_key.html
new file mode 100644
index 0000000000..2f0c3bed7d
--- /dev/null
+++ b/editor/spellchecker/tests/test_spellcheck_after_pressing_navigation_key.html
@@ -0,0 +1,77 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1729653
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1729653</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 rows="20" cols="50">That undfgdfg seems OK.</textarea>
+<script>
+let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+);
+
+function waitForTick() {
+ return new Promise(resolve => SimpleTest.executeSoon(resolve));
+}
+
+function waitForOnSpellCheck(aTextArea) {
+ info("Waiting for onSpellCheck...");
+ return new Promise(resolve => maybeOnSpellCheck(aTextArea, resolve));
+}
+
+/** Test for Bug 1729653 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const textarea = document.querySelector("textarea");
+ textarea.focus();
+ textarea.selectionStart = textarea.selectionEnd = "That undfgdfg".length;
+ const editor = SpecialPowers.wrap(textarea).editor;
+ const nsISelectionController = SpecialPowers.Ci.nsISelectionController;
+ const selection = editor.selectionController.getSelection(nsISelectionController.SELECTION_SPELLCHECK);
+ const spellChecker = SpecialPowers.Cu.createSpellChecker();
+ spellChecker.InitSpellChecker(editor, false);
+ info("Waiting for current dictionary update...");
+ await new Promise(resolve => spellChecker.UpdateCurrentDictionary(resolve));
+ if (selection.rangeCount === 0) {
+ await waitForOnSpellCheck(textarea);
+ }
+ if (selection.rangeCount == 1) {
+ is(
+ selection.getRangeAt(0).toString(),
+ "undfgdfg",
+ "\"undfgdfg\" should be marked as misspelled word at start"
+ );
+ } else {
+ is(selection.rangeCount, 1, "We should have a misspelled word at start");
+ }
+ synthesizeKey(" ");
+ synthesizeKey("KEY_Backspace");
+ textarea.addEventListener("keydown", event => {
+ event.stopImmediatePropagation(); // This shouldn't block spellchecker to handle it.
+ }, {once: true});
+ synthesizeKey("KEY_End");
+ await waitForTick();
+ if (selection.rangeCount === 0) {
+ await waitForOnSpellCheck(textarea);
+ }
+ if (selection.rangeCount == 1) {
+ is(
+ selection.getRangeAt(0).toString(),
+ "undfgdfg",
+ "\"undfgdfg\" should be marked as misspelled word at end"
+ );
+ } else {
+ is(selection.rangeCount, 1, "We should have a misspelled word at end");
+ }
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_spellcheck_selection.html b/editor/spellchecker/tests/test_spellcheck_selection.html
new file mode 100644
index 0000000000..0d0887a8f3
--- /dev/null
+++ b/editor/spellchecker/tests/test_spellcheck_selection.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1779846: Test enableSelectionChecking=true on nsIEditorSpellCheck.InitSpellChecker</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+
+<div contenteditable lang="en-US">missspelled</div>
+
+<script>
+add_task(async function() {
+ await new Promise(resolve => SimpleTest.waitForFocus(resolve));
+
+ let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+
+ let editingHost = document.querySelector("div[contenteditable][lang=en-US]");
+ editingHost.focus();
+
+ await new Promise(resolve => maybeOnSpellCheck(editingHost, resolve));
+
+ let editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ let editor = editingSession.getEditorForWindow(window);
+ let spellchecker = SpecialPowers.Cu.createSpellChecker();
+ spellchecker.setFilterType(spellchecker.FILTERTYPE_NORMAL);
+
+ /* Select "missspelled" in the <div>. */
+ window.getSelection().selectAllChildren(editingHost);
+
+ /* Pass true to InitSpellChecker to spellcheck the current selection of the editor.*/
+ await new Promise(resolve => spellchecker.InitSpellChecker(editor, true, resolve));
+
+ /* InitSpellChecker with enableSelectionChecking=true shouldn't throw any errors. */
+ ok(spellchecker.canSpellCheck());
+});
+</script>
diff --git a/editor/spellchecker/tests/test_suggest.html b/editor/spellchecker/tests/test_suggest.html
new file mode 100644
index 0000000000..2bdee93c1d
--- /dev/null
+++ b/editor/spellchecker/tests/test_suggest.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for nsIEditorSpellChecfker.sugget</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div contenteditable id="en-US" lang="en-US">missspelled</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+add_task(async function() {
+ await new Promise(resolve => SimpleTest.waitForFocus(resolve));
+
+ let { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm"
+ );
+
+ let element = document.getElementById("en-US");
+ element.focus();
+
+ await new Promise(resolve => maybeOnSpellCheck(element, resolve));
+
+ let editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ let editor = editingSession.getEditorForWindow(window);
+ let spellchecker = SpecialPowers.Cu.createSpellChecker();
+ spellchecker.setFilterType(spellchecker.FILTERTYPE_NORMAL);
+ await new Promise(resolve => spellchecker.InitSpellChecker(editor, false, resolve));
+
+ let suggestions = await spellchecker.suggest("misspelled", 5);
+ is(suggestions.length, 0, "\"misspelled\" is correct word");
+
+ suggestions = await spellchecker.suggest("missspelled", 5);
+ is(suggestions.length, 5, "\"missspelled\" isn't correct word");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/txmgr/TransactionItem.cpp b/editor/txmgr/TransactionItem.cpp
new file mode 100644
index 0000000000..26d114dfc9
--- /dev/null
+++ b/editor/txmgr/TransactionItem.cpp
@@ -0,0 +1,257 @@
+/* -*- 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 "TransactionItem.h"
+
+#include "mozilla/mozalloc.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/TransactionManager.h"
+#include "mozilla/TransactionStack.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsISupportsImpl.h"
+#include "nsITransaction.h"
+
+namespace mozilla {
+
+TransactionItem::TransactionItem(nsITransaction* aTransaction)
+ : mTransaction(aTransaction), mUndoStack(0), mRedoStack(0) {}
+
+TransactionItem::~TransactionItem() {
+ delete mRedoStack;
+ delete mUndoStack;
+}
+
+void TransactionItem::CleanUp() {
+ mData.Clear();
+ mTransaction = nullptr;
+ if (mRedoStack) {
+ mRedoStack->DoUnlink();
+ }
+ if (mUndoStack) {
+ mUndoStack->DoUnlink();
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(TransactionItem)
+NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE_WITH_LAST_RELEASE(TransactionItem,
+ CleanUp())
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(TransactionItem)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TransactionItem)
+ tmp->CleanUp();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TransactionItem)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mData)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction)
+ if (tmp->mRedoStack) {
+ tmp->mRedoStack->DoTraverse(cb);
+ }
+ if (tmp->mUndoStack) {
+ tmp->mUndoStack->DoTraverse(cb);
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+nsresult TransactionItem::AddChild(TransactionItem& aTransactionItem) {
+ if (!mUndoStack) {
+ mUndoStack = new TransactionStack(TransactionStack::FOR_UNDO);
+ }
+
+ mUndoStack->Push(&aTransactionItem);
+ return NS_OK;
+}
+
+already_AddRefed<nsITransaction> TransactionItem::GetTransaction() {
+ return do_AddRef(mTransaction);
+}
+
+nsresult TransactionItem::DoTransaction() {
+ if (!mTransaction) {
+ return NS_OK;
+ }
+ OwningNonNull<nsITransaction> transaction = *mTransaction;
+ nsresult rv = transaction->DoTransaction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsITransaction::DoTransaction() failed");
+ return rv;
+}
+
+nsresult TransactionItem::UndoTransaction(
+ TransactionManager* aTransactionManager) {
+ nsresult rv = UndoChildren(aTransactionManager);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TransactionItem::UndoChildren() failed");
+ DebugOnly<nsresult> rvIgnored = RecoverFromUndoError(aTransactionManager);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TransactionItem::RecoverFromUndoError() failed");
+ return rv;
+ }
+
+ if (!mTransaction) {
+ return NS_OK;
+ }
+
+ OwningNonNull<nsITransaction> transaction = *mTransaction;
+ rv = transaction->UndoTransaction();
+ if (NS_SUCCEEDED(rv)) {
+ return NS_OK;
+ }
+
+ NS_WARNING("TransactionItem::UndoTransaction() failed");
+ DebugOnly<nsresult> rvIgnored = RecoverFromUndoError(aTransactionManager);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TransactionItem::RecoverFromUndoError() failed");
+ return rv;
+}
+
+nsresult TransactionItem::UndoChildren(
+ TransactionManager* aTransactionManager) {
+ if (!mUndoStack) {
+ return NS_OK;
+ }
+
+ if (!mRedoStack) {
+ mRedoStack = new TransactionStack(TransactionStack::FOR_REDO);
+ }
+
+ const size_t undoStackSize = mUndoStack->GetSize();
+
+ nsresult rv = NS_OK;
+ for ([[maybe_unused]] const size_t undoneCount :
+ IntegerRange(undoStackSize)) {
+ RefPtr<TransactionItem> transactionItem = mUndoStack->Peek();
+ if (!transactionItem) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
+ rv = transactionItem->UndoTransaction(aTransactionManager);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionItem::UndoTransaction() failed");
+ if (NS_SUCCEEDED(rv)) {
+ transactionItem = mUndoStack->Pop();
+ mRedoStack->Push(transactionItem.forget());
+ }
+
+ if (transaction) {
+ aTransactionManager->DidUndoNotify(*transaction, rv);
+ }
+ }
+ // NS_OK if there is no Undo items or all methods work fine, otherwise,
+ // the result of the last item's UndoTransaction().
+ return rv;
+}
+
+nsresult TransactionItem::RedoTransaction(
+ TransactionManager* aTransactionManager) {
+ nsCOMPtr<nsITransaction> transaction(mTransaction);
+ if (transaction) {
+ nsresult rv = transaction->RedoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsITransaction::RedoTransaction() failed");
+ return rv;
+ }
+ }
+
+ nsresult rv = RedoChildren(aTransactionManager);
+ if (NS_SUCCEEDED(rv)) {
+ return NS_OK;
+ }
+
+ NS_WARNING("TransactionItem::RedoChildren() failed");
+ DebugOnly<nsresult> rvIgnored = RecoverFromRedoError(aTransactionManager);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TransactionItem::RecoverFromRedoError() failed");
+ return rv;
+}
+
+nsresult TransactionItem::RedoChildren(
+ TransactionManager* aTransactionManager) {
+ if (!mRedoStack) {
+ return NS_OK;
+ }
+
+ /* Redo all of the transaction items children! */
+ const size_t redoStackSize = mRedoStack->GetSize();
+
+ nsresult rv = NS_OK;
+ for ([[maybe_unused]] const size_t redoneCount :
+ IntegerRange(redoStackSize)) {
+ RefPtr<TransactionItem> transactionItem = mRedoStack->Peek();
+ if (NS_WARN_IF(!transactionItem)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
+ rv = transactionItem->RedoTransaction(aTransactionManager);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionItem::RedoTransaction() failed");
+ if (NS_SUCCEEDED(rv)) {
+ transactionItem = mRedoStack->Pop();
+ mUndoStack->Push(transactionItem.forget());
+ }
+
+ // XXX Shouldn't this DidRedoNotify()? (bug 1311626)
+ if (transaction) {
+ aTransactionManager->DidUndoNotify(*transaction, rv);
+ }
+ }
+ // NS_OK if there is no Redo items or all methods work fine, otherwise,
+ // the result of the last item's RedoTransaction().
+ return rv;
+}
+
+size_t TransactionItem::NumberOfUndoItems() const {
+ NS_WARNING_ASSERTION(!mUndoStack || mUndoStack->GetSize() > 0,
+ "UndoStack cannot have no children");
+ return mUndoStack ? mUndoStack->GetSize() : 0;
+}
+
+size_t TransactionItem::NumberOfRedoItems() const {
+ NS_WARNING_ASSERTION(!mRedoStack || mRedoStack->GetSize() > 0,
+ "UndoStack cannot have no children");
+ return mRedoStack ? mRedoStack->GetSize() : 0;
+}
+
+nsresult TransactionItem::RecoverFromUndoError(
+ TransactionManager* aTransactionManager) {
+ // If this method gets called, we never got to the point where we
+ // successfully called UndoTransaction() for the transaction item itself.
+ // Just redo any children that successfully called undo!
+ nsresult rv = RedoChildren(aTransactionManager);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionItem::RedoChildren() failed");
+ return rv;
+}
+
+nsresult TransactionItem::RecoverFromRedoError(
+ TransactionManager* aTransactionManager) {
+ // If this method gets called, we already successfully called
+ // RedoTransaction() for the transaction item itself. Undo all
+ // the children that successfully called RedoTransaction(),
+ // then undo the transaction item itself.
+ nsresult rv = UndoChildren(aTransactionManager);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TransactionItem::UndoChildren() failed");
+ return rv;
+ }
+
+ if (!mTransaction) {
+ return NS_OK;
+ }
+
+ OwningNonNull<nsITransaction> transaction = *mTransaction;
+ rv = transaction->UndoTransaction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsITransaction::UndoTransaction() failed");
+ return rv;
+}
+
+} // namespace mozilla
diff --git a/editor/txmgr/TransactionItem.h b/editor/txmgr/TransactionItem.h
new file mode 100644
index 0000000000..6a8142a1fa
--- /dev/null
+++ b/editor/txmgr/TransactionItem.h
@@ -0,0 +1,74 @@
+/* -*- 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 TransactionItem_h
+#define TransactionItem_h
+
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+
+class nsITransaction;
+
+namespace mozilla {
+
+class TransactionManager;
+class TransactionStack;
+
+class TransactionItem final {
+ public:
+ explicit TransactionItem(nsITransaction* aTransaction);
+ NS_METHOD_(MozExternalRefCountType) AddRef();
+ NS_METHOD_(MozExternalRefCountType) Release();
+
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(TransactionItem)
+
+ nsresult AddChild(TransactionItem& aTransactionItem);
+ already_AddRefed<nsITransaction> GetTransaction();
+ size_t NumberOfChildren() const {
+ return NumberOfUndoItems() + NumberOfRedoItems();
+ }
+ nsresult GetChild(size_t aIndex, TransactionItem** aChild);
+
+ MOZ_CAN_RUN_SCRIPT nsresult DoTransaction();
+ MOZ_CAN_RUN_SCRIPT nsresult
+ UndoTransaction(TransactionManager* aTransactionManager);
+ MOZ_CAN_RUN_SCRIPT nsresult
+ RedoTransaction(TransactionManager* aTransactionManager);
+
+ nsCOMArray<nsISupports>& GetData() { return mData; }
+
+ private:
+ MOZ_CAN_RUN_SCRIPT nsresult
+ UndoChildren(TransactionManager* aTransactionManager);
+ MOZ_CAN_RUN_SCRIPT nsresult
+ RedoChildren(TransactionManager* aTransactionManager);
+
+ MOZ_CAN_RUN_SCRIPT nsresult
+ RecoverFromUndoError(TransactionManager* aTransactionManager);
+ MOZ_CAN_RUN_SCRIPT nsresult
+ RecoverFromRedoError(TransactionManager* aTransactionManager);
+
+ size_t NumberOfUndoItems() const;
+ size_t NumberOfRedoItems() const;
+
+ void CleanUp();
+
+ ~TransactionItem();
+
+ nsCycleCollectingAutoRefCnt mRefCnt;
+ NS_DECL_OWNINGTHREAD
+
+ nsCOMArray<nsISupports> mData;
+ nsCOMPtr<nsITransaction> mTransaction;
+ TransactionStack* mUndoStack;
+ TransactionStack* mRedoStack;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef TransactionItem_h
diff --git a/editor/txmgr/TransactionManager.cpp b/editor/txmgr/TransactionManager.cpp
new file mode 100644
index 0000000000..ad4d641d56
--- /dev/null
+++ b/editor/txmgr/TransactionManager.cpp
@@ -0,0 +1,509 @@
+/* -*- 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/TransactionManager.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/TransactionStack.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsISupports.h"
+#include "nsISupportsUtils.h"
+#include "nsITransaction.h"
+#include "nsIWeakReference.h"
+#include "TransactionItem.h"
+
+namespace mozilla {
+
+TransactionManager::TransactionManager(int32_t aMaxTransactionCount)
+ : mMaxTransactionCount(aMaxTransactionCount),
+ mDoStack(TransactionStack::FOR_UNDO),
+ mUndoStack(TransactionStack::FOR_UNDO),
+ mRedoStack(TransactionStack::FOR_REDO) {}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(TransactionManager)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TransactionManager)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mHTMLEditor)
+ tmp->mDoStack.DoUnlink();
+ tmp->mUndoStack.DoUnlink();
+ tmp->mRedoStack.DoUnlink();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TransactionManager)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHTMLEditor)
+ tmp->mDoStack.DoTraverse(cb);
+ tmp->mUndoStack.DoTraverse(cb);
+ tmp->mRedoStack.DoTraverse(cb);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransactionManager)
+ NS_INTERFACE_MAP_ENTRY(nsITransactionManager)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITransactionManager)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TransactionManager)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TransactionManager)
+
+void TransactionManager::Attach(HTMLEditor& aHTMLEditor) {
+ mHTMLEditor = &aHTMLEditor;
+}
+
+void TransactionManager::Detach(const HTMLEditor& aHTMLEditor) {
+ MOZ_DIAGNOSTIC_ASSERT_IF(mHTMLEditor, &aHTMLEditor == mHTMLEditor);
+ if (mHTMLEditor == &aHTMLEditor) {
+ mHTMLEditor = nullptr;
+ }
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+TransactionManager::DoTransaction(nsITransaction* aTransaction) {
+ if (NS_WARN_IF(!aTransaction)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ OwningNonNull<nsITransaction> transaction = *aTransaction;
+
+ nsresult rv = BeginTransaction(transaction, nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TransactionManager::BeginTransaction() failed");
+ DidDoNotify(transaction, rv);
+ return rv;
+ }
+
+ rv = EndTransaction(false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionManager::EndTransaction() failed");
+
+ DidDoNotify(transaction, rv);
+ return rv;
+}
+
+MOZ_CAN_RUN_SCRIPT NS_IMETHODIMP TransactionManager::UndoTransaction() {
+ return Undo();
+}
+
+nsresult TransactionManager::Undo() {
+ // It's possible to be called Undo() again while the transaction manager is
+ // executing a transaction's DoTransaction() method. If this happens,
+ // the Undo() request is ignored, and we return NS_ERROR_FAILURE. This
+ // may occur if a mutation event listener calls document.execCommand("undo").
+ if (NS_WARN_IF(!mDoStack.IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Peek at the top of the undo stack. Don't remove the transaction
+ // until it has successfully completed.
+ RefPtr<TransactionItem> transactionItem = mUndoStack.Peek();
+ if (!transactionItem) {
+ // Bail if there's nothing on the stack.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
+ nsresult rv = transactionItem->UndoTransaction(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionItem::UndoTransaction() failed");
+ if (NS_SUCCEEDED(rv)) {
+ transactionItem = mUndoStack.Pop();
+ mRedoStack.Push(transactionItem.forget());
+ }
+
+ if (transaction) {
+ DidUndoNotify(*transaction, rv);
+ }
+ return rv;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+TransactionManager::RedoTransaction() {
+ return Redo();
+}
+
+nsresult TransactionManager::Redo() {
+ // It's possible to be called Redo() again while the transaction manager is
+ // executing a transaction's DoTransaction() method. If this happens,
+ // the Redo() request is ignored, and we return NS_ERROR_FAILURE. This
+ // may occur if a mutation event listener calls document.execCommand("redo").
+ if (NS_WARN_IF(!mDoStack.IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Peek at the top of the redo stack. Don't remove the transaction
+ // until it has successfully completed.
+ RefPtr<TransactionItem> transactionItem = mRedoStack.Peek();
+ if (!transactionItem) {
+ // Bail if there's nothing on the stack.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
+ nsresult rv = transactionItem->RedoTransaction(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionItem::RedoTransaction() failed");
+ if (NS_SUCCEEDED(rv)) {
+ transactionItem = mRedoStack.Pop();
+ mUndoStack.Push(transactionItem.forget());
+ }
+
+ if (transaction) {
+ DidRedoNotify(*transaction, rv);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP TransactionManager::Clear() {
+ return NS_WARN_IF(!ClearUndoRedo()) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+NS_IMETHODIMP TransactionManager::BeginBatch(nsISupports* aData) {
+ nsresult rv = BeginBatchInternal(aData);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionManager::BeginBatchInternal() failed");
+ return rv;
+}
+
+nsresult TransactionManager::BeginBatchInternal(nsISupports* aData) {
+ // We can batch independent transactions together by simply pushing
+ // a dummy transaction item on the do stack. This dummy transaction item
+ // will be popped off the do stack, and then pushed on the undo stack
+ // in EndBatch().
+ nsresult rv = BeginTransaction(nullptr, aData);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionManager::BeginTransaction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP TransactionManager::EndBatch(bool aAllowEmpty) {
+ nsresult rv = EndBatchInternal(aAllowEmpty);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionManager::EndBatchInternal() failed");
+ return rv;
+}
+
+nsresult TransactionManager::EndBatchInternal(bool aAllowEmpty) {
+ // XXX: Need to add some mechanism to detect the case where the transaction
+ // at the top of the do stack isn't the dummy transaction, so we can
+ // throw an error!! This can happen if someone calls EndBatch() within
+ // the DoTransaction() method of a transaction.
+ //
+ // For now, we can detect this case by checking the value of the
+ // dummy transaction's mTransaction field. If it is our dummy
+ // transaction, it should be nullptr. This may not be true in the
+ // future when we allow users to execute a transaction when beginning
+ // a batch!!!!
+ RefPtr<TransactionItem> transactionItem = mDoStack.Peek();
+ if (NS_WARN_IF(!transactionItem)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
+ if (NS_WARN_IF(transaction)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = EndTransaction(aAllowEmpty);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionManager::EndTransaction() failed");
+ return rv;
+}
+
+NS_IMETHODIMP TransactionManager::GetNumberOfUndoItems(int32_t* aNumItems) {
+ *aNumItems = static_cast<int32_t>(NumberOfUndoItems());
+ MOZ_ASSERT(*aNumItems >= 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP TransactionManager::GetNumberOfRedoItems(int32_t* aNumItems) {
+ *aNumItems = static_cast<int32_t>(NumberOfRedoItems());
+ MOZ_ASSERT(*aNumItems >= 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP TransactionManager::GetMaxTransactionCount(int32_t* aMaxCount) {
+ if (NS_WARN_IF(!aMaxCount)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aMaxCount = mMaxTransactionCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP TransactionManager::SetMaxTransactionCount(int32_t aMaxCount) {
+ return NS_WARN_IF(!EnableUndoRedo(aMaxCount)) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+bool TransactionManager::EnableUndoRedo(int32_t aMaxTransactionCount) {
+ // It is illegal to call EnableUndoRedo() while the transaction manager is
+ // executing a transaction's DoTransaction() method because the undo and redo
+ // stacks might get pruned. If this happens, the EnableUndoRedo() request is
+ // ignored, and we return false.
+ if (NS_WARN_IF(!mDoStack.IsEmpty())) {
+ return false;
+ }
+
+ // If aMaxTransactionCount is 0, it means to disable undo/redo.
+ if (!aMaxTransactionCount) {
+ mUndoStack.Clear();
+ mRedoStack.Clear();
+ mMaxTransactionCount = 0;
+ return true;
+ }
+
+ // If aMaxTransactionCount is less than zero, the user wants unlimited
+ // levels of undo! No need to prune the undo or redo stacks.
+ if (aMaxTransactionCount < 0) {
+ mMaxTransactionCount = -1;
+ return true;
+ }
+
+ // If new max transaction count is greater than or equal to current max
+ // transaction count, we don't need to remove any transactions.
+ if (mMaxTransactionCount >= 0 &&
+ mMaxTransactionCount <= aMaxTransactionCount) {
+ mMaxTransactionCount = aMaxTransactionCount;
+ return true;
+ }
+
+ // If aMaxTransactionCount is greater than the number of transactions that
+ // currently exist on the undo and redo stack, there is no need to prune the
+ // undo or redo stacks.
+ size_t numUndoItems = NumberOfUndoItems();
+ size_t numRedoItems = NumberOfRedoItems();
+ size_t total = numUndoItems + numRedoItems;
+ size_t newMaxTransactionCount = static_cast<size_t>(aMaxTransactionCount);
+ if (newMaxTransactionCount > total) {
+ mMaxTransactionCount = aMaxTransactionCount;
+ return true;
+ }
+
+ // Try getting rid of some transactions on the undo stack! Start at
+ // the bottom of the stack and pop towards the top.
+ for (; numUndoItems && (numRedoItems + numUndoItems) > newMaxTransactionCount;
+ numUndoItems--) {
+ RefPtr<TransactionItem> transactionItem = mUndoStack.PopBottom();
+ MOZ_ASSERT(transactionItem);
+ }
+
+ // If necessary, get rid of some transactions on the redo stack! Start at
+ // the bottom of the stack and pop towards the top.
+ for (; numRedoItems && (numRedoItems + numUndoItems) > newMaxTransactionCount;
+ numRedoItems--) {
+ RefPtr<TransactionItem> transactionItem = mRedoStack.PopBottom();
+ MOZ_ASSERT(transactionItem);
+ }
+
+ mMaxTransactionCount = aMaxTransactionCount;
+ return true;
+}
+
+NS_IMETHODIMP TransactionManager::PeekUndoStack(nsITransaction** aTransaction) {
+ MOZ_ASSERT(aTransaction);
+ *aTransaction = PeekUndoStack().take();
+ return NS_OK;
+}
+
+already_AddRefed<nsITransaction> TransactionManager::PeekUndoStack() {
+ RefPtr<TransactionItem> transactionItem = mUndoStack.Peek();
+ if (!transactionItem) {
+ return nullptr;
+ }
+ return transactionItem->GetTransaction();
+}
+
+NS_IMETHODIMP TransactionManager::PeekRedoStack(nsITransaction** aTransaction) {
+ MOZ_ASSERT(aTransaction);
+ *aTransaction = PeekRedoStack().take();
+ return NS_OK;
+}
+
+already_AddRefed<nsITransaction> TransactionManager::PeekRedoStack() {
+ RefPtr<TransactionItem> transactionItem = mRedoStack.Peek();
+ if (!transactionItem) {
+ return nullptr;
+ }
+ return transactionItem->GetTransaction();
+}
+
+nsresult TransactionManager::BatchTopUndo() {
+ if (mUndoStack.GetSize() < 2) {
+ // Not enough transactions to merge into one batch.
+ return NS_OK;
+ }
+
+ RefPtr<TransactionItem> lastUndo = mUndoStack.Pop();
+ MOZ_ASSERT(lastUndo, "There should be at least two transactions.");
+
+ RefPtr<TransactionItem> previousUndo = mUndoStack.Peek();
+ MOZ_ASSERT(previousUndo, "There should be at least two transactions.");
+
+ nsresult rv = previousUndo->AddChild(*lastUndo);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TransactionItem::AddChild() failed");
+
+ // Transfer data from the transactions that is going to be
+ // merged to the transaction that it is being merged with.
+ nsCOMArray<nsISupports>& lastData = lastUndo->GetData();
+ nsCOMArray<nsISupports>& previousData = previousUndo->GetData();
+ if (!previousData.AppendObjects(lastData)) {
+ NS_WARNING("nsISupports::AppendObjects() failed");
+ return NS_ERROR_FAILURE;
+ }
+ lastData.Clear();
+ return rv;
+}
+
+nsresult TransactionManager::RemoveTopUndo() {
+ if (mUndoStack.IsEmpty()) {
+ return NS_OK;
+ }
+
+ RefPtr<TransactionItem> lastUndo = mUndoStack.Pop();
+ return NS_OK;
+}
+
+NS_IMETHODIMP TransactionManager::ClearUndoStack() {
+ if (NS_WARN_IF(!mDoStack.IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+ mUndoStack.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP TransactionManager::ClearRedoStack() {
+ if (NS_WARN_IF(!mDoStack.IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+ mRedoStack.Clear();
+ return NS_OK;
+}
+
+void TransactionManager::DidDoNotify(nsITransaction& aTransaction,
+ nsresult aDoResult) {
+ if (mHTMLEditor) {
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+ htmlEditor->DidDoTransaction(*this, aTransaction, aDoResult);
+ }
+}
+
+void TransactionManager::DidUndoNotify(nsITransaction& aTransaction,
+ nsresult aUndoResult) {
+ if (mHTMLEditor) {
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+ htmlEditor->DidUndoTransaction(*this, aTransaction, aUndoResult);
+ }
+}
+
+void TransactionManager::DidRedoNotify(nsITransaction& aTransaction,
+ nsresult aRedoResult) {
+ if (mHTMLEditor) {
+ RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
+ htmlEditor->DidRedoTransaction(*this, aTransaction, aRedoResult);
+ }
+}
+
+nsresult TransactionManager::BeginTransaction(nsITransaction* aTransaction,
+ nsISupports* aData) {
+ // XXX: POSSIBLE OPTIMIZATION
+ // We could use a factory that pre-allocates/recycles transaction items.
+ RefPtr<TransactionItem> transactionItem = new TransactionItem(aTransaction);
+
+ if (aData) {
+ nsCOMArray<nsISupports>& data = transactionItem->GetData();
+ data.AppendObject(aData);
+ }
+
+ mDoStack.Push(transactionItem);
+
+ nsresult rv = transactionItem->DoTransaction();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TransactionItem::DoTransaction() failed");
+ transactionItem = mDoStack.Pop();
+ }
+ return rv;
+}
+
+nsresult TransactionManager::EndTransaction(bool aAllowEmpty) {
+ RefPtr<TransactionItem> transactionItem = mDoStack.Pop();
+ if (NS_WARN_IF(!transactionItem)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
+ if (!transaction && !aAllowEmpty) {
+ // If we get here, the transaction must be a dummy batch transaction
+ // created by BeginBatch(). If it contains no children, get rid of it!
+ if (!transactionItem->NumberOfChildren()) {
+ return NS_OK;
+ }
+ }
+
+ // Check if the transaction is transient. If it is, there's nothing
+ // more to do, just return.
+ if (transaction) {
+ bool isTransient = false;
+ nsresult rv = transaction->GetIsTransient(&isTransient);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsITransaction::GetIsTransient() failed");
+ return rv;
+ }
+ // XXX: Should we be clearing the redo stack if the transaction
+ // is transient and there is nothing on the do stack?
+ if (isTransient) {
+ return NS_OK;
+ }
+ }
+
+ if (!mMaxTransactionCount) {
+ return NS_OK;
+ }
+
+ // Check if there is a transaction on the do stack. If there is,
+ // the current transaction is a "sub" transaction, and should
+ // be added to the transaction at the top of the do stack.
+ RefPtr<TransactionItem> topTransactionItem = mDoStack.Peek();
+ if (topTransactionItem) {
+ // XXX: What do we do if this fails?
+ nsresult rv = topTransactionItem->AddChild(*transactionItem);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TransactionItem::AddChild() failed");
+ return rv;
+ }
+
+ // The transaction succeeded, so clear the redo stack.
+ mRedoStack.Clear();
+
+ // Check if we can coalesce this transaction with the one at the top
+ // of the undo stack.
+ topTransactionItem = mUndoStack.Peek();
+ if (transaction && topTransactionItem) {
+ bool didMerge = false;
+ nsCOMPtr<nsITransaction> topTransaction =
+ topTransactionItem->GetTransaction();
+ if (topTransaction) {
+ nsresult rv = topTransaction->Merge(transaction, &didMerge);
+ if (didMerge) {
+ return rv;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsITransaction::Merge() failed, but ignored");
+ }
+ }
+
+ // Check to see if we've hit the max level of undo. If so,
+ // pop the bottom transaction off the undo stack and release it!
+ int32_t sz = mUndoStack.GetSize();
+ if (mMaxTransactionCount > 0 && sz >= mMaxTransactionCount) {
+ RefPtr<TransactionItem> overflow = mUndoStack.PopBottom();
+ }
+
+ // Push the transaction on the undo stack:
+ mUndoStack.Push(transactionItem.forget());
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/txmgr/TransactionManager.h b/editor/txmgr/TransactionManager.h
new file mode 100644
index 0000000000..cdabc83c90
--- /dev/null
+++ b/editor/txmgr/TransactionManager.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 mozilla_TransactionManager_h
+#define mozilla_TransactionManager_h
+
+#include "mozilla/EditorForwards.h"
+#include "mozilla/TransactionStack.h"
+
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nsITransactionManager.h"
+#include "nsWeakReference.h"
+#include "nscore.h"
+
+class nsITransaction;
+
+namespace mozilla {
+
+class TransactionManager final : public nsITransactionManager,
+ public nsSupportsWeakReference {
+ public:
+ explicit TransactionManager(int32_t aMaxTransactionCount = -1);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TransactionManager,
+ nsITransactionManager)
+
+ NS_DECL_NSITRANSACTIONMANAGER
+
+ already_AddRefed<nsITransaction> PeekUndoStack();
+ already_AddRefed<nsITransaction> PeekRedoStack();
+
+ MOZ_CAN_RUN_SCRIPT nsresult Undo();
+ MOZ_CAN_RUN_SCRIPT nsresult Redo();
+
+ size_t NumberOfUndoItems() const { return mUndoStack.GetSize(); }
+ size_t NumberOfRedoItems() const { return mRedoStack.GetSize(); }
+
+ int32_t NumberOfMaximumTransactions() const { return mMaxTransactionCount; }
+
+ bool EnableUndoRedo(int32_t aMaxTransactionCount = -1);
+ bool DisableUndoRedo() { return EnableUndoRedo(0); }
+ bool ClearUndoRedo() {
+ if (NS_WARN_IF(!mDoStack.IsEmpty())) {
+ return false;
+ }
+ mUndoStack.Clear();
+ mRedoStack.Clear();
+ return true;
+ }
+
+ void Attach(HTMLEditor& aHTMLEditor);
+ void Detach(const HTMLEditor& aHTMLEditor);
+
+ MOZ_CAN_RUN_SCRIPT void DidDoNotify(nsITransaction& aTransaction,
+ nsresult aDoResult);
+ MOZ_CAN_RUN_SCRIPT void DidUndoNotify(nsITransaction& aTransaction,
+ nsresult aUndoResult);
+ MOZ_CAN_RUN_SCRIPT void DidRedoNotify(nsITransaction& aTransaction,
+ nsresult aRedoResult);
+
+ /**
+ * Exposing non-virtual methods of nsITransactionManager methods.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult BeginBatchInternal(nsISupports* aData);
+ nsresult EndBatchInternal(bool aAllowEmpty);
+
+ private:
+ virtual ~TransactionManager() = default;
+
+ MOZ_CAN_RUN_SCRIPT nsresult BeginTransaction(nsITransaction* aTransaction,
+ nsISupports* aData);
+ nsresult EndTransaction(bool aAllowEmpty);
+
+ int32_t mMaxTransactionCount;
+ TransactionStack mDoStack;
+ TransactionStack mUndoStack;
+ TransactionStack mRedoStack;
+ RefPtr<HTMLEditor> mHTMLEditor;
+};
+
+} // namespace mozilla
+
+mozilla::TransactionManager* nsITransactionManager::AsTransactionManager() {
+ return static_cast<mozilla::TransactionManager*>(this);
+}
+
+#endif // #ifndef mozilla_TransactionManager_h
diff --git a/editor/txmgr/TransactionStack.cpp b/editor/txmgr/TransactionStack.cpp
new file mode 100644
index 0000000000..f0d9c7d455
--- /dev/null
+++ b/editor/txmgr/TransactionStack.cpp
@@ -0,0 +1,75 @@
+/* -*- 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 "TransactionStack.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsDebug.h"
+#include "nsISupportsUtils.h"
+#include "nscore.h"
+#include "TransactionItem.h"
+
+namespace mozilla {
+
+TransactionStack::TransactionStack(Type aType)
+ : nsRefPtrDeque<TransactionItem>(), mType(aType) {}
+
+TransactionStack::~TransactionStack() { Clear(); }
+
+void TransactionStack::Push(TransactionItem* aTransactionItem) {
+ if (NS_WARN_IF(!aTransactionItem)) {
+ return;
+ }
+ nsRefPtrDeque<TransactionItem>::Push(aTransactionItem);
+}
+
+void TransactionStack::Push(
+ already_AddRefed<TransactionItem> aTransactionItem) {
+ TransactionItem* transactionItem = aTransactionItem.take();
+ if (NS_WARN_IF(!transactionItem)) {
+ return;
+ }
+ nsRefPtrDeque<TransactionItem>::Push(dont_AddRef(transactionItem));
+}
+
+already_AddRefed<TransactionItem> TransactionStack::Pop() {
+ return nsRefPtrDeque<TransactionItem>::Pop();
+}
+
+already_AddRefed<TransactionItem> TransactionStack::PopBottom() {
+ return nsRefPtrDeque<TransactionItem>::PopFront();
+}
+
+already_AddRefed<TransactionItem> TransactionStack::Peek() const {
+ RefPtr<TransactionItem> item = nsRefPtrDeque<TransactionItem>::Peek();
+ return item.forget();
+}
+
+already_AddRefed<TransactionItem> TransactionStack::GetItemAt(
+ size_t aIndex) const {
+ RefPtr<TransactionItem> item =
+ nsRefPtrDeque<TransactionItem>::ObjectAt(aIndex);
+ return item.forget();
+}
+
+void TransactionStack::Clear() {
+ while (GetSize()) {
+ RefPtr<TransactionItem> item = mType == FOR_UNDO ? Pop() : PopBottom();
+ }
+}
+
+void TransactionStack::DoTraverse(nsCycleCollectionTraversalCallback& cb) {
+ size_t size = GetSize();
+ for (size_t i = 0; i < size; ++i) {
+ auto* item = nsRefPtrDeque<TransactionItem>::ObjectAt(i);
+ if (item) {
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "transaction stack mDeque[i]");
+ cb.NoteNativeChild(item,
+ NS_CYCLE_COLLECTION_PARTICIPANT(TransactionItem));
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/editor/txmgr/TransactionStack.h b/editor/txmgr/TransactionStack.h
new file mode 100644
index 0000000000..74e8aebec8
--- /dev/null
+++ b/editor/txmgr/TransactionStack.h
@@ -0,0 +1,44 @@
+/* -*- 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_TransactionStack_h
+#define mozilla_TransactionStack_h
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "nsDeque.h"
+
+class nsCycleCollectionTraversalCallback;
+
+namespace mozilla {
+
+class TransactionItem;
+
+class TransactionStack : private nsRefPtrDeque<TransactionItem> {
+ public:
+ enum Type { FOR_UNDO, FOR_REDO };
+
+ explicit TransactionStack(Type aType);
+ ~TransactionStack();
+
+ void Push(TransactionItem* aTransactionItem);
+ void Push(already_AddRefed<TransactionItem> aTransactionItem);
+ already_AddRefed<TransactionItem> Pop();
+ already_AddRefed<TransactionItem> PopBottom();
+ already_AddRefed<TransactionItem> Peek() const;
+ already_AddRefed<TransactionItem> GetItemAt(size_t aIndex) const;
+ void Clear();
+ size_t GetSize() const { return nsRefPtrDeque<TransactionItem>::GetSize(); }
+ bool IsEmpty() const { return GetSize() == 0; }
+
+ void DoUnlink() { Clear(); }
+ void DoTraverse(nsCycleCollectionTraversalCallback& cb);
+
+ private:
+ const Type mType;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_TransactionStack_h
diff --git a/editor/txmgr/moz.build b/editor/txmgr/moz.build
new file mode 100644
index 0000000000..f4310ab298
--- /dev/null
+++ b/editor/txmgr/moz.build
@@ -0,0 +1,31 @@
+# -*- 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/.
+
+TEST_DIRS += ["tests"]
+
+XPIDL_SOURCES += [
+ "nsITransaction.idl",
+ "nsITransactionManager.idl",
+]
+
+XPIDL_MODULE = "txmgr"
+
+EXPORTS += [
+ "nsTransactionManagerCID.h",
+]
+
+EXPORTS.mozilla += [
+ "TransactionManager.h",
+ "TransactionStack.h",
+]
+
+UNIFIED_SOURCES += [
+ "TransactionItem.cpp",
+ "TransactionManager.cpp",
+ "TransactionStack.cpp",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/editor/txmgr/nsITransaction.idl b/editor/txmgr/nsITransaction.idl
new file mode 100644
index 0000000000..bc8a8ed1d2
--- /dev/null
+++ b/editor/txmgr/nsITransaction.idl
@@ -0,0 +1,82 @@
+/* -*- 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 "nsISupports.idl"
+
+%{ C++
+namespace mozilla {
+class EditTransactionBase;
+}
+template <typename T>
+struct already_AddRefed;
+%}
+
+[ptr] native EditTransactionBasePtr(mozilla::EditTransactionBase);
+
+/*
+ * The nsITransaction interface.
+ * <P>
+ * This interface is implemented by an object that needs to
+ * execute some behavior that must be tracked by the transaction manager.
+ */
+[scriptable, uuid(58e330c1-7b48-11d2-98b9-00805f297d89)]
+interface nsITransaction : nsISupports
+{
+ /**
+ * Executes the transaction.
+ */
+ [can_run_script]
+ void doTransaction();
+
+ /**
+ * Restores the state to what it was before the transaction was executed.
+ */
+ [can_run_script]
+ void undoTransaction();
+
+ /**
+ * Executes the transaction again. Can only be called on a transaction that
+ * was previously undone.
+ * <P>
+ * In most cases, the redoTransaction() method will actually call the
+ * doTransaction() method to execute the transaction again.
+ */
+ [can_run_script]
+ void redoTransaction();
+
+ /**
+ * The transaction's transient state. This attribute is checked by
+ * the transaction manager after the transaction's Execute() method is called.
+ * If the transient state is false, a reference to the transaction is
+ * held by the transaction manager so that the transactions' undoTransaction()
+ * and redoTransaction() methods can be called. If the transient state is
+ * true, the transaction manager returns immediately after the transaction's
+ * doTransaction() method is called, no references to the transaction are
+ * maintained. Transient transactions cannot be undone or redone by the
+ * transaction manager.
+ */
+ readonly attribute boolean isTransient;
+
+ /**
+ * Attempts to merge a transaction into "this" transaction. Both transactions
+ * must be in their undo state, doTransaction() methods already called. The
+ * transaction manager calls this method to coalesce a new transaction with
+ * the transaction on the top of the undo stack.
+ * This method returns a boolean value that indicates the merge result.
+ * A true value indicates that the transactions were merged successfully,
+ * a false value if the merge was not possible or failed. If true,
+ * the transaction manager will Release() the new transacton instead of
+ * pushing it on the undo stack.
+ * @param aTransaction the previously executed transaction to merge.
+ */
+ boolean merge(in nsITransaction aTransaction);
+
+ [noscript] EditTransactionBasePtr getAsEditTransactionBase();
+
+%{ C++
+ inline already_AddRefed<mozilla::EditTransactionBase>
+ GetAsEditTransactionBase();
+%}
+};
diff --git a/editor/txmgr/nsITransactionManager.idl b/editor/txmgr/nsITransactionManager.idl
new file mode 100644
index 0000000000..612071380b
--- /dev/null
+++ b/editor/txmgr/nsITransactionManager.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+#include "nsITransaction.idl"
+
+%{C++
+namespace mozilla {
+class TransactionManager;
+} // namespace mozilla
+%}
+
+/**
+ * The nsITransactionManager interface.
+ * <P>
+ * This interface is implemented by an object that wants to
+ * manage/track transactions.
+ */
+[scriptable, builtinclass, uuid(c77763df-0fb9-41a8-8074-8e882f605755)]
+interface nsITransactionManager : nsISupports
+{
+ /**
+ * Calls a transaction's doTransaction() method, then pushes it on the
+ * undo stack.
+ * <P>
+ * This method calls the transaction's AddRef() method.
+ * The transaction's Release() method will be called when the undo or redo
+ * stack is pruned or when the transaction manager is destroyed.
+ * @param aTransaction the transaction to do.
+ */
+ [can_run_script]
+ void doTransaction(in nsITransaction aTransaction);
+
+ /**
+ * Pops the topmost transaction on the undo stack, calls its
+ * undoTransaction() method, then pushes it on the redo stack.
+ */
+ [can_run_script]
+ void undoTransaction();
+
+ /**
+ * Pops the topmost transaction on the redo stack, calls its
+ * redoTransaction() method, then pushes it on the undo stack.
+ */
+ [can_run_script]
+ void redoTransaction();
+
+ /**
+ * Clears the undo and redo stacks.
+ */
+ void clear();
+
+ /**
+ * Clears the undo stack only.
+ */
+ void clearUndoStack();
+
+ /**
+ * Clears the redo stack only.
+ */
+ void clearRedoStack();
+
+ /**
+ * Turns on the transaction manager's batch mode, forcing all transactions
+ * executed by the transaction manager's doTransaction() method to be
+ * aggregated together until EndBatch() is called. This mode allows an
+ * application to execute and group together several independent transactions
+ * so they can be undone with a single call to undoTransaction().
+ * @param aData An arbitrary nsISupports object that is associated with the
+ * batch. Can be retrieved from the undo or redo stacks.
+ */
+ [can_run_script]
+ void beginBatch(in nsISupports aData);
+
+ /**
+ * Turns off the transaction manager's batch mode.
+ * @param aAllowEmpty If true, a batch containing no children will be
+ * pushed onto the undo stack. Otherwise, ending a batch with no
+ * children will result in no transactions being pushed on the undo stack.
+ */
+ void endBatch(in boolean aAllowEmpty);
+
+ /**
+ * The number of items on the undo stack.
+ */
+ readonly attribute long numberOfUndoItems;
+
+ /**
+ * The number of items on the redo stack.
+ */
+ readonly attribute long numberOfRedoItems;
+
+ /**
+ * Sets the maximum number of transaction items the transaction manager will
+ * maintain at any time. This is commonly referred to as the number of levels
+ * of undo.
+ * @param aMaxCount A value of -1 means no limit. A value of zero means the
+ * transaction manager will execute each transaction, then immediately release
+ * all references it has to the transaction without pushing it on the undo
+ * stack. A value greater than zero indicates the max number of transactions
+ * that can exist at any time on both the undo and redo stacks. This method
+ * will prune the necessary number of transactions on the undo and redo
+ * stacks if the value specified is less than the number of items that exist
+ * on both the undo and redo stacks.
+ */
+ attribute long maxTransactionCount;
+
+ /**
+ * Combines the transaction at the top of the undo stack (if any) with the
+ * preceding undo transaction (if any) into a batch transaction. Thus,
+ * a call to undoTransaction() will undo both transactions.
+ */
+ void batchTopUndo();
+
+ /**
+ * Removes the transaction at the top of the undo stack (if any) without
+ * transacting.
+ */
+ void removeTopUndo();
+
+ /**
+ * Returns an AddRef'd pointer to the transaction at the top of the
+ * undo stack. Callers should be aware that this method could return
+ * return a null in some implementations if there is a batch at the top
+ * of the undo stack.
+ */
+ nsITransaction peekUndoStack();
+
+ /**
+ * Returns an AddRef'd pointer to the transaction at the top of the
+ * redo stack. Callers should be aware that this method could return
+ * return a null in some implementations if there is a batch at the top
+ * of the redo stack.
+ */
+ nsITransaction peekRedoStack();
+
+%{C++
+ /**
+ * AsTransactionManager() returns a pointer to TransactionManager class.
+ *
+ * In order to avoid circular dependency issues, this method is defined
+ * in mozilla/TransactionManager.h. Consumers need to #include that header.
+ */
+ inline mozilla::TransactionManager* AsTransactionManager();
+%}
+};
diff --git a/editor/txmgr/nsTransactionManagerCID.h b/editor/txmgr/nsTransactionManagerCID.h
new file mode 100644
index 0000000000..11beb737c7
--- /dev/null
+++ b/editor/txmgr/nsTransactionManagerCID.h
@@ -0,0 +1,7 @@
+/* -*- 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/. */
+
+// XXX Needs to modify mailnews/base/src/nsMsgWindow.cpp before removing this
+// header file.
diff --git a/editor/txmgr/tests/TestTXMgr.cpp b/editor/txmgr/tests/TestTXMgr.cpp
new file mode 100644
index 0000000000..71c6cdadf4
--- /dev/null
+++ b/editor/txmgr/tests/TestTXMgr.cpp
@@ -0,0 +1,2013 @@
+/* -*- 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 "gtest/gtest.h"
+
+#include "nsITransactionManager.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Likely.h"
+#include "mozilla/EditTransactionBase.h"
+#include "mozilla/TransactionManager.h"
+
+using mozilla::EditTransactionBase;
+using mozilla::TransactionManager;
+
+static int32_t sConstructorCount = 0;
+static int32_t sDoCount = 0;
+static int32_t* sDoOrderArr = 0;
+static int32_t sUndoCount = 0;
+static int32_t* sUndoOrderArr = 0;
+static int32_t sRedoCount = 0;
+static int32_t* sRedoOrderArr = 0;
+
+int32_t sSimpleTestDoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
+ 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
+ 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75,
+ 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
+ 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105,
+ 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
+ 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131};
+
+int32_t sSimpleTestUndoOrderArr[] = {41, 40, 39, 38, 62, 39, 38, 37,
+ 69, 71, 70, 111, 110, 109, 108, 107,
+ 106, 105, 104, 103, 102, 131, 130, 129,
+ 128, 127, 126, 125, 124, 123, 122};
+
+static int32_t sSimpleTestRedoOrderArr[] = {38, 39, 70};
+
+int32_t sAggregateTestDoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
+ 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
+ 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75,
+ 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
+ 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105,
+ 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
+ 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
+ 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150,
+ 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165,
+ 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180,
+ 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
+ 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210,
+ 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225,
+ 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240,
+ 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255,
+ 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270,
+ 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285,
+ 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300,
+ 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315,
+ 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330,
+ 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345,
+ 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360,
+ 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375,
+ 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390,
+ 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405,
+ 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420,
+ 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435,
+ 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450,
+ 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465,
+ 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480,
+ 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495,
+ 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510,
+ 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525,
+ 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540,
+ 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555,
+ 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570,
+ 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585,
+ 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600,
+ 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615,
+ 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630,
+ 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645,
+ 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660,
+ 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675,
+ 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690,
+ 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705,
+ 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720,
+ 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735,
+ 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750,
+ 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765,
+ 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780,
+ 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795,
+ 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810,
+ 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825,
+ 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840,
+ 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855,
+ 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870,
+ 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885,
+ 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900,
+ 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913};
+
+int32_t sAggregateTestUndoOrderArr[] = {
+ 287, 286, 285, 284, 283, 282, 281, 280, 279, 278, 277, 276, 275, 274, 273,
+ 272, 271, 270, 269, 268, 267, 266, 265, 264, 263, 262, 261, 260, 434, 433,
+ 432, 431, 430, 429, 428, 273, 272, 271, 270, 269, 268, 267, 266, 265, 264,
+ 263, 262, 261, 260, 259, 258, 257, 256, 255, 254, 253, 479, 478, 477, 476,
+ 475, 493, 492, 491, 490, 489, 488, 487, 486, 485, 484, 483, 482, 481, 480,
+ 485, 484, 483, 482, 481, 480, 773, 772, 771, 770, 769, 768, 767, 766, 765,
+ 764, 763, 762, 761, 760, 759, 758, 757, 756, 755, 754, 753, 752, 751, 750,
+ 749, 748, 747, 746, 745, 744, 743, 742, 741, 740, 739, 738, 737, 736, 735,
+ 734, 733, 732, 731, 730, 729, 728, 727, 726, 725, 724, 723, 722, 721, 720,
+ 719, 718, 717, 716, 715, 714, 713, 712, 711, 710, 709, 708, 707, 706, 705,
+ 704, 913, 912, 911, 910, 909, 908, 907, 906, 905, 904, 903, 902, 901, 900,
+ 899, 898, 897, 896, 895, 894, 893, 892, 891, 890, 889, 888, 887, 886, 885,
+ 884, 883, 882, 881, 880, 879, 878, 877, 876, 875, 874, 873, 872, 871, 870,
+ 869, 868, 867, 866, 865, 864, 863, 862, 861, 860, 859, 858, 857, 856, 855,
+ 854, 853, 852, 851, 850, 849, 848, 847, 846, 845, 844};
+
+int32_t sAggregateTestRedoOrderArr[] = {
+ 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272,
+ 273, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486};
+
+int32_t sSimpleBatchTestDoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
+ 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
+ 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
+ 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
+ 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,
+ 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107};
+
+int32_t sSimpleBatchTestUndoOrderArr[] = {
+ 43, 42, 41, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9,
+ 8, 7, 6, 5, 4, 3, 2, 1, 43, 42, 41, 63, 62, 61, 60,
+ 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45,
+ 44, 65, 67, 66, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98};
+
+int32_t sSimpleBatchTestRedoOrderArr[] = {1, 2, 3, 4, 5, 6, 7, 8,
+ 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 41, 42, 43, 66};
+
+int32_t sAggregateBatchTestDoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
+ 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
+ 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75,
+ 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
+ 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105,
+ 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
+ 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
+ 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150,
+ 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165,
+ 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180,
+ 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
+ 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210,
+ 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225,
+ 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240,
+ 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255,
+ 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270,
+ 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285,
+ 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300,
+ 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315,
+ 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330,
+ 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345,
+ 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360,
+ 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375,
+ 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390,
+ 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405,
+ 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420,
+ 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435,
+ 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450,
+ 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465,
+ 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480,
+ 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495,
+ 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510,
+ 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525,
+ 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540,
+ 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555,
+ 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570,
+ 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585,
+ 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600,
+ 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615,
+ 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630,
+ 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645,
+ 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660,
+ 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675,
+ 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690,
+ 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705,
+ 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720,
+ 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735,
+ 736, 737, 738, 739, 740, 741, 742, 743, 744, 745};
+
+int32_t sAggregateBatchTestUndoOrderArr[] = {
+ 301, 300, 299, 298, 297, 296, 295, 294, 293, 292, 291, 290, 289, 288, 287,
+ 286, 285, 284, 283, 282, 281, 140, 139, 138, 137, 136, 135, 134, 133, 132,
+ 131, 130, 129, 128, 127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117,
+ 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102,
+ 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87,
+ 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72,
+ 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57,
+ 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42,
+ 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27,
+ 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12,
+ 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 301, 300, 299, 298,
+ 297, 296, 295, 294, 293, 292, 291, 290, 289, 288, 287, 286, 285, 284, 283,
+ 282, 281, 441, 440, 439, 438, 437, 436, 435, 434, 433, 432, 431, 430, 429,
+ 428, 427, 426, 425, 424, 423, 422, 421, 420, 419, 418, 417, 416, 415, 414,
+ 413, 412, 411, 410, 409, 408, 407, 406, 405, 404, 403, 402, 401, 400, 399,
+ 398, 397, 396, 395, 394, 393, 392, 391, 390, 389, 388, 387, 386, 385, 384,
+ 383, 382, 381, 380, 379, 378, 377, 376, 375, 374, 373, 372, 371, 370, 369,
+ 368, 367, 366, 365, 364, 363, 362, 361, 360, 359, 358, 357, 356, 355, 354,
+ 353, 352, 351, 350, 349, 348, 347, 346, 345, 344, 343, 342, 341, 340, 339,
+ 338, 337, 336, 335, 334, 333, 332, 331, 330, 329, 328, 327, 326, 325, 324,
+ 323, 322, 321, 320, 319, 318, 317, 316, 315, 314, 313, 312, 311, 310, 309,
+ 308, 307, 306, 305, 304, 303, 302, 451, 450, 449, 448, 447, 465, 464, 463,
+ 462, 461, 460, 459, 458, 457, 456, 455, 454, 453, 452, 457, 456, 455, 454,
+ 453, 452, 745, 744, 743, 742, 741, 740, 739, 738, 737, 736, 735, 734, 733,
+ 732, 731, 730, 729, 728, 727, 726, 725, 724, 723, 722, 721, 720, 719, 718,
+ 717, 716, 715, 714, 713, 712, 711, 710, 709, 708, 707, 706, 705, 704, 703,
+ 702, 701, 700, 699, 698, 697, 696, 695, 694, 693, 692, 691, 690, 689, 688,
+ 687, 686, 685, 684, 683, 682, 681, 680, 679, 678, 677, 676};
+
+int32_t sAggregateBatchTestRedoOrderArr[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
+ 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
+ 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75,
+ 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
+ 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105,
+ 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
+ 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
+ 136, 137, 138, 139, 140, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290,
+ 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 448, 449, 450, 451,
+ 452, 453, 454, 455, 456, 457, 458};
+
+class TestTransaction : public nsITransaction {
+ protected:
+ virtual ~TestTransaction() = default;
+
+ public:
+ TestTransaction() {}
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD GetAsEditTransactionBase(EditTransactionBase**) final {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+};
+
+NS_IMPL_ISUPPORTS(TestTransaction, nsITransaction)
+
+class SimpleTransaction : public TestTransaction {
+ protected:
+#define NONE_FLAG 0
+#define THROWS_DO_ERROR_FLAG 1
+#define THROWS_UNDO_ERROR_FLAG 2
+#define THROWS_REDO_ERROR_FLAG 4
+#define MERGE_FLAG 8
+#define TRANSIENT_FLAG 16
+#define BATCH_FLAG 32
+#define ALL_ERROR_FLAGS \
+ (THROWS_DO_ERROR_FLAG | THROWS_UNDO_ERROR_FLAG | THROWS_REDO_ERROR_FLAG)
+
+ int32_t mVal;
+ int32_t mFlags;
+
+ public:
+ explicit SimpleTransaction(int32_t aFlags = NONE_FLAG)
+ : mVal(++sConstructorCount), mFlags(aFlags) {}
+
+ ~SimpleTransaction() override = default;
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD DoTransaction() override {
+ //
+ // Make sure DoTransaction() is called in the order we expect!
+ // Notice that we don't check to see if we go past the end of the array.
+ // This is done on purpose since we want to crash if the order array is out
+ // of date.
+ //
+ if (sDoOrderArr) {
+ EXPECT_EQ(mVal, sDoOrderArr[sDoCount]);
+ }
+
+ ++sDoCount;
+
+ return (mFlags & THROWS_DO_ERROR_FLAG) ? NS_ERROR_FAILURE : NS_OK;
+ }
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD UndoTransaction() override {
+ //
+ // Make sure UndoTransaction() is called in the order we expect!
+ // Notice that we don't check to see if we go past the end of the array.
+ // This is done on purpose since we want to crash if the order array is out
+ // of date.
+ //
+ if (sUndoOrderArr) {
+ EXPECT_EQ(mVal, sUndoOrderArr[sUndoCount]);
+ }
+
+ ++sUndoCount;
+
+ return (mFlags & THROWS_UNDO_ERROR_FLAG) ? NS_ERROR_FAILURE : NS_OK;
+ }
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override {
+ //
+ // Make sure RedoTransaction() is called in the order we expect!
+ // Notice that we don't check to see if we go past the end of the array.
+ // This is done on purpose since we want to crash if the order array is out
+ // of date.
+ //
+ if (sRedoOrderArr) {
+ EXPECT_EQ(mVal, sRedoOrderArr[sRedoCount]);
+ }
+
+ ++sRedoCount;
+
+ return (mFlags & THROWS_REDO_ERROR_FLAG) ? NS_ERROR_FAILURE : NS_OK;
+ }
+
+ NS_IMETHOD GetIsTransient(bool* aIsTransient) override {
+ if (aIsTransient) {
+ *aIsTransient = (mFlags & TRANSIENT_FLAG) ? true : false;
+ }
+ return NS_OK;
+ }
+
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override {
+ if (aDidMerge) {
+ *aDidMerge = (mFlags & MERGE_FLAG) ? true : false;
+ }
+ return NS_OK;
+ }
+};
+
+class AggregateTransaction : public SimpleTransaction {
+ private:
+ AggregateTransaction(nsITransactionManager* aTXMgr, int32_t aLevel,
+ int32_t aNumber, int32_t aMaxLevel,
+ int32_t aNumChildrenPerNode, int32_t aFlags) {
+ mLevel = aLevel;
+ mNumber = aNumber;
+ mTXMgr = aTXMgr;
+ mFlags = aFlags & (~ALL_ERROR_FLAGS);
+ mErrorFlags = aFlags & ALL_ERROR_FLAGS;
+ mTXMgr = aTXMgr;
+ mMaxLevel = aMaxLevel;
+ mNumChildrenPerNode = aNumChildrenPerNode;
+ }
+
+ nsITransactionManager* mTXMgr;
+
+ int32_t mLevel;
+ int32_t mNumber;
+ int32_t mErrorFlags;
+
+ int32_t mMaxLevel;
+ int32_t mNumChildrenPerNode;
+
+ public:
+ AggregateTransaction(nsITransactionManager* aTXMgr, int32_t aMaxLevel,
+ int32_t aNumChildrenPerNode,
+ int32_t aFlags = NONE_FLAG) {
+ mLevel = 1;
+ mNumber = 1;
+ mFlags = aFlags & (~ALL_ERROR_FLAGS);
+ mErrorFlags = aFlags & ALL_ERROR_FLAGS;
+ mTXMgr = aTXMgr;
+ mMaxLevel = aMaxLevel;
+ mNumChildrenPerNode = aNumChildrenPerNode;
+ }
+
+ ~AggregateTransaction() override = default;
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD DoTransaction() override {
+ if (mLevel >= mMaxLevel) {
+ // Only leaf nodes can throw errors!
+ mFlags |= mErrorFlags;
+ }
+
+ nsresult rv = SimpleTransaction::DoTransaction();
+ if (NS_FAILED(rv)) {
+ // fail("QueryInterface() failed for transaction level %d. (%d)\n",
+ // mLevel, rv);
+ return rv;
+ }
+
+ if (mLevel >= mMaxLevel) {
+ return NS_OK;
+ }
+
+ if (mFlags & BATCH_FLAG) {
+ rv = MOZ_KnownLive(mTXMgr)->BeginBatch(nullptr);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ int32_t cLevel = mLevel + 1;
+
+ for (int i = 1; i <= mNumChildrenPerNode; i++) {
+ int32_t flags = mErrorFlags & THROWS_DO_ERROR_FLAG;
+
+ if ((mErrorFlags & THROWS_REDO_ERROR_FLAG) && i == mNumChildrenPerNode) {
+ // Make the rightmost leaf transaction throw the error!
+ flags = THROWS_REDO_ERROR_FLAG;
+ mErrorFlags = mErrorFlags & (~THROWS_REDO_ERROR_FLAG);
+ } else if ((mErrorFlags & THROWS_UNDO_ERROR_FLAG) && i == 1) {
+ // Make the leftmost leaf transaction throw the error!
+ flags = THROWS_UNDO_ERROR_FLAG;
+ mErrorFlags = mErrorFlags & (~THROWS_UNDO_ERROR_FLAG);
+ }
+
+ flags |= mFlags & BATCH_FLAG;
+
+ RefPtr<AggregateTransaction> tximpl = new AggregateTransaction(
+ mTXMgr, cLevel, i, mMaxLevel, mNumChildrenPerNode, flags);
+
+ rv = MOZ_KnownLive(mTXMgr)->DoTransaction(tximpl);
+ if (NS_FAILED(rv)) {
+ if (mFlags & BATCH_FLAG) {
+ mTXMgr->EndBatch(false);
+ }
+ return rv;
+ }
+ }
+
+ if (mFlags & BATCH_FLAG) {
+ mTXMgr->EndBatch(false);
+ }
+ return rv;
+ }
+};
+
+class TestTransactionFactory {
+ public:
+ virtual TestTransaction* create(nsITransactionManager* txmgr,
+ int32_t flags) = 0;
+};
+
+class SimpleTransactionFactory : public TestTransactionFactory {
+ public:
+ TestTransaction* create(nsITransactionManager* txmgr,
+ int32_t flags) override {
+ return (TestTransaction*)new SimpleTransaction(flags);
+ }
+};
+
+class AggregateTransactionFactory : public TestTransactionFactory {
+ private:
+ int32_t mMaxLevel;
+ int32_t mNumChildrenPerNode;
+ int32_t mFixedFlags;
+
+ public:
+ AggregateTransactionFactory(int32_t aMaxLevel, int32_t aNumChildrenPerNode,
+ int32_t aFixedFlags = NONE_FLAG)
+ : mMaxLevel(aMaxLevel),
+ mNumChildrenPerNode(aNumChildrenPerNode),
+ mFixedFlags(aFixedFlags) {}
+
+ TestTransaction* create(nsITransactionManager* txmgr,
+ int32_t flags) override {
+ return (TestTransaction*)new AggregateTransaction(
+ txmgr, mMaxLevel, mNumChildrenPerNode, flags | mFixedFlags);
+ }
+};
+
+void reset_globals() {
+ sConstructorCount = 0;
+
+ sDoCount = 0;
+ sDoOrderArr = 0;
+
+ sUndoCount = 0;
+ sUndoOrderArr = 0;
+
+ sRedoCount = 0;
+ sRedoOrderArr = 0;
+}
+
+/**
+ * Test behaviors in non-batch mode.
+ **/
+MOZ_CAN_RUN_SCRIPT_BOUNDARY void quick_test(TestTransactionFactory* factory) {
+ /*******************************************************************
+ *
+ * Create a transaction manager implementation:
+ *
+ *******************************************************************/
+
+ nsCOMPtr<nsITransactionManager> mgr = new TransactionManager();
+
+ /*******************************************************************
+ *
+ * Call DoTransaction() with a null transaction:
+ *
+ *******************************************************************/
+
+ nsresult rv = mgr->DoTransaction(nullptr);
+ EXPECT_EQ(rv, NS_ERROR_NULL_POINTER);
+
+ /*******************************************************************
+ *
+ * Call UndoTransaction() with an empty undo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ /*******************************************************************
+ *
+ * Call RedoTransaction() with an empty redo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->RedoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ /*******************************************************************
+ *
+ * Call SetMaxTransactionCount(-1) with empty undo and redo stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(-1);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ /*******************************************************************
+ *
+ * Call SetMaxTransactionCount(0) with empty undo and redo stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(0);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ /*******************************************************************
+ *
+ * Call SetMaxTransactionCount(10) with empty undo and redo stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(10);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ /*******************************************************************
+ *
+ * Call Clear() with empty undo and redo stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->Clear();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ /*******************************************************************
+ *
+ * Call GetNumberOfUndoItems() with an empty undo stack:
+ *
+ *******************************************************************/
+
+ int32_t numitems;
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Call GetNumberOfRedoItems() with an empty redo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Call PeekUndoStack() with an empty undo stack:
+ *
+ *******************************************************************/
+
+ {
+ nsCOMPtr<nsITransaction> tx;
+ rv = mgr->PeekUndoStack(getter_AddRefs(tx));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(tx, nullptr);
+ }
+
+ /*******************************************************************
+ *
+ * Call PeekRedoStack() with an empty undo stack:
+ *
+ *******************************************************************/
+
+ {
+ nsCOMPtr<nsITransaction> tx;
+ rv = mgr->PeekRedoStack(getter_AddRefs(tx));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(tx, nullptr);
+ }
+
+ /*******************************************************************
+ *
+ * Test coalescing by executing a transaction that can merge any
+ * command into itself. Then execute 20 transaction. Afterwards,
+ * we should still have the first transaction sitting on the undo
+ * stack. Then clear the undo and redo stacks.
+ *
+ *******************************************************************/
+
+ int32_t i;
+ RefPtr<TestTransaction> tximpl;
+ nsCOMPtr<nsITransaction> u1, u2, r1, r2;
+
+ rv = mgr->SetMaxTransactionCount(10);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ tximpl = factory->create(mgr, MERGE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, tximpl);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->Clear();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ /*******************************************************************
+ *
+ * Execute 20 transactions. Afterwards, we should have 10
+ * transactions on the undo stack:
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Execute 20 transient transactions. Afterwards, we should still
+ * have the same 10 transactions on the undo stack:
+ *
+ *******************************************************************/
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, TRANSIENT_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Undo 4 transactions. Afterwards, we should have 6 transactions
+ * on the undo stack, and 4 on the redo stack:
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 4; i++) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 6);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 4);
+
+ /*******************************************************************
+ *
+ * Redo 2 transactions. Afterwards, we should have 8 transactions
+ * on the undo stack, and 2 on the redo stack:
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->RedoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 8);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 2);
+
+ /*******************************************************************
+ *
+ * Execute a new transaction. The redo stack should get pruned!
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 9);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Undo 4 transactions then clear the undo and redo stacks.
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 4; ++i) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 5);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 4);
+
+ rv = mgr->Clear();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Execute 5 transactions.
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 5; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 5);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Test transaction DoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_DO_ERROR_FLAG);
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 5);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Test transaction UndoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_UNDO_ERROR_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->UndoTransaction();
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 6);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Test transaction RedoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_REDO_ERROR_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ //
+ // Execute a normal transaction to be used in a later test:
+ //
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ //
+ // Undo the 2 transactions just executed.
+ //
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ //
+ // The RedoErrorTransaction should now be at the top of the redo stack!
+ //
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->RedoTransaction();
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 6);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 2);
+
+ /*******************************************************************
+ *
+ * Make sure that setting the transaction manager's max transaction
+ * count to zero, clears both the undo and redo stacks, and executes
+ * all new commands without pushing them on the undo stack!
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(0);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+ }
+
+ /*******************************************************************
+ *
+ * Make sure that setting the transaction manager's max transaction
+ * count to something greater than the number of transactions on
+ * both the undo and redo stacks causes no pruning of the stacks:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(-1);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ // Push 20 transactions on the undo stack:
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, i);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+ }
+
+ for (i = 1; i <= 10; i++) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->SetMaxTransactionCount(25);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ /*******************************************************************
+ *
+ * Test undo stack pruning by setting the transaction
+ * manager's max transaction count to a number lower than the
+ * number of transactions on both the undo and redo stacks:
+ *
+ *******************************************************************/
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->SetMaxTransactionCount(15);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 5);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ /*******************************************************************
+ *
+ * Test redo stack pruning by setting the transaction
+ * manager's max transaction count to a number lower than the
+ * number of transactions on both the undo and redo stacks:
+ *
+ *******************************************************************/
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->SetMaxTransactionCount(5);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_FALSE(u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 5);
+
+ /*******************************************************************
+ *
+ * Release the transaction manager. Any transactions on the undo
+ * and redo stack should automatically be released:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(-1);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ // Push 20 transactions on the undo stack:
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, i);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+ }
+
+ for (i = 1; i <= 10; i++) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ rv = mgr->Clear();
+ EXPECT_NS_SUCCEEDED(rv);
+}
+
+TEST(TestTXMgr, SimpleTest)
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+ reset_globals();
+ sDoOrderArr = sSimpleTestDoOrderArr;
+ sUndoOrderArr = sSimpleTestUndoOrderArr;
+ sRedoOrderArr = sSimpleTestRedoOrderArr;
+
+ /*******************************************************************
+ *
+ * Run the quick test.
+ *
+ *******************************************************************/
+
+ SimpleTransactionFactory factory;
+
+ quick_test(&factory);
+}
+
+TEST(TestTXMgr, AggregationTest)
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+ sDoOrderArr = sAggregateTestDoOrderArr;
+ sUndoOrderArr = sAggregateTestUndoOrderArr;
+ sRedoOrderArr = sAggregateTestRedoOrderArr;
+
+ /*******************************************************************
+ *
+ * Run the quick test.
+ *
+ *******************************************************************/
+
+ AggregateTransactionFactory factory(3, 2);
+
+ quick_test(&factory);
+}
+
+/**
+ * Test behaviors in batch mode.
+ **/
+MOZ_CAN_RUN_SCRIPT_BOUNDARY void quick_batch_test(
+ TestTransactionFactory* factory) {
+ /*******************************************************************
+ *
+ * Create a transaction manager implementation:
+ *
+ *******************************************************************/
+
+ nsCOMPtr<nsITransactionManager> mgr = new TransactionManager();
+
+ int32_t numitems;
+
+ /*******************************************************************
+ *
+ * Make sure an unbalanced call to EndBatch(false) with empty undo stack
+ * throws an error!
+ *
+ *******************************************************************/
+
+ nsresult rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Make sure that an empty batch is not added to the undo stack
+ * when it is closed.
+ *
+ *******************************************************************/
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ int32_t i;
+ RefPtr<TestTransaction> tximpl;
+
+ /*******************************************************************
+ *
+ * Execute 20 transactions. Afterwards, we should have 1
+ * transaction on the undo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ nsCOMPtr<nsITransaction> u1, u2, r1, r2;
+
+ /*******************************************************************
+ *
+ * Execute 20 transient transactions. Afterwards, we should still
+ * have the same transaction on the undo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, TRANSIENT_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Test nested batching. Afterwards, we should have 2 transactions
+ * on the undo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 2);
+
+ /*******************************************************************
+ *
+ * Undo 2 batch transactions. Afterwards, we should have 0
+ * transactions on the undo stack and 2 on the redo stack.
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 2);
+
+ /*******************************************************************
+ *
+ * Redo 2 batch transactions. Afterwards, we should have 2
+ * transactions on the undo stack and 0 on the redo stack.
+ *
+ *******************************************************************/
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->RedoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 2);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Call undo. Afterwards, we should have 1 transaction
+ * on the undo stack, and 1 on the redo stack:
+ *
+ *******************************************************************/
+
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ /*******************************************************************
+ *
+ * Make sure an unbalanced call to EndBatch(false) throws an error and
+ * doesn't affect the undo and redo stacks!
+ *
+ *******************************************************************/
+
+ rv = mgr->EndBatch(false);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ /*******************************************************************
+ *
+ * Make sure that an empty batch is not added to the undo stack
+ * when it is closed, and that it does not affect the undo and redo
+ * stacks.
+ *
+ *******************************************************************/
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ /*******************************************************************
+ *
+ * Execute a new transaction. The redo stack should get pruned!
+ *
+ *******************************************************************/
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 2);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Call undo.
+ *
+ *******************************************************************/
+
+ // Move a transaction over to the redo stack, so that we have one
+ // transaction on the undo stack, and one on the redo stack!
+
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ /*******************************************************************
+ *
+ * Test transaction DoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_DO_ERROR_FLAG);
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 1);
+
+ /*******************************************************************
+ *
+ * Test transaction UndoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_UNDO_ERROR_FLAG);
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->UndoTransaction();
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 2);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ /*******************************************************************
+ *
+ * Test transaction RedoTransaction() error:
+ *
+ *******************************************************************/
+
+ tximpl = factory->create(mgr, THROWS_REDO_ERROR_FLAG);
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ //
+ // Execute a normal transaction to be used in a later test:
+ //
+
+ tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ //
+ // Undo the 2 transactions just executed.
+ //
+
+ for (i = 1; i <= 2; ++i) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ //
+ // The RedoErrorTransaction should now be at the top of the redo stack!
+ //
+
+ u1 = u2 = r1 = r2 = nullptr;
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r1));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->RedoTransaction();
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+
+ rv = mgr->PeekUndoStack(getter_AddRefs(u2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(u1, u2);
+
+ rv = mgr->PeekRedoStack(getter_AddRefs(r2));
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(r1, r2);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 2);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 2);
+
+ /*******************************************************************
+ *
+ * Make sure that setting the transaction manager's max transaction
+ * count to zero, clears both the undo and redo stacks, and executes
+ * all new commands without pushing them on the undo stack!
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(0);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+ }
+
+ /*******************************************************************
+ *
+ * Release the transaction manager. Any transactions on the undo
+ * and redo stack should automatically be released:
+ *
+ *******************************************************************/
+
+ rv = mgr->SetMaxTransactionCount(-1);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ // Push 20 transactions on the undo stack:
+
+ for (i = 1; i <= 20; i++) {
+ tximpl = factory->create(mgr, NONE_FLAG);
+
+ rv = mgr->BeginBatch(nullptr);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->EndBatch(false);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, i);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 0);
+ }
+
+ for (i = 1; i <= 10; i++) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+ rv = mgr->GetNumberOfUndoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ rv = mgr->GetNumberOfRedoItems(&numitems);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_EQ(numitems, 10);
+
+ rv = mgr->Clear();
+ EXPECT_NS_SUCCEEDED(rv);
+}
+
+TEST(TestTXMgr, SimpleBatchTest)
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+ reset_globals();
+ sDoOrderArr = sSimpleBatchTestDoOrderArr;
+ sUndoOrderArr = sSimpleBatchTestUndoOrderArr;
+ sRedoOrderArr = sSimpleBatchTestRedoOrderArr;
+
+ /*******************************************************************
+ *
+ * Run the quick batch test.
+ *
+ *******************************************************************/
+
+ SimpleTransactionFactory factory;
+ quick_batch_test(&factory);
+}
+
+TEST(TestTXMgr, AggregationBatchTest)
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+ sDoOrderArr = sAggregateBatchTestDoOrderArr;
+ sUndoOrderArr = sAggregateBatchTestUndoOrderArr;
+ sRedoOrderArr = sAggregateBatchTestRedoOrderArr;
+
+ /*******************************************************************
+ *
+ * Run the quick batch test.
+ *
+ *******************************************************************/
+
+ AggregateTransactionFactory factory(3, 2, BATCH_FLAG);
+
+ quick_batch_test(&factory);
+}
+
+/**
+ * Create 'iterations * (iterations + 1) / 2' transactions;
+ * do/undo/redo/undo them.
+ **/
+MOZ_CAN_RUN_SCRIPT_BOUNDARY void stress_test(TestTransactionFactory* factory,
+ int32_t iterations) {
+ /*******************************************************************
+ *
+ * Create a transaction manager:
+ *
+ *******************************************************************/
+
+ nsCOMPtr<nsITransactionManager> mgr = new TransactionManager();
+
+ nsresult rv;
+ int32_t i, j;
+
+ for (i = 1; i <= iterations; i++) {
+ /*******************************************************************
+ *
+ * Create and execute a bunch of transactions:
+ *
+ *******************************************************************/
+
+ for (j = 1; j <= i; j++) {
+ RefPtr<TestTransaction> tximpl = factory->create(mgr, NONE_FLAG);
+ rv = mgr->DoTransaction(tximpl);
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ /*******************************************************************
+ *
+ * Undo all the transactions:
+ *
+ *******************************************************************/
+
+ for (j = 1; j <= i; j++) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ /*******************************************************************
+ *
+ * Redo all the transactions:
+ *
+ *******************************************************************/
+
+ for (j = 1; j <= i; j++) {
+ rv = mgr->RedoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ /*******************************************************************
+ *
+ * Undo all the transactions again so that they all end up on
+ * the redo stack for pruning the next time we execute a new
+ * transaction
+ *
+ *******************************************************************/
+
+ for (j = 1; j <= i; j++) {
+ rv = mgr->UndoTransaction();
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+ }
+
+ rv = mgr->Clear();
+ EXPECT_NS_SUCCEEDED(rv);
+}
+
+TEST(TestTXMgr, SimpleStressTest)
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+
+ /*******************************************************************
+ *
+ * Do the stress test:
+ *
+ *******************************************************************/
+
+ SimpleTransactionFactory factory;
+
+ int32_t iterations =
+#ifdef DEBUG
+ 10
+#else
+ //
+ // 1500 iterations sends 1,125,750 transactions through the system!!
+ //
+ 1500
+#endif
+ ;
+ stress_test(&factory, iterations);
+}
+
+TEST(TestTXMgr, AggregationStressTest)
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+
+ /*******************************************************************
+ *
+ * Do the stress test:
+ *
+ *******************************************************************/
+
+ AggregateTransactionFactory factory(3, 4);
+
+ int32_t iterations =
+#ifdef DEBUG
+ 10
+#else
+ //
+ // 500 iterations sends 2,630,250 transactions through the system!!
+ //
+ 500
+#endif
+ ;
+ stress_test(&factory, iterations);
+}
+
+TEST(TestTXMgr, AggregationBatchStressTest)
+{
+ /*******************************************************************
+ *
+ * Initialize globals for test.
+ *
+ *******************************************************************/
+
+ reset_globals();
+
+ /*******************************************************************
+ *
+ * Do the stress test:
+ *
+ *******************************************************************/
+
+ AggregateTransactionFactory factory(3, 4, BATCH_FLAG);
+
+ int32_t iterations =
+#ifdef DEBUG
+ 10
+#else
+# if defined(MOZ_ASAN) || defined(MOZ_WIDGET_ANDROID)
+ // See Bug 929985: 500 is too many for ASAN and Android, 100 is safe.
+ 100
+# else
+ //
+ // 500 iterations sends 2,630,250 transactions through the system!!
+ //
+ 500
+# endif
+#endif
+ ;
+ stress_test(&factory, iterations);
+}
diff --git a/editor/txmgr/tests/crashtests/407072-1.html b/editor/txmgr/tests/crashtests/407072-1.html
new file mode 100644
index 0000000000..e3aa3c216a
--- /dev/null
+++ b/editor/txmgr/tests/crashtests/407072-1.html
@@ -0,0 +1,22 @@
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var br = document.getElementById("br");
+ br.contentEditable = "true";
+ br.focus();
+
+ try { document.execCommand("justifyfull", false, null); } catch(e) { }
+ try { document.execCommand("justifyfull", false, null); } catch(e) { }
+ document.execCommand("underline", false, null);
+ document.execCommand("insertimage", false, "foo");
+ try { document.execCommand("outdent", false, null); } catch(e) { }
+}
+
+</script>
+</head>
+
+<body onload="boom();"><br id="br"></body>
+</html>
diff --git a/editor/txmgr/tests/crashtests/449006-1.html b/editor/txmgr/tests/crashtests/449006-1.html
new file mode 100644
index 0000000000..723792a239
--- /dev/null
+++ b/editor/txmgr/tests/crashtests/449006-1.html
@@ -0,0 +1,28 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.getElementById("d").focus();
+ document.execCommand("inserthtml", false, "AB");
+ document.execCommand("delete", false, null);
+ document.execCommand("undo", false, null);
+
+ document.addEventListener("DOMCharacterDataModified", f);
+ document.execCommand("redo", false, null);
+ document.removeEventListener("DOMCharacterDataModified", f);
+
+ function f()
+ {
+ document.removeEventListener("DOMCharacterDataModified", f);
+ document.execCommand("formatBlock", false, "<h3>");
+ }
+}
+
+</script>
+</head>
+
+<body onload="boom();"><div id="d" contenteditable="true"></div></body>
+
+</html>
diff --git a/editor/txmgr/tests/crashtests/crashtests.list b/editor/txmgr/tests/crashtests/crashtests.list
new file mode 100644
index 0000000000..d391875925
--- /dev/null
+++ b/editor/txmgr/tests/crashtests/crashtests.list
@@ -0,0 +1,2 @@
+needs-focus load 407072-1.html
+load 449006-1.html
diff --git a/editor/txmgr/tests/moz.build b/editor/txmgr/tests/moz.build
new file mode 100644
index 0000000000..9a8985dbc4
--- /dev/null
+++ b/editor/txmgr/tests/moz.build
@@ -0,0 +1,10 @@
+# -*- 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/.
+
+SOURCES += [
+ "TestTXMgr.cpp",
+]
+FINAL_LIBRARY = "xul-gtest"