From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- devtools/client/inspector/markup/markup.js | 1878 +++++++++++++++++ devtools/client/inspector/markup/markup.xhtml | 105 + devtools/client/inspector/markup/moz.build | 16 + devtools/client/inspector/markup/test/.eslintrc.js | 6 + .../inspector/markup/test/actor_events_form.js | 62 + devtools/client/inspector/markup/test/browser.ini | 155 ++ .../browser_markup_accessibility_focus_blur.js | 59 + .../browser_markup_accessibility_navigation.js | 277 +++ ...r_markup_accessibility_navigation_after_edit.js | 126 ++ .../test/browser_markup_accessibility_semantics.js | 100 + .../markup/test/browser_markup_anonymous_01.js | 44 + .../markup/test/browser_markup_anonymous_02.js | 31 + .../markup/test/browser_markup_anonymous_03.js | 34 + .../markup/test/browser_markup_anonymous_04.js | 37 + .../markup/test/browser_markup_copy_image_data.js | 67 + ...ser_markup_css_completion_style_attribute_01.js | 76 + ...ser_markup_css_completion_style_attribute_02.js | 106 + ...ser_markup_css_completion_style_attribute_03.js | 54 + .../test/browser_markup_dragdrop_autoscroll_01.js | 51 + .../test/browser_markup_dragdrop_autoscroll_02.js | 49 + .../test/browser_markup_dragdrop_distance.js | 49 + .../test/browser_markup_dragdrop_dragRootNode.js | 22 + .../test/browser_markup_dragdrop_draggable.js | 63 + .../test/browser_markup_dragdrop_escapeKeyPress.js | 34 + .../test/browser_markup_dragdrop_invalidNodes.js | 48 + .../markup/test/browser_markup_dragdrop_reorder.js | 109 + .../markup/test/browser_markup_dragdrop_tooltip.js | 35 + .../markup/test/browser_markup_events-overflow.js | 91 + .../test/browser_markup_events-windowed-host.js | 61 + .../markup/test/browser_markup_events1.js | 149 ++ .../markup/test/browser_markup_events2.js | 163 ++ .../markup/test/browser_markup_events3.js | 161 ++ .../markup/test/browser_markup_events_form.js | 61 + .../test/browser_markup_events_jquery_1.0.js | 237 +++ .../test/browser_markup_events_jquery_1.1.js | 271 +++ .../test/browser_markup_events_jquery_1.11.1.js | 196 ++ .../test/browser_markup_events_jquery_1.2.js | 191 ++ .../test/browser_markup_events_jquery_1.3.js | 224 ++ .../test/browser_markup_events_jquery_1.4.js | 287 +++ .../test/browser_markup_events_jquery_1.6.js | 388 ++++ .../test/browser_markup_events_jquery_1.7.js | 234 +++ .../test/browser_markup_events_jquery_2.1.1.js | 196 ++ .../markup/test/browser_markup_html_edit_01.js | 84 + .../markup/test/browser_markup_html_edit_02.js | 119 ++ .../markup/test/browser_markup_html_edit_03.js | 200 ++ .../markup/test/browser_markup_image_tooltip.js | 60 + .../test/browser_markup_image_tooltip_mutations.js | 83 + .../markup/test/browser_markup_keybindings_01.js | 49 + .../markup/test/browser_markup_keybindings_02.js | 32 + .../markup/test/browser_markup_keybindings_03.js | 50 + .../markup/test/browser_markup_keybindings_04.js | 58 + ...browser_markup_keybindings_delete_attributes.js | 63 + .../browser_markup_keybindings_scrolltonode.js | 87 + .../markup/test/browser_markup_links_01.js | 128 ++ .../markup/test/browser_markup_links_02.js | 38 + .../markup/test/browser_markup_links_03.js | 38 + .../markup/test/browser_markup_links_04.js | 116 ++ .../markup/test/browser_markup_links_05.js | 69 + .../markup/test/browser_markup_links_06.js | 53 + .../markup/test/browser_markup_links_07.js | 109 + .../markup/test/browser_markup_load_01.js | 71 + .../markup/test/browser_markup_mutation_01.js | 340 +++ .../markup/test/browser_markup_mutation_02.js | 159 ++ .../markup/test/browser_markup_navigation.js | 147 ++ .../markup/test/browser_markup_node_names.js | 28 + .../test/browser_markup_node_names_namespaced.js | 43 + .../test/browser_markup_node_not_displayed_01.js | 35 + .../test/browser_markup_node_not_displayed_02.js | 150 ++ .../markup/test/browser_markup_pagesize_01.js | 86 + .../markup/test/browser_markup_pagesize_02.js | 47 + .../test/browser_markup_remove_xul_attributes.js | 28 + .../markup/test/browser_markup_search_01.js | 51 + .../markup/test/browser_markup_tag_edit_01.js | 68 + .../markup/test/browser_markup_tag_edit_02.js | 44 + .../markup/test/browser_markup_tag_edit_03.js | 51 + .../test/browser_markup_tag_edit_04-backspace.js | 59 + .../test/browser_markup_tag_edit_04-delete.js | 59 + .../markup/test/browser_markup_tag_edit_05.js | 77 + .../markup/test/browser_markup_tag_edit_06.js | 85 + .../markup/test/browser_markup_tag_edit_07.js | 135 ++ .../markup/test/browser_markup_tag_edit_08.js | 132 ++ .../markup/test/browser_markup_tag_edit_09.js | 71 + .../markup/test/browser_markup_tag_edit_10.js | 34 + .../markup/test/browser_markup_tag_edit_11.js | 38 + .../markup/test/browser_markup_tag_edit_12.js | 98 + .../test/browser_markup_tag_edit_13-other.js | 38 + .../test/browser_markup_tag_edit_long-classname.js | 41 + .../test/browser_markup_textcontent_display.js | 89 + .../test/browser_markup_textcontent_edit_01.js | 84 + .../test/browser_markup_textcontent_edit_02.js | 116 ++ .../markup/test/browser_markup_toggle_01.js | 58 + .../markup/test/browser_markup_toggle_02.js | 49 + .../markup/test/browser_markup_toggle_03.js | 35 + .../test/browser_markup_update-on-navigtion.js | 44 + .../test/browser_markup_void_elements_html.js | 44 + .../test/browser_markup_void_elements_xhtml.js | 28 + .../markup/test/browser_markup_whitespace.js | 66 + .../markup/test/doc_markup_anonymous.html | 34 + .../inspector/markup/test/doc_markup_dragdrop.html | 23 + .../test/doc_markup_dragdrop_autoscroll_01.html | 87 + .../test/doc_markup_dragdrop_autoscroll_02.html | 40 + .../inspector/markup/test/doc_markup_edit.html | 48 + .../markup/test/doc_markup_events-overflow.html | 19 + .../inspector/markup/test/doc_markup_events1.html | 113 + .../inspector/markup/test/doc_markup_events2.html | 111 + .../inspector/markup/test/doc_markup_events3.html | 115 ++ .../markup/test/doc_markup_events_form.html | 11 + .../markup/test/doc_markup_events_jquery.html | 67 + .../inspector/markup/test/doc_markup_flashing.html | 15 + .../markup/test/doc_markup_html_mixed_case.html | 12 + .../markup/test/doc_markup_image_and_canvas.html | 24 + .../markup/test/doc_markup_image_and_canvas_2.html | 25 + .../inspector/markup/test/doc_markup_links.html | 42 + .../inspector/markup/test/doc_markup_mutation.html | 42 + .../markup/test/doc_markup_navigation.html | 28 + .../markup/test/doc_markup_not_displayed.html | 18 + .../markup/test/doc_markup_pagesize_01.html | 32 + .../markup/test/doc_markup_pagesize_02.html | 33 + .../inspector/markup/test/doc_markup_search.html | 11 + .../markup/test/doc_markup_svg_attributes.html | 8 + .../inspector/markup/test/doc_markup_toggle.html | 28 + .../inspector/markup/test/doc_markup_tooltip.png | Bin 0 -> 1095 bytes .../markup/test/doc_markup_void_elements.html | 18 + .../markup/test/doc_markup_void_elements.xhtml | 21 + .../markup/test/doc_markup_whitespace.html | 25 + .../inspector/markup/test/doc_markup_xul.xul | 9 + devtools/client/inspector/markup/test/head.js | 653 ++++++ .../markup/test/helper_attributes_test_runner.js | 160 ++ .../markup/test/helper_events_test_runner.js | 111 + .../test/helper_markup_accessibility_navigation.js | 70 + .../markup/test/helper_outerhtml_test_runner.js | 82 + .../markup/test/helper_style_attr_test_runner.js | 132 ++ .../client/inspector/markup/test/lib_jquery_1.0.js | 1814 ++++++++++++++++ .../client/inspector/markup/test/lib_jquery_1.1.js | 2172 ++++++++++++++++++++ .../inspector/markup/test/lib_jquery_1.11.1_min.js | 4 + .../inspector/markup/test/lib_jquery_1.2_min.js | 32 + .../inspector/markup/test/lib_jquery_1.3_min.js | 19 + .../inspector/markup/test/lib_jquery_1.4_min.js | 151 ++ .../inspector/markup/test/lib_jquery_1.6_min.js | 16 + .../inspector/markup/test/lib_jquery_1.7_min.js | 4 + .../inspector/markup/test/lib_jquery_2.1.1_min.js | 4 + devtools/client/inspector/markup/utils.js | 135 ++ .../inspector/markup/views/element-container.js | 193 ++ .../inspector/markup/views/element-editor.js | 560 +++++ .../client/inspector/markup/views/html-editor.js | 180 ++ .../inspector/markup/views/markup-container.js | 720 +++++++ devtools/client/inspector/markup/views/moz.build | 17 + .../inspector/markup/views/read-only-container.js | 33 + .../inspector/markup/views/read-only-editor.js | 43 + .../inspector/markup/views/root-container.js | 55 + .../inspector/markup/views/text-container.js | 40 + .../client/inspector/markup/views/text-editor.js | 109 + 152 files changed, 19435 insertions(+) create mode 100644 devtools/client/inspector/markup/markup.js create mode 100644 devtools/client/inspector/markup/markup.xhtml create mode 100644 devtools/client/inspector/markup/moz.build create mode 100644 devtools/client/inspector/markup/test/.eslintrc.js create mode 100644 devtools/client/inspector/markup/test/actor_events_form.js create mode 100644 devtools/client/inspector/markup/test/browser.ini create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_anonymous_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_copy_image_data.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events-overflow.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events2.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events3.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_form.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_html_edit_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_image_tooltip.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_04.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_05.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_06.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_links_07.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_load_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_mutation_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_mutation_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_navigation.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_names.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_pagesize_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_pagesize_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_search_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_textcontent_display.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_01.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_02.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_toggle_03.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_void_elements_html.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js create mode 100644 devtools/client/inspector/markup/test/browser_markup_whitespace.js create mode 100644 devtools/client/inspector/markup/test/doc_markup_anonymous.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_dragdrop.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_edit.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events-overflow.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events1.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events2.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events3.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_form.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_events_jquery.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_flashing.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_links.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_mutation.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_navigation.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_not_displayed.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_pagesize_01.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_pagesize_02.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_search.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_svg_attributes.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_toggle.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_tooltip.png create mode 100644 devtools/client/inspector/markup/test/doc_markup_void_elements.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml create mode 100644 devtools/client/inspector/markup/test/doc_markup_whitespace.html create mode 100644 devtools/client/inspector/markup/test/doc_markup_xul.xul create mode 100644 devtools/client/inspector/markup/test/head.js create mode 100644 devtools/client/inspector/markup/test/helper_attributes_test_runner.js create mode 100644 devtools/client/inspector/markup/test/helper_events_test_runner.js create mode 100644 devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js create mode 100644 devtools/client/inspector/markup/test/helper_outerhtml_test_runner.js create mode 100644 devtools/client/inspector/markup/test/helper_style_attr_test_runner.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.0.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.1.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.2_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.3_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.4_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.6_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_1.7_min.js create mode 100644 devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js create mode 100644 devtools/client/inspector/markup/utils.js create mode 100644 devtools/client/inspector/markup/views/element-container.js create mode 100644 devtools/client/inspector/markup/views/element-editor.js create mode 100644 devtools/client/inspector/markup/views/html-editor.js create mode 100644 devtools/client/inspector/markup/views/markup-container.js create mode 100644 devtools/client/inspector/markup/views/moz.build create mode 100644 devtools/client/inspector/markup/views/read-only-container.js create mode 100644 devtools/client/inspector/markup/views/read-only-editor.js create mode 100644 devtools/client/inspector/markup/views/root-container.js create mode 100644 devtools/client/inspector/markup/views/text-container.js create mode 100644 devtools/client/inspector/markup/views/text-editor.js (limited to 'devtools/client/inspector/markup') diff --git a/devtools/client/inspector/markup/markup.js b/devtools/client/inspector/markup/markup.js new file mode 100644 index 000000000..d6e9f8c11 --- /dev/null +++ b/devtools/client/inspector/markup/markup.js @@ -0,0 +1,1878 @@ +/* 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/. */ + +"use strict"; + +const promise = require("promise"); +const Services = require("Services"); +const defer = require("devtools/shared/defer"); +const {Task} = require("devtools/shared/task"); +const nodeConstants = require("devtools/shared/dom-node-constants"); +const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {LocalizationHelper} = require("devtools/shared/l10n"); +const {PluralForm} = require("devtools/shared/plural-form"); +const {template} = require("devtools/shared/gcli/templater"); +const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll"); +const {UndoStack} = require("devtools/client/shared/undo"); +const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip"); +const {PrefObserver} = require("devtools/client/styleeditor/utils"); +const HTMLEditor = require("devtools/client/inspector/markup/views/html-editor"); +const MarkupElementContainer = require("devtools/client/inspector/markup/views/element-container"); +const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/read-only-container"); +const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container"); +const RootContainer = require("devtools/client/inspector/markup/views/root-container"); + +const INSPECTOR_L10N = + new LocalizationHelper("devtools/client/locales/inspector.properties"); + +// Page size for pageup/pagedown +const PAGE_SIZE = 10; +const DEFAULT_MAX_CHILDREN = 100; +const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000; +const DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE = 50; +const DRAG_DROP_AUTOSCROLL_EDGE_RATIO = 0.1; +const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 2; +const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 8; +const DRAG_DROP_HEIGHT_TO_SPEED = 500; +const DRAG_DROP_HEIGHT_TO_SPEED_MIN = 0.5; +const DRAG_DROP_HEIGHT_TO_SPEED_MAX = 1; +const ATTR_COLLAPSE_ENABLED_PREF = "devtools.markup.collapseAttributes"; +const ATTR_COLLAPSE_LENGTH_PREF = "devtools.markup.collapseAttributeLength"; + +/** + * Vocabulary for the purposes of this file: + * + * MarkupContainer - the structure that holds an editor and its + * immediate children in the markup panel. + * - MarkupElementContainer: markup container for element nodes + * - MarkupTextContainer: markup container for text / comment nodes + * - MarkupReadonlyContainer: markup container for other nodes + * Node - A content node. + * object.elt - A UI element in the markup panel. + */ + +/** + * The markup tree. Manages the mapping of nodes to MarkupContainers, + * updating based on mutations, and the undo/redo bindings. + * + * @param {Inspector} inspector + * The inspector we're watching. + * @param {iframe} frame + * An iframe in which the caller has kindly loaded markup.xhtml. + */ +function MarkupView(inspector, frame, controllerWindow) { + this.inspector = inspector; + this.walker = this.inspector.walker; + this._frame = frame; + this.win = this._frame.contentWindow; + this.doc = this._frame.contentDocument; + this._elt = this.doc.querySelector("#root"); + this.htmlEditor = new HTMLEditor(this.doc); + + try { + this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize"); + } catch (ex) { + this.maxChildren = DEFAULT_MAX_CHILDREN; + } + + this.collapseAttributes = + Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF); + this.collapseAttributeLength = + Services.prefs.getIntPref(ATTR_COLLAPSE_LENGTH_PREF); + + // Creating the popup to be used to show CSS suggestions. + // The popup will be attached to the toolbox document. + this.popup = new AutocompletePopup(inspector.toolbox.doc, { + autoSelect: true, + theme: "auto", + }); + + this.undo = new UndoStack(); + this.undo.installController(controllerWindow); + + this._containers = new Map(); + + // Binding functions that need to be called in scope. + this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this); + this._mutationObserver = this._mutationObserver.bind(this); + this._onDisplayChange = this._onDisplayChange.bind(this); + this._onMouseClick = this._onMouseClick.bind(this); + this._onMouseUp = this._onMouseUp.bind(this); + this._onNewSelection = this._onNewSelection.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onFocus = this._onFocus.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onToolboxPickerCanceled = this._onToolboxPickerCanceled.bind(this); + this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this); + this._onCollapseAttributesPrefChange = + this._onCollapseAttributesPrefChange.bind(this); + this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this); + this._onBlur = this._onBlur.bind(this); + + EventEmitter.decorate(this); + + // Listening to various events. + this._elt.addEventListener("click", this._onMouseClick, false); + this._elt.addEventListener("mousemove", this._onMouseMove, false); + this._elt.addEventListener("mouseout", this._onMouseOut, false); + this._elt.addEventListener("blur", this._onBlur, true); + this.win.addEventListener("mouseup", this._onMouseUp); + this.win.addEventListener("copy", this._onCopy); + this._frame.addEventListener("focus", this._onFocus, false); + this.walker.on("mutations", this._mutationObserver); + this.walker.on("display-change", this._onDisplayChange); + this.inspector.selection.on("new-node-front", this._onNewSelection); + this.toolbox.on("picker-canceled", this._onToolboxPickerCanceled); + this.toolbox.on("picker-node-hovered", this._onToolboxPickerHover); + + this._onNewSelection(); + this._initTooltips(); + + this._prefObserver = new PrefObserver("devtools.markup"); + this._prefObserver.on(ATTR_COLLAPSE_ENABLED_PREF, + this._onCollapseAttributesPrefChange); + this._prefObserver.on(ATTR_COLLAPSE_LENGTH_PREF, + this._onCollapseAttributesPrefChange); + + this._initShortcuts(); +} + +MarkupView.prototype = { + /** + * How long does a node flash when it mutates (in ms). + */ + CONTAINER_FLASHING_DURATION: 500, + + _selectedContainer: null, + + get toolbox() { + return this.inspector.toolbox; + }, + + /** + * Handle promise rejections for various asynchronous actions, and only log errors if + * the markup view still exists. + * This is useful to silence useless errors that happen when the markup view is + * destroyed while still initializing (and making protocol requests). + */ + _handleRejectionIfNotDestroyed: function (e) { + if (!this._destroyer) { + console.error(e); + } + }, + + _initTooltips: function () { + // The tooltips will be attached to the toolbox document. + this.eventDetailsTooltip = new HTMLTooltip(this.toolbox.doc, + {type: "arrow"}); + this.imagePreviewTooltip = new HTMLTooltip(this.toolbox.doc, + {type: "arrow", useXulWrapper: "true"}); + this._enableImagePreviewTooltip(); + }, + + _enableImagePreviewTooltip: function () { + this.imagePreviewTooltip.startTogglingOnHover(this._elt, + this._isImagePreviewTarget); + }, + + _disableImagePreviewTooltip: function () { + this.imagePreviewTooltip.stopTogglingOnHover(); + }, + + _onToolboxPickerHover: function (event, nodeFront) { + this.showNode(nodeFront).then(() => { + this._showContainerAsHovered(nodeFront); + }, e => console.error(e)); + }, + + /** + * If the element picker gets canceled, make sure and re-center the view on the + * current selected element. + */ + _onToolboxPickerCanceled: function () { + if (this._selectedContainer) { + scrollIntoViewIfNeeded(this._selectedContainer.editor.elt); + } + }, + + isDragging: false, + + _onMouseMove: function (event) { + let target = event.target; + + // Auto-scroll if we're dragging. + if (this.isDragging) { + event.preventDefault(); + this._autoScroll(event); + return; + } + + // Show the current container as hovered and highlight it. + // This requires finding the current MarkupContainer (walking up the DOM). + while (!target.container) { + if (target.tagName.toLowerCase() === "body") { + return; + } + target = target.parentNode; + } + + let container = target.container; + if (this._hoveredNode !== container.node) { + this._showBoxModel(container.node); + } + this._showContainerAsHovered(container.node); + + this.emit("node-hover"); + }, + + /** + * If focus is moved outside of the markup view document and there is a + * selected container, make its contents not focusable by a keyboard. + */ + _onBlur: function (event) { + if (!this._selectedContainer) { + return; + } + + let {relatedTarget} = event; + if (relatedTarget && relatedTarget.ownerDocument === this.doc) { + return; + } + + if (this._selectedContainer) { + this._selectedContainer.clearFocus(); + } + }, + + /** + * Executed on each mouse-move while a node is being dragged in the view. + * Auto-scrolls the view to reveal nodes below the fold to drop the dragged + * node in. + */ + _autoScroll: function (event) { + let docEl = this.doc.documentElement; + + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + + // Auto-scroll when the mouse approaches top/bottom edge. + let fromBottom = docEl.clientHeight - event.pageY + this.win.scrollY; + let fromTop = event.pageY - this.win.scrollY; + let edgeDistance = Math.min(DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE, + docEl.clientHeight * DRAG_DROP_AUTOSCROLL_EDGE_RATIO); + + // The smaller the screen, the slower the movement. + let heightToSpeedRatio = + Math.max(DRAG_DROP_HEIGHT_TO_SPEED_MIN, + Math.min(DRAG_DROP_HEIGHT_TO_SPEED_MAX, + docEl.clientHeight / DRAG_DROP_HEIGHT_TO_SPEED)); + + if (fromBottom <= edgeDistance) { + // Map our distance range to a speed range so that the speed is not too + // fast or too slow. + let speed = map( + fromBottom, + 0, edgeDistance, + DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED); + + this._runUpdateLoop(() => { + docEl.scrollTop -= heightToSpeedRatio * + (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED); + }); + } + + if (fromTop <= edgeDistance) { + let speed = map( + fromTop, + 0, edgeDistance, + DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED); + + this._runUpdateLoop(() => { + docEl.scrollTop += heightToSpeedRatio * + (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED); + }); + } + }, + + /** + * Run a loop on the requestAnimationFrame. + */ + _runUpdateLoop: function (update) { + let loop = () => { + update(); + this._autoScrollAnimationFrame = this.win.requestAnimationFrame(loop); + }; + loop(); + }, + + _onMouseClick: function (event) { + // From the target passed here, let's find the parent MarkupContainer + // and ask it if the tooltip should be shown + let parentNode = event.target; + let container; + while (parentNode !== this.doc.body) { + if (parentNode.container) { + container = parentNode.container; + break; + } + parentNode = parentNode.parentNode; + } + + if (container instanceof MarkupElementContainer) { + // With the newly found container, delegate the tooltip content creation + // and decision to show or not the tooltip + container._buildEventTooltipContent(event.target, + this.eventDetailsTooltip); + } + }, + + _onMouseUp: function () { + this.indicateDropTarget(null); + this.indicateDragTarget(null); + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + }, + + _onCollapseAttributesPrefChange: function () { + this.collapseAttributes = + Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF); + this.collapseAttributeLength = + Services.prefs.getIntPref(ATTR_COLLAPSE_LENGTH_PREF); + this.update(); + }, + + cancelDragging: function () { + if (!this.isDragging) { + return; + } + + for (let [, container] of this._containers) { + if (container.isDragging) { + container.cancelDragging(); + break; + } + } + + this.indicateDropTarget(null); + this.indicateDragTarget(null); + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + }, + + _hoveredNode: null, + + /** + * Show a NodeFront's container as being hovered + * + * @param {NodeFront} nodeFront + * The node to show as hovered + */ + _showContainerAsHovered: function (nodeFront) { + if (this._hoveredNode === nodeFront) { + return; + } + + if (this._hoveredNode) { + this.getContainer(this._hoveredNode).hovered = false; + } + + this.getContainer(nodeFront).hovered = true; + this._hoveredNode = nodeFront; + // Emit an event that the container view is actually hovered now, as this function + // can be called by an asynchronous caller. + this.emit("showcontainerhovered"); + }, + + _onMouseOut: function (event) { + // Emulate mouseleave by skipping any relatedTarget inside the markup-view. + if (this._elt.contains(event.relatedTarget)) { + return; + } + + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + if (this.isDragging) { + return; + } + + this._hideBoxModel(true); + if (this._hoveredNode) { + this.getContainer(this._hoveredNode).hovered = false; + } + this._hoveredNode = null; + + this.emit("leave"); + }, + + /** + * Show the box model highlighter on a given node front + * + * @param {NodeFront} nodeFront + * The node to show the highlighter for + * @return {Promise} Resolves when the highlighter for this nodeFront is + * shown, taking into account that there could already be highlighter + * requests queued up + */ + _showBoxModel: function (nodeFront) { + return this.toolbox.highlighterUtils.highlightNodeFront(nodeFront); + }, + + /** + * Hide the box model highlighter on a given node front + * + * @param {Boolean} forceHide + * See toolbox-highlighter-utils/unhighlight + * @return {Promise} Resolves when the highlighter for this nodeFront is + * hidden, taking into account that there could already be highlighter + * requests queued up + */ + _hideBoxModel: function (forceHide) { + return this.toolbox.highlighterUtils.unhighlight(forceHide); + }, + + _briefBoxModelTimer: null, + + _clearBriefBoxModelTimer: function () { + if (this._briefBoxModelTimer) { + clearTimeout(this._briefBoxModelTimer); + this._briefBoxModelPromise.resolve(); + this._briefBoxModelPromise = null; + this._briefBoxModelTimer = null; + } + }, + + _brieflyShowBoxModel: function (nodeFront) { + this._clearBriefBoxModelTimer(); + let onShown = this._showBoxModel(nodeFront); + this._briefBoxModelPromise = defer(); + + this._briefBoxModelTimer = setTimeout(() => { + this._hideBoxModel() + .then(this._briefBoxModelPromise.resolve, + this._briefBoxModelPromise.resolve); + }, NEW_SELECTION_HIGHLIGHTER_TIMER); + + return promise.all([onShown, this._briefBoxModelPromise.promise]); + }, + + template: function (name, dest, options = {stack: "markup.xhtml"}) { + let node = this.doc.getElementById("template-" + name).cloneNode(true); + node.removeAttribute("id"); + template(node, dest, options); + return node; + }, + + /** + * Get the MarkupContainer object for a given node, or undefined if + * none exists. + */ + getContainer: function (node) { + return this._containers.get(node); + }, + + update: function () { + let updateChildren = (node) => { + this.getContainer(node).update(); + for (let child of node.treeChildren()) { + updateChildren(child); + } + }; + + // Start with the documentElement + let documentElement; + for (let node of this._rootNode.treeChildren()) { + if (node.isDocumentElement === true) { + documentElement = node; + break; + } + } + + // Recursively update each node starting with documentElement. + updateChildren(documentElement); + }, + + /** + * Executed when the mouse hovers over a target in the markup-view and is used + * to decide whether this target should be used to display an image preview + * tooltip. + * Delegates the actual decision to the corresponding MarkupContainer instance + * if one is found. + * + * @return {Promise} the promise returned by + * MarkupElementContainer._isImagePreviewTarget + */ + _isImagePreviewTarget: Task.async(function* (target) { + // From the target passed here, let's find the parent MarkupContainer + // and ask it if the tooltip should be shown + if (this.isDragging) { + return false; + } + + let parent = target, container; + while (parent !== this.doc.body) { + if (parent.container) { + container = parent.container; + break; + } + parent = parent.parentNode; + } + + if (container instanceof MarkupElementContainer) { + // With the newly found container, delegate the tooltip content creation + // and decision to show or not the tooltip + return container.isImagePreviewTarget(target, this.imagePreviewTooltip); + } + + return false; + }), + + /** + * Given the known reason, should the current selection be briefly highlighted + * In a few cases, we don't want to highlight the node: + * - If the reason is null (used to reset the selection), + * - if it's "inspector-open" (when the inspector opens up, let's not + * highlight the default node) + * - if it's "navigateaway" (since the page is being navigated away from) + * - if it's "test" (this is a special case for mochitest. In tests, we often + * need to select elements but don't necessarily want the highlighter to come + * and go after a delay as this might break test scenarios) + * We also do not want to start a brief highlight timeout if the node is + * already being hovered over, since in that case it will already be + * highlighted. + */ + _shouldNewSelectionBeHighlighted: function () { + let reason = this.inspector.selection.reason; + let unwantedReasons = [ + "inspector-open", + "navigateaway", + "nodeselected", + "test" + ]; + let isHighlight = this._hoveredNode === this.inspector.selection.nodeFront; + return !isHighlight && reason && unwantedReasons.indexOf(reason) === -1; + }, + + /** + * React to new-node-front selection events. + * Highlights the node if needed, and make sure it is shown and selected in + * the view. + */ + _onNewSelection: function () { + let selection = this.inspector.selection; + + this.htmlEditor.hide(); + if (this._hoveredNode && this._hoveredNode !== selection.nodeFront) { + this.getContainer(this._hoveredNode).hovered = false; + this._hoveredNode = null; + } + + if (!selection.isNode()) { + this.unmarkSelectedNode(); + return; + } + + let done = this.inspector.updating("markup-view"); + let onShowBoxModel, onShow; + + // Highlight the element briefly if needed. + if (this._shouldNewSelectionBeHighlighted()) { + onShowBoxModel = this._brieflyShowBoxModel(selection.nodeFront); + } + + onShow = this.showNode(selection.nodeFront).then(() => { + // We could be destroyed by now. + if (this._destroyer) { + return promise.reject("markupview destroyed"); + } + + // Mark the node as selected. + this.markNodeAsSelected(selection.nodeFront); + + // Make sure the new selection is navigated to. + this.maybeNavigateToNewSelection(); + return undefined; + }).catch(this._handleRejectionIfNotDestroyed); + + promise.all([onShowBoxModel, onShow]).then(done); + }, + + /** + * Maybe make selected the current node selection's MarkupContainer depending + * on why the current node got selected. + */ + maybeNavigateToNewSelection: function () { + let {reason, nodeFront} = this.inspector.selection; + + // The list of reasons that should lead to navigating to the node. + let reasonsToNavigate = [ + // If the user picked an element with the element picker. + "picker-node-picked", + // If the user shift-clicked (previewed) an element. + "picker-node-previewed", + // If the user selected an element with the browser context menu. + "browser-context-menu", + // If the user added a new node by clicking in the inspector toolbar. + "node-inserted" + ]; + + if (reasonsToNavigate.includes(reason)) { + this.getContainer(this._rootNode).elt.focus(); + this.navigate(this.getContainer(nodeFront)); + } + }, + + /** + * Create a TreeWalker to find the next/previous + * node for selection. + */ + _selectionWalker: function (start) { + let walker = this.doc.createTreeWalker( + start || this._elt, + nodeFilterConstants.SHOW_ELEMENT, + function (element) { + if (element.container && + element.container.elt === element && + element.container.visible) { + return nodeFilterConstants.FILTER_ACCEPT; + } + return nodeFilterConstants.FILTER_SKIP; + } + ); + walker.currentNode = this._selectedContainer.elt; + return walker; + }, + + _onCopy: function (evt) { + // Ignore copy events from editors + if (this._isInputOrTextarea(evt.target)) { + return; + } + + let selection = this.inspector.selection; + if (selection.isNode()) { + this.inspector.copyOuterHTML(); + } + evt.stopPropagation(); + evt.preventDefault(); + }, + + /** + * Register all key shortcuts. + */ + _initShortcuts: function () { + let shortcuts = new KeyShortcuts({ + window: this.win, + }); + + this._onShortcut = this._onShortcut.bind(this); + + // Process localizable keys + ["markupView.hide.key", + "markupView.edit.key", + "markupView.scrollInto.key"].forEach(name => { + let key = INSPECTOR_L10N.getStr(name); + shortcuts.on(key, (_, event) => this._onShortcut(name, event)); + }); + + // Process generic keys: + ["Delete", "Backspace", "Home", "Left", "Right", "Up", "Down", "PageUp", + "PageDown", "Esc", "Enter", "Space"].forEach(key => { + shortcuts.on(key, this._onShortcut); + }); + }, + + /** + * Key shortcut listener. + */ + _onShortcut(name, event) { + if (this._isInputOrTextarea(event.target)) { + return; + } + switch (name) { + // Localizable keys + case "markupView.hide.key": { + let node = this._selectedContainer.node; + if (node.hidden) { + this.walker.unhideNode(node); + } else { + this.walker.hideNode(node); + } + break; + } + case "markupView.edit.key": { + this.beginEditingOuterHTML(this._selectedContainer.node); + break; + } + case "markupView.scrollInto.key": { + let selection = this._selectedContainer.node; + this.inspector.scrollNodeIntoView(selection); + break; + } + // Generic keys + case "Delete": { + this.deleteNodeOrAttribute(); + break; + } + case "Backspace": { + this.deleteNodeOrAttribute(true); + break; + } + case "Home": { + let rootContainer = this.getContainer(this._rootNode); + this.navigate(rootContainer.children.firstChild.container); + break; + } + case "Left": { + if (this._selectedContainer.expanded) { + this.collapseNode(this._selectedContainer.node); + } else { + let parent = this._selectionWalker().parentNode(); + if (parent) { + this.navigate(parent.container); + } + } + break; + } + case "Right": { + if (!this._selectedContainer.expanded && + this._selectedContainer.hasChildren) { + this._expandContainer(this._selectedContainer); + } else { + let next = this._selectionWalker().nextNode(); + if (next) { + this.navigate(next.container); + } + } + break; + } + case "Up": { + let previousNode = this._selectionWalker().previousNode(); + if (previousNode) { + this.navigate(previousNode.container); + } + break; + } + case "Down": { + let nextNode = this._selectionWalker().nextNode(); + if (nextNode) { + this.navigate(nextNode.container); + } + break; + } + case "PageUp": { + let walker = this._selectionWalker(); + let selection = this._selectedContainer; + for (let i = 0; i < PAGE_SIZE; i++) { + let previousNode = walker.previousNode(); + if (!previousNode) { + break; + } + selection = previousNode.container; + } + this.navigate(selection); + break; + } + case "PageDown": { + let walker = this._selectionWalker(); + let selection = this._selectedContainer; + for (let i = 0; i < PAGE_SIZE; i++) { + let nextNode = walker.nextNode(); + if (!nextNode) { + break; + } + selection = nextNode.container; + } + this.navigate(selection); + break; + } + case "Enter": + case "Space": { + if (!this._selectedContainer.canFocus) { + this._selectedContainer.canFocus = true; + this._selectedContainer.focus(); + } else { + // Return early to prevent cancelling the event. + return; + } + break; + } + case "Esc": { + if (this.isDragging) { + this.cancelDragging(); + } else { + // Return early to prevent cancelling the event when not + // dragging, to allow the split console to be toggled. + return; + } + break; + } + default: + console.error("Unexpected markup-view key shortcut", name); + return; + } + // Prevent default for this action + event.stopPropagation(); + event.preventDefault(); + }, + + /** + * Check if a node is an input or textarea + */ + _isInputOrTextarea: function (element) { + let name = element.tagName.toLowerCase(); + return name === "input" || name === "textarea"; + }, + + /** + * If there's an attribute on the current node that's currently focused, then + * delete this attribute, otherwise delete the node itself. + * + * @param {Boolean} moveBackward + * If set to true and if we're deleting the node, focus the previous + * sibling after deletion, otherwise the next one. + */ + deleteNodeOrAttribute: function (moveBackward) { + let focusedAttribute = this.doc.activeElement + ? this.doc.activeElement.closest(".attreditor") + : null; + if (focusedAttribute) { + // The focused attribute might not be in the current selected container. + let container = focusedAttribute.closest("li.child").container; + container.removeAttribute(focusedAttribute.dataset.attr); + } else { + this.deleteNode(this._selectedContainer.node, moveBackward); + } + }, + + /** + * Delete a node from the DOM. + * This is an undoable action. + * + * @param {NodeFront} node + * The node to remove. + * @param {Boolean} moveBackward + * If set to true, focus the previous sibling, otherwise the next one. + */ + deleteNode: function (node, moveBackward) { + if (node.isDocumentElement || + node.nodeType == nodeConstants.DOCUMENT_TYPE_NODE || + node.isAnonymous) { + return; + } + + let container = this.getContainer(node); + + // Retain the node so we can undo this... + this.walker.retainNode(node).then(() => { + let parent = node.parentNode(); + let nextSibling = null; + this.undo.do(() => { + this.walker.removeNode(node).then(siblings => { + nextSibling = siblings.nextSibling; + let prevSibling = siblings.previousSibling; + let focusNode = moveBackward ? prevSibling : nextSibling; + + // If we can't move as the user wants, we move to the other direction. + // If there is no sibling elements anymore, move to the parent node. + if (!focusNode) { + focusNode = nextSibling || prevSibling || parent; + } + + let isNextSiblingText = nextSibling ? + nextSibling.nodeType === nodeConstants.TEXT_NODE : false; + let isPrevSiblingText = prevSibling ? + prevSibling.nodeType === nodeConstants.TEXT_NODE : false; + + // If the parent had two children and the next or previous sibling + // is a text node, then it now has only a single text node, is about + // to be in-lined; and focus should move to the parent. + if (parent.numChildren === 2 + && (isNextSiblingText || isPrevSiblingText)) { + focusNode = parent; + } + + if (container.selected) { + this.navigate(this.getContainer(focusNode)); + } + }); + }, () => { + let isValidSibling = nextSibling && !nextSibling.isPseudoElement; + nextSibling = isValidSibling ? nextSibling : null; + this.walker.insertBefore(node, parent, nextSibling); + }); + }).then(null, console.error); + }, + + /** + * If an editable item is focused, select its container. + */ + _onFocus: function (event) { + let parent = event.target; + while (!parent.container) { + parent = parent.parentNode; + } + if (parent) { + this.navigate(parent.container); + } + }, + + /** + * Handle a user-requested navigation to a given MarkupContainer, + * updating the inspector's currently-selected node. + * + * @param {MarkupContainer} container + * The container we're navigating to. + */ + navigate: function (container) { + if (!container) { + return; + } + + let node = container.node; + this.markNodeAsSelected(node, "treepanel"); + }, + + /** + * Make sure a node is included in the markup tool. + * + * @param {NodeFront} node + * The node in the content document. + * @param {Boolean} flashNode + * Whether the newly imported node should be flashed + * @return {MarkupContainer} The MarkupContainer object for this element. + */ + importNode: function (node, flashNode) { + if (!node) { + return null; + } + + if (this._containers.has(node)) { + return this.getContainer(node); + } + + let container; + let {nodeType, isPseudoElement} = node; + if (node === this.walker.rootNode) { + container = new RootContainer(this, node); + this._elt.appendChild(container.elt); + this._rootNode = node; + } else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) { + container = new MarkupElementContainer(this, node, this.inspector); + } else if (nodeType == nodeConstants.COMMENT_NODE || + nodeType == nodeConstants.TEXT_NODE) { + container = new MarkupTextContainer(this, node, this.inspector); + } else { + container = new MarkupReadOnlyContainer(this, node, this.inspector); + } + + if (flashNode) { + container.flashMutation(); + } + + this._containers.set(node, container); + container.childrenDirty = true; + + this._updateChildren(container); + + this.inspector.emit("container-created", container); + + return container; + }, + + /** + * Mutation observer used for included nodes. + */ + _mutationObserver: function (mutations) { + for (let mutation of mutations) { + let type = mutation.type; + let target = mutation.target; + + if (mutation.type === "documentUnload") { + // Treat this as a childList change of the child (maybe the protocol + // should do this). + type = "childList"; + target = mutation.targetParent; + if (!target) { + continue; + } + } + + let container = this.getContainer(target); + if (!container) { + // Container might not exist if this came from a load event for a node + // we're not viewing. + continue; + } + + if (type === "attributes" && mutation.attributeName === "class") { + container.updateIsDisplayed(); + } + if (type === "attributes" || type === "characterData" + || type === "events" || type === "pseudoClassLock") { + container.update(); + } else if (type === "childList" || type === "nativeAnonymousChildList") { + container.childrenDirty = true; + // Update the children to take care of changes in the markup view DOM + // and update container (and its subtree) DOM tree depth level for + // accessibility where necessary. + this._updateChildren(container, {flash: true}).then(() => + container.updateLevel()); + } else if (type === "inlineTextChild") { + container.childrenDirty = true; + this._updateChildren(container, {flash: true}); + container.update(); + } + } + + this._waitForChildren().then(() => { + if (this._destroyer) { + // Could not fully update after markup mutations, the markup-view was destroyed + // while waiting for children. Bail out silently. + return; + } + this._flashMutatedNodes(mutations); + this.inspector.emit("markupmutation", mutations); + + // Since the htmlEditor is absolutely positioned, a mutation may change + // the location in which it should be shown. + this.htmlEditor.refresh(); + }); + }, + + /** + * React to display-change events from the walker + * + * @param {Array} nodes + * An array of nodeFronts + */ + _onDisplayChange: function (nodes) { + for (let node of nodes) { + let container = this.getContainer(node); + if (container) { + container.updateIsDisplayed(); + } + } + }, + + /** + * Given a list of mutations returned by the mutation observer, flash the + * corresponding containers to attract attention. + */ + _flashMutatedNodes: function (mutations) { + let addedOrEditedContainers = new Set(); + let removedContainers = new Set(); + + for (let {type, target, added, removed, newValue} of mutations) { + let container = this.getContainer(target); + + if (container) { + if (type === "characterData") { + addedOrEditedContainers.add(container); + } else if (type === "attributes" && newValue === null) { + // Removed attributes should flash the entire node. + // New or changed attributes will flash the attribute itself + // in ElementEditor.flashAttribute. + addedOrEditedContainers.add(container); + } else if (type === "childList") { + // If there has been removals, flash the parent + if (removed.length) { + removedContainers.add(container); + } + + // If there has been additions, flash the nodes if their associated + // container exist (so if their parent is expanded in the inspector). + added.forEach(node => { + let addedContainer = this.getContainer(node); + if (addedContainer) { + addedOrEditedContainers.add(addedContainer); + + // The node may be added as a result of an append, in which case + // it will have been removed from another container first, but in + // these cases we don't want to flash both the removal and the + // addition + removedContainers.delete(container); + } + }); + } + } + } + + for (let container of removedContainers) { + container.flashMutation(); + } + for (let container of addedOrEditedContainers) { + container.flashMutation(); + } + }, + + /** + * Make sure the given node's parents are expanded and the + * node is scrolled on to screen. + */ + showNode: function (node, centered = true) { + let parent = node; + + this.importNode(node); + + while ((parent = parent.parentNode())) { + this.importNode(parent); + this.expandNode(parent); + } + + return this._waitForChildren().then(() => { + if (this._destroyer) { + return promise.reject("markupview destroyed"); + } + return this._ensureVisible(node); + }).then(() => { + scrollIntoViewIfNeeded(this.getContainer(node).editor.elt, centered); + }, this._handleRejectionIfNotDestroyed); + }, + + /** + * Expand the container's children. + */ + _expandContainer: function (container) { + return this._updateChildren(container, {expand: true}).then(() => { + if (this._destroyer) { + // Could not expand the node, the markup-view was destroyed in the meantime. Just + // silently give up. + return; + } + container.setExpanded(true); + }); + }, + + /** + * Expand the node's children. + */ + expandNode: function (node) { + let container = this.getContainer(node); + this._expandContainer(container); + }, + + /** + * Expand the entire tree beneath a container. + * + * @param {MarkupContainer} container + * The container to expand. + */ + _expandAll: function (container) { + return this._expandContainer(container).then(() => { + let child = container.children.firstChild; + let promises = []; + while (child) { + promises.push(this._expandAll(child.container)); + child = child.nextSibling; + } + return promise.all(promises); + }).then(null, console.error); + }, + + /** + * Expand the entire tree beneath a node. + * + * @param {DOMNode} node + * The node to expand, or null to start from the top. + */ + expandAll: function (node) { + node = node || this._rootNode; + return this._expandAll(this.getContainer(node)); + }, + + /** + * Collapse the node's children. + */ + collapseNode: function (node) { + let container = this.getContainer(node); + container.setExpanded(false); + }, + + /** + * Returns either the innerHTML or the outerHTML for a remote node. + * + * @param {NodeFront} node + * The NodeFront to get the outerHTML / innerHTML for. + * @param {Boolean} isOuter + * If true, makes the function return the outerHTML, + * otherwise the innerHTML. + * @return {Promise} that will be resolved with the outerHTML / innerHTML. + */ + _getNodeHTML: function (node, isOuter) { + let walkerPromise = null; + + if (isOuter) { + walkerPromise = this.walker.outerHTML(node); + } else { + walkerPromise = this.walker.innerHTML(node); + } + + return walkerPromise.then(longstr => { + return longstr.string().then(html => { + longstr.release().then(null, console.error); + return html; + }); + }); + }, + + /** + * Retrieve the outerHTML for a remote node. + * + * @param {NodeFront} node + * The NodeFront to get the outerHTML for. + * @return {Promise} that will be resolved with the outerHTML. + */ + getNodeOuterHTML: function (node) { + return this._getNodeHTML(node, true); + }, + + /** + * Retrieve the innerHTML for a remote node. + * + * @param {NodeFront} node + * The NodeFront to get the innerHTML for. + * @return {Promise} that will be resolved with the innerHTML. + */ + getNodeInnerHTML: function (node) { + return this._getNodeHTML(node); + }, + + /** + * Listen to mutations, expect a given node to be removed and try and select + * the node that sits at the same place instead. + * This is useful when changing the outerHTML or the tag name so that the + * newly inserted node gets selected instead of the one that just got removed. + */ + reselectOnRemoved: function (removedNode, reason) { + // Only allow one removed node reselection at a time, so that when there are + // more than 1 request in parallel, the last one wins. + this.cancelReselectOnRemoved(); + + // Get the removedNode index in its parent node to reselect the right node. + let isHTMLTag = removedNode.tagName.toLowerCase() === "html"; + let oldContainer = this.getContainer(removedNode); + let parentContainer = this.getContainer(removedNode.parentNode()); + let childIndex = parentContainer.getChildContainers().indexOf(oldContainer); + + let onMutations = this._removedNodeObserver = (e, mutations) => { + let isNodeRemovalMutation = false; + for (let mutation of mutations) { + let containsRemovedNode = mutation.removed && + mutation.removed.some(n => n === removedNode); + if (mutation.type === "childList" && + (containsRemovedNode || isHTMLTag)) { + isNodeRemovalMutation = true; + break; + } + } + if (!isNodeRemovalMutation) { + return; + } + + this.inspector.off("markupmutation", onMutations); + this._removedNodeObserver = null; + + // Don't select the new node if the user has already changed the current + // selection. + if (this.inspector.selection.nodeFront === parentContainer.node || + (this.inspector.selection.nodeFront === removedNode && isHTMLTag)) { + let childContainers = parentContainer.getChildContainers(); + if (childContainers && childContainers[childIndex]) { + this.markNodeAsSelected(childContainers[childIndex].node, reason); + if (childContainers[childIndex].hasChildren) { + this.expandNode(childContainers[childIndex].node); + } + this.emit("reselectedonremoved"); + } + } + }; + + // Start listening for mutations until we find a childList change that has + // removedNode removed. + this.inspector.on("markupmutation", onMutations); + }, + + /** + * Make sure to stop listening for node removal markupmutations and not + * reselect the corresponding node when that happens. + * Useful when the outerHTML/tagname edition failed. + */ + cancelReselectOnRemoved: function () { + if (this._removedNodeObserver) { + this.inspector.off("markupmutation", this._removedNodeObserver); + this._removedNodeObserver = null; + this.emit("canceledreselectonremoved"); + } + }, + + /** + * Replace the outerHTML of any node displayed in the inspector with + * some other HTML code + * + * @param {NodeFront} node + * Node which outerHTML will be replaced. + * @param {String} newValue + * The new outerHTML to set on the node. + * @param {String} oldValue + * The old outerHTML that will be used if the user undoes the update. + * @return {Promise} that will resolve when the outer HTML has been updated. + */ + updateNodeOuterHTML: function (node, newValue) { + let container = this.getContainer(node); + if (!container) { + return promise.reject(); + } + + // Changing the outerHTML removes the node which outerHTML was changed. + // Listen to this removal to reselect the right node afterwards. + this.reselectOnRemoved(node, "outerhtml"); + return this.walker.setOuterHTML(node, newValue).then(null, () => { + this.cancelReselectOnRemoved(); + }); + }, + + /** + * Replace the innerHTML of any node displayed in the inspector with + * some other HTML code + * @param {Node} node + * node which innerHTML will be replaced. + * @param {String} newValue + * The new innerHTML to set on the node. + * @param {String} oldValue + * The old innerHTML that will be used if the user undoes the update. + * @return {Promise} that will resolve when the inner HTML has been updated. + */ + updateNodeInnerHTML: function (node, newValue, oldValue) { + let container = this.getContainer(node); + if (!container) { + return promise.reject(); + } + + let def = defer(); + + container.undo.do(() => { + this.walker.setInnerHTML(node, newValue).then(def.resolve, def.reject); + }, () => { + this.walker.setInnerHTML(node, oldValue); + }); + + return def.promise; + }, + + /** + * Insert adjacent HTML to any node displayed in the inspector. + * + * @param {NodeFront} node + * The reference node. + * @param {String} position + * The position as specified for Element.insertAdjacentHTML + * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd"). + * @param {String} newValue + * The adjacent HTML. + * @return {Promise} that will resolve when the adjacent HTML has + * been inserted. + */ + insertAdjacentHTMLToNode: function (node, position, value) { + let container = this.getContainer(node); + if (!container) { + return promise.reject(); + } + + let def = defer(); + + let injectedNodes = []; + container.undo.do(() => { + this.walker.insertAdjacentHTML(node, position, value).then(nodeArray => { + injectedNodes = nodeArray.nodes; + return nodeArray; + }).then(def.resolve, def.reject); + }, () => { + this.walker.removeNodes(injectedNodes); + }); + + return def.promise; + }, + + /** + * Open an editor in the UI to allow editing of a node's outerHTML. + * + * @param {NodeFront} node + * The NodeFront to edit. + */ + beginEditingOuterHTML: function (node) { + this.getNodeOuterHTML(node).then(oldValue => { + let container = this.getContainer(node); + if (!container) { + return; + } + this.htmlEditor.show(container.tagLine, oldValue); + this.htmlEditor.once("popuphidden", (e, commit, value) => { + // Need to focus the element instead of the frame / window + // in order to give keyboard focus back to doc (from editor). + this.doc.documentElement.focus(); + + if (commit) { + this.updateNodeOuterHTML(node, value, oldValue); + } + }); + }); + }, + + /** + * Mark the given node expanded. + * + * @param {NodeFront} node + * The NodeFront to mark as expanded. + * @param {Boolean} expanded + * Whether the expand or collapse. + * @param {Boolean} expandDescendants + * Whether to expand all descendants too + */ + setNodeExpanded: function (node, expanded, expandDescendants) { + if (expanded) { + if (expandDescendants) { + this.expandAll(node); + } else { + this.expandNode(node); + } + } else { + this.collapseNode(node); + } + }, + + /** + * Mark the given node selected, and update the inspector.selection + * object's NodeFront to keep consistent state between UI and selection. + * + * @param {NodeFront} aNode + * The NodeFront to mark as selected. + * @param {String} reason + * The reason for marking the node as selected. + * @return {Boolean} False if the node is already marked as selected, true + * otherwise. + */ + markNodeAsSelected: function (node, reason) { + let container = this.getContainer(node); + + if (this._selectedContainer === container) { + return false; + } + + // Un-select and remove focus from the previous container. + if (this._selectedContainer) { + this._selectedContainer.selected = false; + this._selectedContainer.clearFocus(); + } + + // Select the new container. + this._selectedContainer = container; + if (node) { + this._selectedContainer.selected = true; + } + + // Change the current selection if needed. + if (this.inspector.selection.nodeFront !== node) { + this.inspector.selection.setNodeFront(node, reason || "nodeselected"); + } + + return true; + }, + + /** + * Make sure that every ancestor of the selection are updated + * and included in the list of visible children. + */ + _ensureVisible: function (node) { + while (node) { + let container = this.getContainer(node); + let parent = node.parentNode(); + if (!container.elt.parentNode) { + let parentContainer = this.getContainer(parent); + if (parentContainer) { + parentContainer.childrenDirty = true; + this._updateChildren(parentContainer, {expand: true}); + } + } + + node = parent; + } + return this._waitForChildren(); + }, + + /** + * Unmark selected node (no node selected). + */ + unmarkSelectedNode: function () { + if (this._selectedContainer) { + this._selectedContainer.selected = false; + this._selectedContainer = null; + } + }, + + /** + * Check if the current selection is a descendent of the container. + * if so, make sure it's among the visible set for the container, + * and set the dirty flag if needed. + * + * @return The node that should be made visible, if any. + */ + _checkSelectionVisible: function (container) { + let centered = null; + let node = this.inspector.selection.nodeFront; + while (node) { + if (node.parentNode() === container.node) { + centered = node; + break; + } + node = node.parentNode(); + } + + return centered; + }, + + /** + * Make sure all children of the given container's node are + * imported and attached to the container in the right order. + * + * Children need to be updated only in the following circumstances: + * a) We just imported this node and have never seen its children. + * container.childrenDirty will be set by importNode in this case. + * b) We received a childList mutation on the node. + * container.childrenDirty will be set in that case too. + * c) We have changed the selection, and the path to that selection + * wasn't loaded in a previous children request (because we only + * grab a subset). + * container.childrenDirty should be set in that case too! + * + * @param {MarkupContainer} container + * The markup container whose children need updating + * @param {Object} options + * Options are {expand:boolean,flash:boolean} + * @return {Promise} that will be resolved when the children are ready + * (which may be immediately). + */ + _updateChildren: function (container, options) { + let expand = options && options.expand; + let flash = options && options.flash; + + container.hasChildren = container.node.hasChildren; + // Accessibility should either ignore empty children or semantically + // consider them a group. + container.setChildrenRole(); + + if (!this._queuedChildUpdates) { + this._queuedChildUpdates = new Map(); + } + + if (this._queuedChildUpdates.has(container)) { + return this._queuedChildUpdates.get(container); + } + + if (!container.childrenDirty) { + return promise.resolve(container); + } + + if (container.inlineTextChild + && container.inlineTextChild != container.node.inlineTextChild) { + // This container was doing double duty as a container for a single + // text child, back that out. + this._containers.delete(container.inlineTextChild); + container.clearInlineTextChild(); + + if (container.hasChildren && container.selected) { + container.setExpanded(true); + } + } + + if (container.node.inlineTextChild) { + container.setExpanded(false); + // this container will do double duty as the container for the single + // text child. + while (container.children.firstChild) { + container.children.removeChild(container.children.firstChild); + } + + container.setInlineTextChild(container.node.inlineTextChild); + + this._containers.set(container.node.inlineTextChild, container); + container.childrenDirty = false; + return promise.resolve(container); + } + + if (!container.hasChildren) { + while (container.children.firstChild) { + container.children.removeChild(container.children.firstChild); + } + container.childrenDirty = false; + container.setExpanded(false); + return promise.resolve(container); + } + + // If we're not expanded (or asked to update anyway), we're done for + // now. Note that this will leave the childrenDirty flag set, so when + // expanded we'll refresh the child list. + if (!(container.expanded || expand)) { + return promise.resolve(container); + } + + // We're going to issue a children request, make sure it includes the + // centered node. + let centered = this._checkSelectionVisible(container); + + // Children aren't updated yet, but clear the childrenDirty flag anyway. + // If the dirty flag is re-set while we're fetching we'll need to fetch + // again. + container.childrenDirty = false; + let updatePromise = + this._getVisibleChildren(container, centered).then(children => { + if (!this._containers) { + return promise.reject("markup view destroyed"); + } + this._queuedChildUpdates.delete(container); + + // If children are dirty, we got a change notification for this node + // while the request was in progress, we need to do it again. + if (container.childrenDirty) { + return this._updateChildren(container, {expand: centered}); + } + + let fragment = this.doc.createDocumentFragment(); + + for (let child of children.nodes) { + let childContainer = this.importNode(child, flash); + fragment.appendChild(childContainer.elt); + } + + while (container.children.firstChild) { + container.children.removeChild(container.children.firstChild); + } + + if (!(children.hasFirst && children.hasLast)) { + let nodesCount = container.node.numChildren; + let showAllString = PluralForm.get(nodesCount, + INSPECTOR_L10N.getStr("markupView.more.showAll2")); + let data = { + showing: INSPECTOR_L10N.getStr("markupView.more.showing"), + showAll: showAllString.replace("#1", nodesCount), + allButtonClick: () => { + container.maxChildren = -1; + container.childrenDirty = true; + this._updateChildren(container); + } + }; + + if (!children.hasFirst) { + let span = this.template("more-nodes", data); + fragment.insertBefore(span, fragment.firstChild); + } + if (!children.hasLast) { + let span = this.template("more-nodes", data); + fragment.appendChild(span); + } + } + + container.children.appendChild(fragment); + return container; + }).catch(this._handleRejectionIfNotDestroyed); + this._queuedChildUpdates.set(container, updatePromise); + return updatePromise; + }, + + _waitForChildren: function () { + if (!this._queuedChildUpdates) { + return promise.resolve(undefined); + } + + return promise.all([...this._queuedChildUpdates.values()]); + }, + + /** + * Return a list of the children to display for this container. + */ + _getVisibleChildren: function (container, centered) { + let maxChildren = container.maxChildren || this.maxChildren; + if (maxChildren == -1) { + maxChildren = undefined; + } + + return this.walker.children(container.node, { + maxNodes: maxChildren, + center: centered + }); + }, + + /** + * Tear down the markup panel. + */ + destroy: function () { + if (this._destroyer) { + return this._destroyer; + } + + this._destroyer = promise.resolve(); + + this._clearBriefBoxModelTimer(); + + this._hoveredNode = null; + + this.htmlEditor.destroy(); + this.htmlEditor = null; + + this.undo.destroy(); + this.undo = null; + + this.popup.destroy(); + this.popup = null; + + this._elt.removeEventListener("click", this._onMouseClick, false); + this._elt.removeEventListener("mousemove", this._onMouseMove, false); + this._elt.removeEventListener("mouseout", this._onMouseOut, false); + this._elt.removeEventListener("blur", this._onBlur, true); + this.win.removeEventListener("mouseup", this._onMouseUp); + this.win.removeEventListener("copy", this._onCopy); + this._frame.removeEventListener("focus", this._onFocus, false); + this.walker.off("mutations", this._mutationObserver); + this.walker.off("display-change", this._onDisplayChange); + this.inspector.selection.off("new-node-front", this._onNewSelection); + this.toolbox.off("picker-node-hovered", + this._onToolboxPickerHover); + + this._prefObserver.off(ATTR_COLLAPSE_ENABLED_PREF, + this._onCollapseAttributesPrefChange); + this._prefObserver.off(ATTR_COLLAPSE_LENGTH_PREF, + this._onCollapseAttributesPrefChange); + this._prefObserver.destroy(); + + this._elt = null; + + for (let [, container] of this._containers) { + container.destroy(); + } + this._containers = null; + + this.eventDetailsTooltip.destroy(); + this.eventDetailsTooltip = null; + + this.imagePreviewTooltip.destroy(); + this.imagePreviewTooltip = null; + + this.win = null; + this.doc = null; + + this._lastDropTarget = null; + this._lastDragTarget = null; + + return this._destroyer; + }, + + /** + * Find the closest element with class tag-line. These are used to indicate + * drag and drop targets. + * + * @param {DOMNode} el + * @return {DOMNode} + */ + findClosestDragDropTarget: function (el) { + return el.classList.contains("tag-line") + ? el + : el.querySelector(".tag-line") || el.closest(".tag-line"); + }, + + /** + * Takes an element as it's only argument and marks the element + * as the drop target + */ + indicateDropTarget: function (el) { + if (this._lastDropTarget) { + this._lastDropTarget.classList.remove("drop-target"); + } + + if (!el) { + return; + } + + let target = this.findClosestDragDropTarget(el); + if (target) { + target.classList.add("drop-target"); + this._lastDropTarget = target; + } + }, + + /** + * Takes an element to mark it as indicator of dragging target's initial place + */ + indicateDragTarget: function (el) { + if (this._lastDragTarget) { + this._lastDragTarget.classList.remove("drag-target"); + } + + if (!el) { + return; + } + + let target = this.findClosestDragDropTarget(el); + if (target) { + target.classList.add("drag-target"); + this._lastDragTarget = target; + } + }, + + /** + * Used to get the nodes required to modify the markup after dragging the + * element (parent/nextSibling). + */ + get dropTargetNodes() { + let target = this._lastDropTarget; + + if (!target) { + return null; + } + + let parent, nextSibling; + + if (target.previousElementSibling && + target.previousElementSibling.nodeName.toLowerCase() === "ul") { + parent = target.parentNode.container.node; + nextSibling = null; + } else { + parent = target.parentNode.container.node.parentNode(); + nextSibling = target.parentNode.container.node; + } + + if (nextSibling && nextSibling.isBeforePseudoElement) { + nextSibling = target.parentNode.parentNode.children[1].container.node; + } + if (nextSibling && nextSibling.isAfterPseudoElement) { + parent = target.parentNode.container.node.parentNode(); + nextSibling = null; + } + + if (parent.nodeType !== nodeConstants.ELEMENT_NODE) { + return null; + } + + return {parent, nextSibling}; + } +}; + +/** + * Map a number from one range to another. + */ +function map(value, oldMin, oldMax, newMin, newMax) { + let ratio = oldMax - oldMin; + if (ratio == 0) { + return value; + } + return newMin + (newMax - newMin) * ((value - oldMin) / ratio); +} + +module.exports = MarkupView; diff --git a/devtools/client/inspector/markup/markup.xhtml b/devtools/client/inspector/markup/markup.xhtml new file mode 100644 index 000000000..88b06aadd --- /dev/null +++ b/devtools/client/inspector/markup/markup.xhtml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/devtools/client/inspector/markup/moz.build b/devtools/client/inspector/markup/moz.build new file mode 100644 index 000000000..4d721cc3c --- /dev/null +++ b/devtools/client/inspector/markup/moz.build @@ -0,0 +1,16 @@ +# -*- 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 += [ + 'views', +] + +DevToolsModules( + 'markup.js', + 'utils.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/markup/test/.eslintrc.js b/devtools/client/inspector/markup/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/markup/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/markup/test/actor_events_form.js b/devtools/client/inspector/markup/test/actor_events_form.js new file mode 100644 index 000000000..bd1b1e91a --- /dev/null +++ b/devtools/client/inspector/markup/test/actor_events_form.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test actor is used for testing the addition of custom form data +// on NodeActor. Custom form property is set when 'form' event is sent +// by NodeActor actor (see 'onNodeActorForm' method). + +const Events = require("sdk/event/core"); +const {ActorClassWithSpec, Actor, FrontClassWithSpec, Front, generateActorSpec} = + require("devtools/shared/protocol"); + +const {NodeActor} = require("devtools/server/actors/inspector"); + +var eventsSpec = generateActorSpec({ + typeName: "eventsFormActor", + + methods: { + attach: { + request: {}, + response: {} + }, + detach: { + request: {}, + response: {} + } + } +}); + +var EventsFormActor = ActorClassWithSpec(eventsSpec, { + initialize: function () { + Actor.prototype.initialize.apply(this, arguments); + }, + + attach: function () { + Events.on(NodeActor, "form", this.onNodeActorForm); + }, + + detach: function () { + Events.off(NodeActor, "form", this.onNodeActorForm); + }, + + onNodeActorForm: function (event) { + let nodeActor = event.target; + if (nodeActor.rawNode.id == "container") { + let form = event.data; + form.setFormProperty("test-property", "test-value"); + } + } +}); + +var EventsFormFront = FrontClassWithSpec(eventsSpec, { + initialize: function (client, form) { + Front.prototype.initialize.apply(this, arguments); + + this.actorID = form[EventsFormActor.prototype.typeName]; + this.manage(this); + } +}); + +exports.EventsFormFront = EventsFormFront; diff --git a/devtools/client/inspector/markup/test/browser.ini b/devtools/client/inspector/markup/test/browser.ini new file mode 100644 index 000000000..3116e4beb --- /dev/null +++ b/devtools/client/inspector/markup/test/browser.ini @@ -0,0 +1,155 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + actor_events_form.js + doc_markup_anonymous.html + doc_markup_dragdrop.html + doc_markup_dragdrop_autoscroll_01.html + doc_markup_dragdrop_autoscroll_02.html + doc_markup_edit.html + doc_markup_events1.html + doc_markup_events2.html + doc_markup_events3.html + doc_markup_events_form.html + doc_markup_events_jquery.html + doc_markup_events-overflow.html + doc_markup_flashing.html + doc_markup_html_mixed_case.html + doc_markup_image_and_canvas.html + doc_markup_image_and_canvas_2.html + doc_markup_links.html + doc_markup_mutation.html + doc_markup_navigation.html + doc_markup_not_displayed.html + doc_markup_pagesize_01.html + doc_markup_pagesize_02.html + doc_markup_search.html + doc_markup_svg_attributes.html + doc_markup_toggle.html + doc_markup_tooltip.png + doc_markup_void_elements.html + doc_markup_void_elements.xhtml + doc_markup_whitespace.html + doc_markup_xul.xul + head.js + helper_attributes_test_runner.js + helper_events_test_runner.js + helper_markup_accessibility_navigation.js + helper_outerhtml_test_runner.js + helper_style_attr_test_runner.js + lib_jquery_1.0.js + lib_jquery_1.1.js + lib_jquery_1.2_min.js + lib_jquery_1.3_min.js + lib_jquery_1.4_min.js + lib_jquery_1.6_min.js + lib_jquery_1.7_min.js + lib_jquery_1.11.1_min.js + lib_jquery_2.1.1_min.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_markup_accessibility_focus_blur.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_markup_accessibility_navigation.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_markup_accessibility_navigation_after_edit.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_markup_accessibility_semantics.js] +[browser_markup_anonymous_01.js] +[browser_markup_anonymous_02.js] +skip-if = e10s # scratchpad.xul is not loading in e10s window +[browser_markup_anonymous_03.js] +[browser_markup_anonymous_04.js] +[browser_markup_copy_image_data.js] +subsuite = clipboard +[browser_markup_css_completion_style_attribute_01.js] +[browser_markup_css_completion_style_attribute_02.js] +[browser_markup_css_completion_style_attribute_03.js] +[browser_markup_dragdrop_autoscroll_01.js] +[browser_markup_dragdrop_autoscroll_02.js] +[browser_markup_dragdrop_distance.js] +[browser_markup_dragdrop_draggable.js] +[browser_markup_dragdrop_dragRootNode.js] +[browser_markup_dragdrop_escapeKeyPress.js] +[browser_markup_dragdrop_invalidNodes.js] +[browser_markup_dragdrop_reorder.js] +[browser_markup_dragdrop_tooltip.js] +[browser_markup_events1.js] +[browser_markup_events2.js] +[browser_markup_events3.js] +[browser_markup_events_form.js] +[browser_markup_events_jquery_1.0.js] +[browser_markup_events_jquery_1.1.js] +[browser_markup_events_jquery_1.2.js] +[browser_markup_events_jquery_1.3.js] +[browser_markup_events_jquery_1.4.js] +[browser_markup_events_jquery_1.6.js] +[browser_markup_events_jquery_1.7.js] +[browser_markup_events_jquery_1.11.1.js] +[browser_markup_events_jquery_2.1.1.js] +[browser_markup_events-overflow.js] +skip-if = true # Bug 1177550 +[browser_markup_events-windowed-host.js] +[browser_markup_links_01.js] +[browser_markup_links_02.js] +[browser_markup_links_03.js] +[browser_markup_links_04.js] +subsuite = clipboard +[browser_markup_links_05.js] +[browser_markup_links_06.js] +[browser_markup_links_07.js] +[browser_markup_load_01.js] +[browser_markup_html_edit_01.js] +[browser_markup_html_edit_02.js] +[browser_markup_html_edit_03.js] +[browser_markup_image_tooltip.js] +[browser_markup_image_tooltip_mutations.js] +[browser_markup_keybindings_01.js] +[browser_markup_keybindings_02.js] +[browser_markup_keybindings_03.js] +[browser_markup_keybindings_04.js] +[browser_markup_keybindings_delete_attributes.js] +[browser_markup_keybindings_scrolltonode.js] +[browser_markup_mutation_01.js] +[browser_markup_mutation_02.js] +[browser_markup_navigation.js] +[browser_markup_node_names.js] +[browser_markup_node_names_namespaced.js] +[browser_markup_node_not_displayed_01.js] +[browser_markup_node_not_displayed_02.js] +[browser_markup_pagesize_01.js] +[browser_markup_pagesize_02.js] +[browser_markup_remove_xul_attributes.js] +skip-if = e10s # Bug 1036409 - The last selected node isn't reselected +[browser_markup_search_01.js] +[browser_markup_tag_edit_01.js] +[browser_markup_tag_edit_02.js] +[browser_markup_tag_edit_03.js] +[browser_markup_tag_edit_04-backspace.js] +[browser_markup_tag_edit_04-delete.js] +[browser_markup_tag_edit_05.js] +[browser_markup_tag_edit_06.js] +[browser_markup_tag_edit_07.js] +[browser_markup_tag_edit_08.js] +[browser_markup_tag_edit_09.js] +[browser_markup_tag_edit_10.js] +[browser_markup_tag_edit_11.js] +[browser_markup_tag_edit_12.js] +[browser_markup_tag_edit_13-other.js] +[browser_markup_tag_edit_long-classname.js] +[browser_markup_textcontent_display.js] +[browser_markup_textcontent_edit_01.js] +[browser_markup_textcontent_edit_02.js] +[browser_markup_toggle_01.js] +[browser_markup_toggle_02.js] +[browser_markup_toggle_03.js] +[browser_markup_update-on-navigtion.js] +[browser_markup_void_elements_html.js] +[browser_markup_void_elements_xhtml.js] +[browser_markup_whitespace.js] diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js new file mode 100644 index 000000000..7e94669c0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js @@ -0,0 +1,59 @@ +/* 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/. */ + +"use strict"; + +// Test inspector markup view handling focus and blur when moving between markup +// view, its root and other containers, and other parts of inspector. + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL( + "data:text/html;charset=utf-8,

foo

bar"); + let markup = inspector.markup; + let doc = markup.doc; + let win = doc.defaultView; + + let spanContainer = yield getContainerForSelector("span", inspector); + let rootContainer = markup.getContainer(markup._rootNode); + + is(doc.activeElement, doc.body, + "Keyboard focus by default is on document body"); + + yield selectNode("span", inspector); + + is(doc.activeElement, doc.body, + "Keyboard focus is still on document body"); + + info("Focusing on the test span node using 'Return' key"); + // Focus on the tree element. + rootContainer.elt.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + + is(doc.activeElement, spanContainer.editor.tag, + "Keyboard focus should be on tag element of focused container"); + + info("Focusing on search box, external to markup view document"); + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + is(doc.activeElement, doc.body, + "Keyboard focus should be removed from focused container"); + + info("Selecting the test span node again"); + yield selectNode("span", inspector); + + is(doc.activeElement, doc.body, + "Keyboard focus should again be on document body"); + + info("Focusing on the test span node using 'Space' key"); + // Focus on the tree element. + rootContainer.elt.focus(); + EventUtils.synthesizeKey("VK_SPACE", {}, win); + + is(doc.activeElement, spanContainer.editor.tag, + "Keyboard focus should again be on tag element of focused container"); + + yield clickOnInspectMenuItem(testActor, "h1"); + is(doc.activeElement, rootContainer.elt, + "When inspect menu item is used keyboard focus should move to tree."); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js new file mode 100644 index 000000000..41e35afef --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js @@ -0,0 +1,277 @@ +/* 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/. */ +/* import-globals-from helper_markup_accessibility_navigation.js */ + +"use strict"; + +// Test keyboard navigation accessibility of inspector's markup view. + +loadHelperScript("helper_markup_accessibility_navigation.js"); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * key {String} key event's key + * options {?Object} optional event data such as shiftKey, etc + * focused {String} path to expected focused element relative to + * its container + * activedescendant {String} path to expected aria-activedescendant element + * relative to its container + * waitFor {String} optional event to wait for if keyboard actions + * result in asynchronous updates + * } + */ +const TESTS = [ + { + desc: "Collapse body container", + focused: "root.elt", + activedescendant: "body.tagLine", + key: "VK_LEFT", + options: { }, + waitFor: "collapsed" + }, + { + desc: "Expand body container", + focused: "root.elt", + activedescendant: "body.tagLine", + key: "VK_RIGHT", + options: { }, + waitFor: "expanded" + }, + { + desc: "Select header container", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_DOWN", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Expand header container", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_RIGHT", + options: { }, + waitFor: "expanded" + }, + { + desc: "Select text container", + focused: "root.elt", + activedescendant: "container-0.tagLine", + key: "VK_DOWN", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Select header container again", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_UP", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Collapse header container", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_LEFT", + options: { }, + waitFor: "collapsed" + }, + { + desc: "Focus on header container tag", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_RETURN", + options: { } + }, + { + desc: "Remove focus from header container tag", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_ESCAPE", + options: { } + }, + { + desc: "Focus on header container tag again", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_SPACE", + options: { } + }, + { + desc: "Focus on header id attribute", + focused: "header.focusableElms.1", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Focus on header class attribute", + focused: "header.focusableElms.2", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Focus on header new attribute", + focused: "header.focusableElms.3", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Circle back and focus on header tag again", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Circle back and focus on header new attribute again", + focused: "header.focusableElms.3", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Tab back and focus on header class attribute", + focused: "header.focusableElms.2", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Tab back and focus on header id attribute", + focused: "header.focusableElms.1", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Tab back and focus on header tag", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Expand header container, ensure that focus is still on header tag", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_RIGHT", + options: { }, + waitFor: "expanded" + }, + { + desc: "Activate header tag editor", + focused: "header.editor.tag.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_RETURN", + options: { } + }, + { + desc: "Activate header id attribute editor", + focused: "header.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Deselect text in header id attribute editor", + focused: "header.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Activate header class attribute editor", + focused: "header.editor.attrList.children.1.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Deselect text in header class attribute editor", + focused: "header.editor.attrList.children.1.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Activate header new attribute editor", + focused: "header.editor.newAttr.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Circle back and activate header tag editor again", + focused: "header.editor.tag.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Circle back and activate header new attribute editor again", + focused: "header.editor.newAttr.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Exit edit mode and keep focus on header new attribute", + focused: "header.focusableElms.3", + activedescendant: "header.tagLine", + key: "VK_ESCAPE", + options: { } + }, + { + desc: "Move the selection to body and reset focus to container tree", + focused: "docBody", + activedescendant: "body.tagLine", + key: "VK_UP", + options: { }, + waitFor: "inspector-updated" + }, +]; + +let containerID = 0; +let elms = {}; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(`data:text/html;charset=utf-8, +

fooChild span

`); + + // Record containers that are created after inspector is initialized to be + // useful in testing. + inspector.on("container-created", memorizeContainer); + registerCleanupFunction(() => { + inspector.off("container-created", memorizeContainer); + }); + + elms.docBody = inspector.markup.doc.body; + elms.root = inspector.markup.getContainer(inspector.markup._rootNode); + elms.header = yield getContainerForSelector("h1", inspector); + elms.body = yield getContainerForSelector("body", inspector); + + // Initial focus is on root element and active descendant should be set on + // body tag line. + testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine); + + // Focus on the tree element. + elms.root.elt.focus(); + + for (let testData of TESTS) { + yield runAccessibilityNavigationTest(inspector, elms, testData); + } + + elms = null; +}); + +// Record all containers that are created dynamically into elms object. +function memorizeContainer(event, container) { + elms[`container-${containerID++}`] = container; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js new file mode 100644 index 000000000..ec217db09 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js @@ -0,0 +1,126 @@ +/* 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/. */ +/* import-globals-from helper_markup_accessibility_navigation.js */ + +"use strict"; + +// Test keyboard navigation accessibility is preserved after editing attributes. + +loadHelperScript("helper_markup_accessibility_navigation.js"); + +const TEST_URI = '
'; + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * key {String} key event's key + * options {?Object} optional event data such as shiftKey, etc + * focused {String} path to expected focused element relative to + * its container + * activedescendant {String} path to expected aria-activedescendant element + * relative to its container + * waitFor {String} optional event to wait for if keyboard actions + * result in asynchronous updates + * } + */ +const TESTS = [ + { + desc: "Select header container", + focused: "root.elt", + activedescendant: "div.tagLine", + key: "VK_DOWN", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Focus on header tag", + focused: "div.focusableElms.0", + activedescendant: "div.tagLine", + key: "VK_RETURN", + options: { } + }, + { + desc: "Activate header tag editor", + focused: "div.editor.tag.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_RETURN", + options: { } + }, + { + desc: "Activate header id attribute editor", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Deselect text in header id attribute editor", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Move the cursor to the left", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_LEFT", + options: { } + }, + { + desc: "Modify the attribute", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "A", + options: { } + }, + { + desc: "Commit the attribute change", + focused: "div.focusableElms.1", + activedescendant: "div.tagLine", + key: "VK_RETURN", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Tab and focus on header class attribute", + focused: "div.focusableElms.2", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Tab and focus on header new attribute node", + focused: "div.focusableElms.3", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: { } + }, +]; + +let elms = {}; + +add_task(function* () { + let url = `data:text/html;charset=utf-8,${TEST_URI}`; + let { inspector } = yield openInspectorForURL(url); + + elms.docBody = inspector.markup.doc.body; + elms.root = inspector.markup.getContainer(inspector.markup._rootNode); + elms.div = yield getContainerForSelector("div", inspector); + elms.body = yield getContainerForSelector("body", inspector); + + // Initial focus is on root element and active descendant should be set on + // body tag line. + testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine); + + // Focus on the tree element. + elms.root.elt.focus(); + + for (let testData of TESTS) { + yield runAccessibilityNavigationTest(inspector, elms, testData); + } + + elms = null; +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js new file mode 100644 index 000000000..b38a68c10 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js @@ -0,0 +1,100 @@ +/* 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/. */ + +"use strict"; + +// Test that inspector markup view has all expected ARIA properties set and +// updated. + +const TOP_CONTAINER_LEVEL = 3; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(` + data:text/html;charset=utf-8, +

foo

+ bar + `); + let markup = inspector.markup; + let doc = markup.doc; + let win = doc.defaultView; + + let rootElt = markup.getContainer(markup._rootNode).elt; + let bodyContainer = yield getContainerForSelector("body", inspector); + let spanContainer = yield getContainerForSelector("span", inspector); + let headerContainer = yield getContainerForSelector("h1", inspector); + let listContainer = yield getContainerForSelector("ul", inspector); + + // Focus on the tree element. + rootElt.focus(); + + // Test tree related semantics + is(rootElt.getAttribute("role"), "tree", + "Root container should have tree semantics"); + is(rootElt.getAttribute("aria-dropeffect"), "none", + "By default root container's drop effect should be set to none"); + is(rootElt.getAttribute("aria-activedescendant"), + bodyContainer.tagLine.getAttribute("id"), + "Default active descendant should be set to body"); + is(bodyContainer.tagLine.getAttribute("aria-level"), TOP_CONTAINER_LEVEL - 1, + "Body container tagLine should have nested level up to date"); + [spanContainer, headerContainer, listContainer].forEach(container => { + let treeitem = container.tagLine; + is(treeitem.getAttribute("role"), "treeitem", + "Child container tagLine elements should have tree item semantics"); + is(treeitem.getAttribute("aria-level"), TOP_CONTAINER_LEVEL, + "Child container tagLine should have nested level up to date"); + is(treeitem.getAttribute("aria-grabbed"), "false", + "Child container should be draggable but not grabbed by default"); + is(container.children.getAttribute("role"), "group", + "Container with children should have its children element have group " + + "semantics"); + ok(treeitem.id, "Tree item should have id assigned"); + if (container.closeTagLine) { + is(container.closeTagLine.getAttribute("role"), "presentation", + "Ignore closing tag"); + } + if (container.expander) { + is(container.expander.getAttribute("role"), "presentation", + "Ignore expander"); + } + }); + + // Test expanding/expandable semantics + ok(!spanContainer.tagLine.hasAttribute("aria-expanded"), + "Non expandable tree items should not have aria-expanded attribute"); + ok(!headerContainer.tagLine.hasAttribute("aria-expanded"), + "Non expandable tree items should not have aria-expanded attribute"); + is(listContainer.tagLine.getAttribute("aria-expanded"), "false", + "Closed tree item should have aria-expanded unset"); + + info("Selecting and expanding list container"); + let updated = waitForMultipleChildrenUpdates(inspector); + yield selectNode("ul", inspector); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + yield updated; + + is(rootElt.getAttribute("aria-activedescendant"), + listContainer.tagLine.getAttribute("id"), + "Active descendant should not be set to list container tagLine"); + is(listContainer.tagLine.getAttribute("aria-expanded"), "true", + "Open tree item should have aria-expanded set"); + let listItemContainer = yield getContainerForSelector("li", inspector); + is(listItemContainer.tagLine.getAttribute("aria-level"), + TOP_CONTAINER_LEVEL + 1, + "Grand child container tagLine should have nested level up to date"); + is(listItemContainer.children.getAttribute("role"), "presentation", + "Container with no children should have its children element ignored by " + + "accessibility"); + + info("Collapsing list container"); + updated = waitForMultipleChildrenUpdates(inspector); + EventUtils.synthesizeKey("VK_LEFT", {}, win); + yield updated; + + is(listContainer.tagLine.getAttribute("aria-expanded"), "false", + "Closed tree item should have aria-expanded unset"); +}); + diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js new file mode 100644 index 000000000..fd32251d0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js @@ -0,0 +1,44 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test native anonymous content in the markupview. +const TEST_URL = URL_ROOT + "doc_markup_anonymous.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + let pseudo = yield getNodeFront("#pseudo", inspector); + + // Markup looks like:
<::before /><::after />
+ let children = yield inspector.walker.children(pseudo); + is(children.nodes.length, 3, "Children returned from walker"); + + info("Checking the ::before pseudo element"); + let before = children.nodes[0]; + yield isEditingMenuDisabled(before, inspector); + + info("Checking the normal child element"); + let span = children.nodes[1]; + yield isEditingMenuEnabled(span, inspector); + + info("Checking the ::after pseudo element"); + let after = children.nodes[2]; + yield isEditingMenuDisabled(after, inspector); + + let native = yield getNodeFront("#native", inspector); + + // Markup looks like:
+ let nativeChildren = yield inspector.walker.children(native); + is(nativeChildren.nodes.length, 1, "Children returned from walker"); + + info("Checking the video element"); + let video = nativeChildren.nodes[0]; + ok(!video.isAnonymous, "